2011年3月22日火曜日

32bit OSに物理メモリを大量に搭載させた場合の問題点


デーモンとして稼動していたプロセスがメモリ不足のためダウンする事象が発生した。
調査していくと、大量の物理メモリを搭載させた32bit OS特有の問題であることが判明した。

事象を整理しておく。


環境
 x86アーキテクチャ 32bit OS
 PAE対応kernel(kernel-smp 64G)
メモリ:24G (8プロセス × 3GB 用)



事象
OOM-Killer(Out of Memory Killer)が発生し、プロセスが強制終了させられた。
32bitOSで1プロセスが利用できるプロセス上限には達していなかった。
1プロセスが利用するメモリは3GB以下であった。


ログのエラー内容

$ cat /var/log/messages
09:00:01 XXXXX kernel: Normal free:3120kB min:3756kB low:4692kB high:5632kB
active:6068kB inactive:128kB present:901120kB pages_scanned:156118 all_unreclaimable?

09:00:01 XXXXX kernel: HighMem free:11078260kB min:512kB low:26348kB high:52188kB
active:12065708kB inactive:1043652kB present:24772604kB pages_scanned:0
all_unreclaimable? no


Normal(Low)ゾーンの最小値は3756kBである。
空き領域が3120kBと下回っていることから、Normalゾーンが不足していることが分かる。

原因はNormalゾーンの不足にあるようだ。

Normalゾーンとは何か。
原因を説明する前に32bit OSでのメモリのレイアウトについて簡単に説明しておく。



メモリの予備知識
現在のメモリのレイアウトと使用状況は下で確認できる。
cat /proc/meminfo

meminfoから確認できるメモリ内訳は下の通りである。

MemTotal = MemFree + Active + Inactive + Slab + VmallocUsed + PageTables
Active + Inactive = AnonPages + Buffers + Cached + SwapCached

MemTotal     : システム全体で利用できる物理メモリの総容量(不変)。
 MemFree      : システム全体で利用できる物理メモリの空き容量。
 Active       : 最近アクセスした(とカーネルが思っている)物理メモリの容量。
 Inactive     : 最近アクセスしていない(とカーネルが思っている)、
                解放してよい物理メモリの容量。
 Slab         : スラブアロケータで使用されている物理メモリの総容量。
 VmallocUsed  : vmalloc()で確保された物理メモリ領域とMMCONFIGで確保している
                 メモリ領域の総容量。
 PageTables  : ページテーブルエントリという「ページ」の管理構造体として
                利用されているメモリ領域。
 AnonPages    : 無名ページ(Anonymous Page)の領域。
                ユーザープロセスがmalloc()などで確保したり、
                プログラム本体用に利用するメモリ領域。
 Buffers      : ファ イルなどのメタデータとして使用している物理メモリの総容量。
 Cached       : ファ イルデータのキャッシュなどに使用している物理メモリの総容量。
                共有メモリは Cached に加算される。SwapCachedは含まない。

そして、x86アーキテクチャ(32bit)にのみ存在する概念が以下である。

 LowTotal : Normal(Low)ゾーンにある物理メモリの容量。
 HighTotal : Highゾーンにある物理メモリの容量。

MemTotal = LowTotal + HighTotal
x86_64アーキテクチャ(64bit)の場合は、すべてのメモリがLow側で扱われる。

では、High、Lowとは何か。

Linuxではプロセスごとに仮想メモリ空間を割り当てる。
各プロセスでは4Gバイトのメモリ空間を利用できるはずである。
しかし実際にプロセスが(ユーザーモードで)利用できるのは先頭から3Gバイトである。
残りの1Gバイトをカーネルが(カーネルモードで)利用するためである。

プロセスが利用する先頭から3Gバイトの領域をプロセス空間と呼び、
残りの1Gバイトの領域をカーネル空間と呼ぶ。
そしてそれぞれはHigh、Lowとも呼ばれる。

そして実は、カーネル空間が1GBをすべて使うわけではない。

1GBの内訳
 カーネル空間
カーネルが必要な実メモリに直接アクセスするためのマッピング領域。
最大で896MBである。

 Highmemアクセス領域
実メモリの大きさが896MBを超える場合、つまりストレートマッピング
できない実メモリ(Highmem)にアクセスるるための領域。


その他として、固定マップ領域もあるがここでの説明は割愛する。

プロセスが異なると仮想メモリ空間も異なる。
ただし、カーネル仮想空間の部分については全プロセスで共用される。



原因
物理メモリを増やしたことで、Normal(Low)領域が減少していた。

Low領域が減少する理由は2つある。
(1)
PTE(Page Table Entry)は、Linuxでは4KB単位でメモリーを管理している。
搭載メモリー量が大きくなれば管理領域のサイズも大きくなる。
管理領域はLowmem側に記録されるため、LowTotalの値が小さくなる。
物理メモリが4GBが増えるごとに、32MB程度のLowメモリが余分に
Highmemアクセス領域に確保される。


/proc/meminfo 内を見る限りでは、LowTotalが減った分、
HighTotalが増えているように見えるが、これは表示上の問題であろう。

論理的には、PAE(Physical Address Extension)対応のkernelは64GBのメモリを認識できる。
しかし、PTEの領域が巨大になり、Low領域のリソースが不足する。
現実的には、システムを安定動作させるために12GB未満の物理メモリで運用することが望まれる。

また下のデメリットを指摘しているサイトもあった。

PAE対応のkernelはそうでないkernelを使用した場合よりも、 
最大で50%の性能低下があります。
これは、PAE対応のkernelでは、PTEの階層が1つ多いために、
TLBがミスヒットした場合の ペナルティーが大きいためです。
また、メモリを大量に管理しているため、TLBのヒット率そのものがあまり高くありません。 


(2) 
kernelのキャッシュ・バッファ用に確保されるLowメモリの増加
もう一つの理由がこれである。
物理メモリが4GBが増えるごとに、32MB程度のLowメモリが、
kernelキャッシュ・バッファ用にカーネル空間に余分に確保されていく。


物理メモリを増やし、プロセスも多く起動させたことで、
Kernel空間がメモリのLow領域を使いきり、KernelがOOM-Killerを発動した。
その際にプロセスの一つがkillされた。
どのプロセスを停止するかはKernelが決定する。



対策
物理メモリを減らさない、という条件で可能な対策を検討する。

(1)は対処療法である。
(2)、(3)、(4)、(5)はチューニングである。
(6)が根本解決である。


(1) 不要なプロセスを停止させ、Low空間をより多く確保する
OOM-Killerが発動するまでの時間を延ばすだけで根本解決にはならない。


(2)  Low領域が足りなくなった場合に、High領域を積極活用する。
チューニングキーのパラメータは3つあり、
それぞれ左から、DMA領域、Normal(Low)領域、High領域用の値になる。

パラメータ値の意味を下の例を使って説明する。

1GBのメモリを積んでおり、内訳は次の通りであるとする。
DMA(16MB)、Normal(784MB)、High(224MB)。
実際に物理メモリがどのような領域に分けられているかは、
起動時のメッセージから読み取れる(dmesgコマンドを使えばよい)。

さて、Normal領域が足りなくなったらどうなるか。
例えば、High領域からメモリが確保される。
224(MB) / 32(High領域のパラメータ) = 7(MB)

パラメータが小さいほど、各領域から転用される容量は増していくことが分かる。
現実的ではないが、パラメータを1にすれば、
特定の領域からメモリを100%確保できる。

 現在値
cat /proc/sys/vm/lowmem_reserve_ratio
256     256     32

 設定変更
echo "256     256     16" > /proc/sys/vm/lowmem_reserve_ratio

 恒久設定
sysctl -w vm.lowmem_reserve_ratio = 256 256 16

 問題点
各領域は元々意図に沿った用途があるわけで、
他の目的のために使い回すことで、その領域が今度は不足することが想定される。


(3) キャッシュ上でまだディスクに書き込まれていない領域(dirty領域)の割合を小さくし、解放を早める。
 現在値
cat /proc/sys/vm/dirty_background_ratio
5 ← 単位は全物理ページに対する割合(%))

 設定変更
echo 2 >  /proc/sys/vm/dirty_ratio

 恒久設定
sysctl -w vm.dirty_ratio=2

 問題点
スループットの低下


(4) キャッシュ上に存在しているページの存在時間を短くし、ライトバックの頻度を早める。
 現在値
cat /proc/sys/vm/dirty_expire_centisecs
2999 ← 単位はms

 設定変更
echo "400" > /proc/sys/vm/dirty_expire_centisecs

 恒久設定
sysctl -w vm.dirty_expire_centisecs=400

 問題点
スループットの低下


(5) Cache領域の開放を早める。
Low、Highの両方の領域から解放可能なキャッシュメモリを解放する。
 現在値
cat /proc/sys/vm/drop_caches
0

 設定変更
ページキャッシュのみを解放したい場合
echo 1 > /proc/sys/vm/drop_caches

Slabキャッシュを解放したい場合
echo 2 > /proc/sys/vm/drop_caches

ページキャッシュとSlabキャッシュを解放したい場合
echo 3 > /proc/sys/vm/drop_caches

 恒久設定
sysctl -w vm.drop_caches=XXX

 問題点
スループットの低下。
キャッシュのヒット率が低ければ問題ないか。


(6) 64bitOSを使う
 問題点
アプリケーションが対応していない場合がある。
補足だが、64bitOSに32bitのアプリケーションをインストールした場合は、
そのアプリケーションはどこまでメモリを使えるだろうか。
答えは4GBである。理由は以上を読んでもらえれば分かるだろう。