Linuxのメモリーの使われ方
1.はじめに
こんにちは。株式会社SHIFT ITソリューション部の力石です。
主にお客さまのアプリケーション開発プロジェクトで、 性能改善、品質向上などの非機能関連のコンサルティングをしています。
システムの性能分析をする際にメモリーはとても重量な項目となります。
メモリー使用量はfreeコマンドで簡単に求められますが、その項目が何を意味してどのように性能に影響するかを把握していますか?
この記事ではLinuxでの実験結果もあわせて説明をしていきます。
2.いきなり結論
メモリーの状況を図解すると以下となります。
メモリーの使われ方を調べるにはfreeコマンドが便利。定期的に取得し推移を見てから判断をする。
freeは利用可能なメモリーサイズではない。freeの値が全て使えるわけではない。
availableはOSが求めた使用可能量だが、ファイルキャッシュで使用するサイズも含まれているため考慮が必要。
※availableが表示されるのはLinuxカーネルのバージョン3.14、RHEL 7アップデート以降。avaiable表示されない場合はfree+buff/cacheで代用可能。cacheはディスク書き込み/読み込みのキャッシュとして使用するので、性能に大きく影響。
メモリー状況は多くの場合freeで取得可能。詳細が知りたい場合は/proc/meminfoを参照。
状況取得は1回だけではなく、継続的に取得し推移を見て判断が必要。
3.間違った判断
実際に体験した間違った判断の例を2つあげます。
例1
既存のサーバーにソフトウェアーを入れることになり、ソフトの要件としてメモリー使用量500MBとなっていました。
現在の状況をfreeコマンドで調査したところ以下のようになっていました。
# free -m
total used free shared buff/cache available
Mem: 80429 58906 579 1,028 20943 9662
Swap: 20480 1402 19077
判断方法
freeコマンドの結果より次のように判断しました。
free(空き)が579MBある。要件は500MBなので足りている。したがって問題なし。
availableは9,662MBもあり、要件は500MBなのでこの値から考えても問題なし。
実際の動き
実際にソフトウェアーをインストールし起動したところ、新規導入ソフトウェアは動作しましたが、既存アプリの性能が劣化しました。
例2
既存サーバーで性能障害が発生しており、動作が毎日定期的に遅くなっていました。
判断方法 CPU、メモリー、I/Oについてそれぞれ次のように判断しました。
CPU使用率を見ても50%以下で余裕があるのでCPUネックではないと判断しました。
swapを使用しているためそれによる性能劣化が考えられるが、遅くなる時間帯とswapの使用状況に相関がない。またfree(空き)も800MB程度あるためめ、メモリー不足ではないと判断しました。
nfs先にファイル出力しているので、ネットワークに関連した劣化が考えられるが、遅くなる時間帯と出力ファイル量に相関がない。ネットワーク回線自体も逼迫していない。
この2つの例において、判断のどこが間違っているかわかりますか?
このあと実験をしながら何が間違っているのか説明をしていきます。
4.メモリーの状況取得
メモリー状況の概要を知るにはfreeコマンドがお勧めです。freeコマンドの出力項目の意味が分かれば、vmstat,topなどのメモリー関連の項目も理解できます。
freeコマンドは/proc/meminfoを参照しています。meminfoには40項目程度あり、freeコマンドより詳細に調査が必要になった場合に参照するのがお勧めです。
freeの結果の意味は以下となります。
5.freeコマンドを使用した分析方法
私がfreeコマンド結果を使用した性能分析方法は次のようになります。
freeコマンドを1回実行するだけではなく、1分ごとなど定期的に取得しそれをグラフにします。グラフにすることで変化の様子が視覚的に分かるのでお勧めです。
コマンド・出力例 出力をMB単位で表示。60秒間隔で取得
# free -m -s 60
total used free shared buff/cache available
Mem: 80429 58906 579 1,028 20943 9662
Swap: 20480 1402 19077
:
分析方法
totalは物理的にメモリーを増やさないと変化しないので一度だけ確認し、全メモリー・swapサイズを把握します。
Swap:usedをみて変動している状態だとメモリー不足です。
メモリーのfreeを見て小さすぎないことを確認します。グラフで推移を確認し底を打っている状態だとメモリー不足です。
freeが少なくなりすぎるとcacheを減らしたりswap outしたりしてfreeが一定値を下回らないようにOSが制御をします。これまでの経験だとtotalが20~70GB程度のシステムだと最低でも150~800MBくらいを確保するように動きます。buff/cacheが小さすぎないことを確認します。システムがファイルI/Oをどのくらいにするかにもよりますが、この値が小さくfreeも小さい場合はメモリー不足でキャッシュに割り当てる領域が足りずファイルの読み書きに時間がかかっている可能性があります。ファイルI/OはアプリケーションだけではなくOS自体もおこなっておりログインできないなどOS自体もフリーズ状態になることがあります。
6.実験1:メモリー使用プロセス実行時のメモリー状況
作業内容
実メモリーを多く使用するプロセス(※)を段階的に多重実行していきfreeの値を下げます。その後も18多重までプロセスを増やしswap outする状況にします。 その後段階的にプロセスを止めていきます。 この間のfreeコマンドの結果を取得します。 ※「10. メモリー使用プログラム」参照
結果
freeコマンドの結果をグラフ化したものが以下です。
分析
グラフから以下のことが分かります。
freeとavaiableは同程度の数値となっています。理由は、avaiable≒free+buff/cacheとなり、buff/cacheの値が低いためです。
プロセスを起動するごとにusedが増えています。freeが0に近くなった後はこれ以上メモリー確保ができないのでSwap:usedが増えています。これよりプロセスがメモリーを必要としているがfreeが少ない場合は、swap outしてプロセスにメモリーを割り当てていることが分かります。
プロセスを停止していくと最初にswap outが解消していき、swapが全て解放されるとその後はMem:usedが解放されていきます。
この処理ではディスクI/Oが無いため、buff/cacheの値は低いままで変化がありません。
メモリー逼迫時にswap outが増え続けている間もfreeのサイズは0にはなっておらず、150MB程度となっています。
これはfreeの最低値はカーネルパラメータのvm.min_free_bytesによって設定しており、それを下回らないようにOSが管理をしているためです。
3環境で実測した内容が以下となります。3環境ともswap使用量が1GB以上で増え続けているときなのでメモリーが逼迫している状態です。 パラメータと実測値を比較すると125~803%とパラメータ値とは大きく差があります。そのためパラメータ値は参考値程度と捉えた方が良さそうです。
7.実験2:ファイル書き込みのメモリー状況
作業内容
1GBのファイルを出力するコマンドを順次実行していきキャッシュがいっぱいになるように80回繰り返します。その後キャッシュに書き込まれた内容がディスクに書き込まれるまで以下の結果を取得します。
freeコマンドの結果
/proc/meminfoのDirtyの結果
ディスクへの書き込みは以下のコマンドで実行します。
dd if=/dev/zero of=1GB.txt bs=1M count=1024
結果
メモリーの状況をグラフ化したものが以下です。
1つ目:freeの結果
2つ目:/proc/meminfoのDirtyとdd書き込み速度
分析
グラフから以下のことが分かります。
freeとbuff/cacheは逆の動きをしています。これよりfreeだった部分がcacheとして使用されていることが分かります。
buff/cacheは15GBくらいになるまでは急激に増えていますが、その後は増加量が鈍化しています。
鈍化したタイミングは2つめのグラフを見ますと、ファイル書き込み速度が急激に落ちている・Dirtyの値が一定になったのと同じタイミングになります。
書き込み用のキャッシュは/proc/meminfoではDirtyで表示されます。
これはディスクへの書き込みにキャッシュが使われており、最初は速かったです。その後、書き込み用のキャッシュがいっぱいになり、ディスクへの書き込みを待っている状態となっています。書き込みのキャッシュがいっぱいになった後もbuff/cacheのサイズは上昇しており、元々freeだったサイズのほとんどがbuff/cacheになっています。これはディスク書き込み時には必ず書き込み用のキャッシュが使われます。書き込み用のキャッシュの内容はOSのバックグラウンドプロセスにてディスクへ書き込まれます。ディスクにまだ書き込んでいない状態をDirtyと言います。書き込んだ時点でDirtyでなくなります。この時点で書き込み用のキャッシュが減り、読み込み用のキャッシュとなるためbuff/cacheのサイズが上昇していきます。
ddの書き込み速度は書き込み用キャッシュがいっぱいになる前は約3,300MB/sだった物が、いっぱいになると約120MB/sとなっており、約28倍も性能劣化しています。
avaiableは77.2GB付近で一定となっています。これより書き込みにキャッシュを使用してもavaiableには影響がないことが分かります。
書き込み用のキャッシュサイズの最大値はカーネルパラメータのvm.dirty_ratioで定義されています。以下の結果より20%になっていることが分かります。
# sysctl vm.dirty_ratio
vm.dirty_ratio = 20
8.実験3:ファイル読み込み時のメモリー状況
作業内容
前半は、実メモリーを多く使用するプロセスを複数動かし、buff/cacheがほぼ0、freeがほぼ0、メモリーのほとんどがusedになうようにします。その後1GBのファイル10個を5回呼び出し、読み込み時間を取得します。
後半は、実メモリーを多く使用するプロセスを全停止します。その後1GBのファイル10個を5回呼び出し、読み込み時間を取得します。
これらの処理実行時のメモリー状況をfreeコマンドで取得します。
ディスクからの読み込みは以下のコマンドで実行します。
# wc -l 1GB_xxxx.txt
※ファイル名のxxxxの部分は変えて10ファイル作成しておきます。
結果
freeコマンドの結果と読み込み時間をグラフ化したものが以下です。
分析
グラフから以下のことが分かります。
前半(18:25~18:32ころ)は、メモリーを多く使用するプロセスを動かしているためfree、avaiable、buff/cacheは少なくなっています。このときはディスクからの読み込みは全て7秒程度と遅くなっています。これより読み込んだ内容はキャッシュには保存されないため、同じファイルを何度読んでもディスクから読み取り速度は遅くなっていることが分かります。
後半(18:32~18:33ころ)は、メモリーを多く使用するプロセスを停止したため、free、avaiableの値が全メモリーのほとんどを占めています。ファイルを読み込んだ際は7ファイル目までは前半とほどと同様に7秒程度かかっていますが、8ファイル目は5秒台、9,10ファイル目は0.1秒程度になっています。
1ファイル目を読み込んだ時からbuff/cacheの値が上昇しており、8ファイル目の途中から一定となっています。これより1ファイル目から8ファイル目の途中まではキャッシュに載っていないために先ほどと同じ時間がかかりましたが、8ファイル目の途中からから10ファイル目まではキャッシュに残っており、そのために速くなっています。
2巡目からは全てキャッシュに載っているため0.1秒程度で読み込みが終わっています。グラフ右下の紫の点は42回分の結果です。これよりディスクキャッシュの効果により約7秒が0.1秒となり、約70倍高速化していることが分かります。
9.『3.間違った判断』の振り返り
これまで説明した内容を基に『3.間違った判断』で例示した内容をどのように判断すべきか 考えてみます。
例1
新たにインストールしたアプリケーションで使用できる容量は以下の式で求められます。
「freeのavaiable」-「OSが確保するfreeの最低量」-「書き込み用キャッシュ」-「読み込み用キャッシュ」
各項目の取得方法は以下となります。
※上記の値をアプリの使用状況が一巡する期間定期的に取得し変化状況を求めます。例:1週間、毎分取得
※定期的に取得する理由は、例えばアプリの状況は日中と夜間、平日と休日で処理状況が変わってくる。またログ取得処理など定期的に動かしている処理があるため、メモリー使用状況は時々刻々変わっている。したがって1回の取得だけでは判断を誤ってしまうためです。
例2
メモリーの使用状況を確認するには例1で示したように定期的に変化状況を取得し、それらを視覚的分析できるようにグラフ化することが重要です。その際にシステムの特性により取得間隔は違ってきますがまずは1分間隔で取得し分析しながら調整していきましょう。
今回の例では、freeコマンドのfree項目が時間帯によって上下しており、低いときは700MBで頭打ち、freeコマンドのbuff/cacheが200MBとなっていました。この状態なのでメモリー不足になっていることは明らかですが、性能障害に結びつけることが出来ていませんでした。
まずは性能障害が発生する時間に何の処理が動いているかを調査したところ、ログファイルのバックアップ処理が動いていました。この状況までは担当者で分析をしていましたが、原因まではたどり着けていない状態でした。
その後調査を引き継ぎバックアップ処理をみたところ次のロジックとなっていました。
複数のログファイルをtarコマンドで1つのtarファイルにまとめる。
tarファイルをgzipコマンドで圧縮してtar.gzファイルにする。
複数ログファイルの合計サイズは10GBあり、圧縮後のファイルは1GBありました。この状況において必要なキャッシュは次のようになります。
キャッシュとして必要な領域は、読み込み用として領域1と領域3の各10GB、書き込み用として領域2の10GBと領域4の1GBの合計11GBとなります。ファイルアクセスは全部で31GBとなり、キャッシュが200MBしか無いためキャッシュの効果は低い状況となっています。ディスクI/Oが100MB/秒とした場合この処理だけで317秒かかってしまいます。(31GB×1024÷100MB/秒≒317秒)
またOSやプリケーションが性能劣化をしているのは、これらも量は少ないがディスクI/Oをおこなっているため性能に影響しています。
対応策としてメモリー増強がありますが、このシステムではH/W制約でメモリーが増やせません。
バックアップ処理の改善方法として、一度tarファイルを作成しその後tar.gzを作成し、tarファイルは中間ファイルの位置付けなのでその後削除しています。tarファイルの作成自体は不要なためtarコマンドにzオプションを付けて、複数のログファイルから直接tar.gzファイルを作成するように修正しました。
この修正により定期的に遅くなる性能障害を回避することが出来ました。
10.まとめ
メモリーの使われ方はfreeコマンドで定期的に取得し、その推移をグラフ化して分析してください。
メモリー不足により性能問題が起きているか判断するときは、プロセスが使用しているメモリーだけでなくキャッシュの使用状況をにも注意をして判断してください。ファイル書き込みは一度キャッシュ書き込んで処理は終了しますが、バックグラウンドプロセスがディスクへの書き込みをおこなっています。など複雑の動きをしているため内部でどのように動いているか特定しないと解決が難しいです。
みなさんも簡単な問題から解決していき、小さな疑問や気になったことを調べることを通して、技術的に成長につなげていただけると良いのではないかなと思います。
11.メモリー使用プログラム
単にメモリーを使用するコマンドを探しましたが見つけられなかったので以下の方法で作成しました。
実行までの手順は以下となります。
任意のディレクトリーに以下の内容で memuse.c ファイルを作成する。
/*
* メモリーを確保し、確保したメモリーを使い続けることでswap outしづらくする
* 使用方法: memuse <取得サイズMB>
* パラメータの最大:2047(約2GB)
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define PAGESIZE 4096 /* "getconf PAGESIZE"コマンドで得たサイズ。swap outはこのサイズ単位で行われるため、このサイズ内にアクセスすることでswap outを防ぐ */
#define TRUE 1
int main (int argc, char *argv[])
{
char (*array)[PAGESIZE] ;
int page_count ; /* パラメータで与えられたサイズが何ページになるか */
int count1 ;
int sum ; /* ページ毎にアクセスした結果を入れる場所 */
if (argc != 2) { /* パラメータの存在チェック */
fprintf (stderr, "使用方法: memuse <取得サイズMB>\n パラメータの最大:2047(約2GB)\n") ; /* 最大サイズは環境によって変わる可能性あり */
exit (1) ;
}
if (atoi(argv[1]) > 2047 || atoi(argv[1]) == 0) { /* パラメータが有効範囲か? 英字などatoi()で0になる場合はエラーとする */
fprintf (stderr, "使用方法: memuse <取得サイズMB>\n パラメータの最大:2047(約2GB)\n") ; /* 最大サイズは環境によって変わる可能性あり */
exit (1) ;
}
page_count = atoi (argv[1]) * 1024 * 1024 / PAGESIZE ; /* ページ数を計算 */
array = malloc (atoi(argv[1]) * 1024 * 1024) ;
if (array == NULL) {
fprintf (stderr, "mallocに失敗しました\n") ;
exit (1) ;
}
while (TRUE) {
for (count1 = 0 ; count1 < page_count ; count1 ++) {
array[count1][0] = rand() % 256 - 128 ; /* コンパイラーの最適化により処理が削除されないように値を設定 -128~127を生成 */
}
sum = 0 ;
for (count1 = 0 ; count1 < page_count ; count1 ++) { /* ページの先頭バイトの値にアクセスする */
sum += array[count1][0] ;
}
printf ("%d\n", sum) ; /* コンパイラーの最適化により処理が削除されないように値を表示 */
sleep (10) ; /* swap outされにくいように10秒に1回アクセスする */
}
}
2.コンパイル
make memuse
3.実行
./memuse 1024
※1024MBのメモリーを使用する。
※実行後はCTRL/Cやkillするまで止まりません。
《この公式ブロガーのおすすめ記事》
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/