さくらインターネット Advent Calendar最終日は、硬派にLinuxのメモリに関する基礎知識についてみてみたいと思います。
最近はサーバーを意識せずプログラミングできるようになり、メモリの空き容量について意識することも少なくなりましたが、いざ低レイヤーに触れなければいけないシチュエーションになった際に、OSを目の前に呆然とする人が多いようです。
基本的にLinux のパフォーマンスについて、メモリをたくさんつめばいいとか、スワップさせないほうが良い とか、このあたりは良く知られたことだと思います。 ただ、なんとなく ps コマンドや free コマンド などの結果を見るだけでなく、もう少しメモリのことについて掘り下げてみてみたいと思います。
メモリとキャッシュ
Linux におけるメモリの状態を大きく分けると「使用中のメモリ」「キャッシュ」「空きメモリ」「スワップ」の 4 つに分けられます。
そして、スワップについては HDD に存在し、それ以外は実メモリに存在し、HDD は実メモリより低速です。
使用中のメモリとはカーネルやアプリケーションなどのプロセスによって使用されているメモリの事です。キャッシュとは HDD アクセスなど I/O の高速化を行うためのキャッシュとして利用されるメモリの事です。
- 使用中のメモリが増えると、空きメモリが減る。
- 空きメモリがある限り、基本的にキャッシュはどんどん増加する。
- 空きメモリが無くなれば、使用中のメモリの増加に応じて、キャッシュが減る。
- キャッシュに割り当てられるメモリが無くなると、I/O のパフォーマンスは低下する。
- 使用中のメモリが実メモリより大きくなりそうになると、あふれる部分をスワップへ書き出す。
これを見ればわかるとおり、必要とされるメモリが増えると、キャッシュに使用できるメモリが減るため、I/O のパフォーマンスが低下しますし、メモリがあふれるとスワップが使用されるために I/O のパフォーマンスはますます低下します。
そのため、使用中のメモリが実メモリよりも小さく、キャッシュが確保され、スワップが使用されない状態が理想であり、恒常的にスワップが利用される状況の場合には、メモリの増設などを検討しなければなりません。
メモリの状態を確認する
次に、実際のメモリ使用状況を確認してみます。
メモリの使用状況は free コマンド、もしくは vmstat コマンドなどで確認することができます。
# free
total used free shared buffers cached
Mem: 1924020 206496 1717524 0 184 8280
-/+ buffers/cache: 198032 1725988
Swap: 4128760 0 4128760
# vmstat 1
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
~略~
0 0 0 1717524 184 8280 0 0 0 0 15 13 0 0 100 0 0
この状況では、空きメモリが 1.7GB(1717524KB)ほどあり、キャッシュに 184KB と 8MB(8280KB)使われていることが分かります。
なおキャッシュには 2 種類があり、cache で示されるページキャッシュと buff で示されるバッファキャッシュが存在します。 ページキャッシュはファイルシステムに対するキャッシュであり、ファイル単位でアクセスするときに使用されるキャッシュです。
例えば、ファイルへデータを書き込んだときは、ページキャッシュにデータが残されるため、次回の読み込み時には HDD にアクセスすることなくデータを利用できます。 もうひとつのバッファキャッシュはブロックデバイスを直接アクセスするときに使用されるキャッシュです。
キャッシュについて考察する
それでは、dd と vmstat を使用してページキャッシュの挙動を見てみたいと思います。
おそらくスワップはまだ利用されていないと思いますが、念のためにスワップをオフ/オンし、スワップが利用されていない状態にします。
# free
total used free shared buffers cached
Mem: 1924020 448076 1475944 0 3396 272788
-/+ buffers/cache: 171892 1752128
Swap: 4128760 74952 4053808
# swapoff -a && swapon -a
# free
total used free shared buffers cached
Mem: 1924020 401084 1522936 0 3524 273492
-/+ buffers/cache: 124068 1799952
Swap: 4128760 0 4128760
free コマンドの表示を見ると、スワップアウトが 0 であることが分かります。
次にキャッシュをクリアしておきます。
# echo 1 > /proc/sys/vm/drop_caches
次に dd を実行し、HDD に書き込みを行います。
# dd if=/dev/zero of=test bs=1M count=512
512+0 records in
512+0 records out
536870912 bytes (537 MB) copied, 1.76426 s, 304 MB/s
vmstat の表示を見るとファイルへの書き込み(io の bo)が発生し、IO ウェイト(cpu の wa)が上昇していることがわかります。 またページキャッシュの使用量が 500MB 強ほど増えており、ファイルを書き込んだタイミングでページキャッシュが使用されたことが分かります。
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 1717524 184 8280 0 0 0 0 13 9 0 0 100 0 0
0 2 0 1326248 1328 455184 0 0 176 177156 653 286 1 32 0 67 0
0 1 0 1248508 1332 531856 0 0 160 125952 421 199 0 9 0 91 0
0 0 0 1250740 1336 531860 0 0 4 90108 253 32 0 3 16 81 0
0 0 0 1257064 1336 531872 0 0 0 0 118 11 0 0 100 0 0
~中略(30秒間)~
0 1 0 1248260 1352 531864 0 0 4 98308 90 22 0 3 83 14 0
0 0 0 1255700 1352 531880 0 0 0 32764 214 13 0 1 77 22 0
ちなみに、ファイルへの書き込みは 1.76 秒で終了していますが、遅延書き込み(ライトバック)が有効な環境では flush というカーネルスレッドによって非同期で HDD への書き込みが行われます。 上記の例でも、書き込み直後は 393,216 ブロック(384MB)のみ書き込まれており、30 秒後の書き込みをあわせて 524,288 ブロック(512MB)となっていることが分かります。
procs の b が一時的に 2 になっているのは、dd とは別に flush が動作しているためです。dd がすでに終了している、30 秒後の書き込み時に procs の b が 1 になっているのも、flush が動作して非同期書き込みしたためです。
次に先ほど書き込んだデータを読み込んでみます。
# dd if=test of=/dev/null bs=1M count=512
512+0 records in
512+0 records out
536870912 bytes (537 MB) copied, 0.277886 s, 3.9 GB/s
ページキャッシュに保存されているため 0.28 秒と一瞬で完了しており、vmstat の結果を見るとディスク IO は発生せず(bi が 0 になっている)、IO ウェイトもありません。
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 1255064 2272 532704 0 0 0 0 139 27 0 11 89 0 0
これで、書き込みしたデータがページキャッシュにキャッシュされ、読み込み時に使用されたことが分かりました。
遅延書き込みを確認する
先ほど、flush というカーネルスレッドにより非同期で HDD への書き込みがおこなわれていることを解説しました。iotop コマンドを利用すればリアルタイムに IO の状況を確認できますので、iotop を実行しながら書き込みを試してみましょう。
vmstat を実行しているターミナルもしくは、新しいターミナルを開き、iotop コマンドを実行してください。
# iotop
まずは、direct オプションを付けて、ページキャッシュを使わない指定をして実行します。
# dd if=/dev/zero of=test bs=1M count=512 oflag=direct
すると、iotop では dd プロセスだけが IO を使用していることが分かります。
Total DISK READ: 0.00 B/s | Total DISK WRITE: 59.42 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
2069 be/4 root 0.00 B/s 59.42 M/s 0.00 % 99.49 % dd if=/dev/zero of=test bs=1M count=1024 oflag=di
1 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % init
2 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kthreadd]
ちなみに、このときの vmstat は以下のようになり、test というファイルに割り当てられていた 500MB強のページキャッシュが解放されたことが分かります。また、procs の b は 1 であり、1 プロセスのみが IO を利用していることが分かります。
procs -----------memory---------- ---swap-- -----io0---- -system-- -----cpu------
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 1039880 40100 700096 0 0 0 0 25 25 0 0 100 0 0
0 1 0 1570468 40108 175808 0 0 0 32824 159 131 0 7 69 24 0
0 1 0 1570468 40108 175808 0 0 0 140288 343 421 0 3 0 97 0
0 1 0 1570468 40108 175808 0 0 0 118784 293 366 0 4 0 96 0
次に、direct オプションを付けずに実行します。
# dd if=/dev/zero of=test bs=1M count=512
すると、iotop では dd プロセスとともに[flush-253:0]というプロセスが IO を使用していることが分かります。
Total DISK READ: 0.00 B/s | Total DISK WRITE: 136.81 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
1009 be/4 root 0.00 B/s 51.47 K/s 0.00 % 96.35 % [flush-253:0]
2071 be/4 root 0.00 B/s 81.78 M/s 0.00 % 93.61 % dd if=/dev/zero of=test bs=1M count=512
1 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % init
vmstat を見ると、再びページキャッシュが確保されて 500MB ほど増加し、procs の b は 2 プロセスとなりました。
procs -----------memory---------- ---swap-- -----io0---- -system--- -----cpu------
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 1571468 40160 175808 0 0 0 0 26 25 0 0 100 0 0
0 2 0 1167220 40160 566272 0 0 0 120836 409 107 0 28 61 11 0
0 2 0 1053264 40160 676856 0 0 0 114688 440 253 0 12 0 88 0
0 2 0 1030448 40160 700088 0 0 0 133120 373 13150 0 5 0 95 0
つまり、direct の場合にはプロセスと同期して HDD への書き込みがおこなわれ、通常の場合にはページキャッシュを経由して非同期に HDD への書き込みがおこなわれることが分かります。
なお、読み込みの頻度が少なく容量の大きなファイルを書き込むときには、ページキャッシュを利用しないほうが速くなることもあります。例えば、VM のイメージやバックアップデータなど、書き込んだまま使用をしばらくしないようなデータの場合が該当します。
また、メモリを大量に搭載したサーバにおいては、メモリがプロセスなどによって利用されない限り、多くのメモリがページキャッシュに割り当てられるため、一見書き込みが高速なように見えても flush中に IO がボトルネックになり、いわゆる「息継ぎ」が発生することもあります。
その為、用途によって遅延書き込みの使用可否、direct オプションの使用などを検討すればよいでしょう。
スワップアウトさせてみる
ここまで、キャッシュの挙動と IO の関係を見てきました。
ここで、実メモリ以上にメモリ空間を確保してメモリ不足の状況を起こし、スワップとキャッシュがどのような挙動をするか見てみます。 実メモリを確保するために、dd でメモリを 2G バイト指定してコピーを実行します。
# dd if=/dev/zero of=/dev/null bs=2G count=1
0+1 records in
0+1 records out
2147479552 bytes (2.1 GB) copied, 8.88384 s, 242 MB/s
すると、みるみるうちにバッファキャッシュとページキャッシュの使用量が減っていき、スワップの使用量が増えてきました。
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 1259544 440 530032 0 0 0 0 25 26 0 0 100 0 0
1 0 1020 66640 120 374540 0 1020 0 1020 484 48 0 46 54 0 0
0 2 50104 53072 108 20052 0 49048 0 49048 491 193 0 41 0 59 0
1 1 112012 53100 116 20056 0 61916 0 61928 211 184 0 4 0 96 0
0 2 168016 53764 116 19000 0 56004 0 56008 220 95 1 5 0 94 0
1 2 214216 53064 116 17872 32 46204 32 46204 223 80 0 6 0 94 0
0 3 265212 52972 116 17868 96 51004 96 51004 217 81 0 5 0 95 0
0 2 327312 53004 116 17860 96 62112 96 62112 262 126 0 7 0 93 0
0 2 374232 53876 116 16624 0 46932 0 46932 225 78 0 5 0 95 0
0 2 433160 54556 116 16584 0 58948 0 58948 214 75 0 6 0 94 0
0 0 75092 1746180 116 16608 476 168 476 168 217 115 0 7 43 50 0
dd の終了とともに空きメモリは増え、スワップは減りましたが、キャッシュは減ったままになっています。
この状態で先ほど書き込んだデータをもう一度読み込んでみます。
# dd if=test of=/dev/null bs=1M count=512
512+0 records in
512+0 records out
536870912 bytes (537 MB) copied, 0.735082 s, 730 MB/s
すると、先ほどは 0.10 秒で完了し 5.2 GB /s だったものが、0.74 秒で 730MB/s と大幅に遅くなって
います。 また、ディスクの読み込みが発生し、IO ウェイトも若干ですが発生しています。
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 79428 1484132 104 281820 32 0 261880 0 2431 4102 0 35 62 3 0
0 0 79428 1236760 104 529968 0 0 248116 0 2303 3830 0 35 63 2 0
このように、メモリが極端に足りない状況になるとページキャッシュにまわされるメモリ空間が無く
なり、IO に負担がかかることが分かります。
ちなみに、一度読み込みを行うと、再びページキャッシュが増えていることが分かります。
試しに先ほどと同じように dd コマンドを実行してみましょう。
# dd if=test of=/dev/null bs=1M count=512
512+0 records in
512+0 records out
536870912 bytes (537 MB) copied, 0.123787 s, 4.3 GB/s
最初にディスク書き込みを行った直後と同じように、一瞬で読み込みが完了し、ページキャッシュが
有効に利用されたことが分かります。
ファイルを削除したときの挙動を確認する
ページキャッシュはファイルに対するキャッシュであるという解説をしましたが、該当のファイルが無くなった場合はどうなるのでしょうか?
先ほど作成したファイルを削除してみます。
# rm -f test
すると、test というファイルのために確保されていたページキャッシュは解放されました。
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 79004 1756304 1152 8736 0 0 0 0 19 19 0 0 100 0 0
バッファキャッシュの挙動を確認する
なお、ここまではページキャッシュについてみてきましたが、ブロックデバイスへアクセスした際のバッファキャッシュの動きについても見てみましょう。
次のように、ブロックデバイスから 512MB の読み込みを試します。
# dd if=/dev/vda of=/dev/null bs=1M count=512
512+0 records in
512+0 records out
536870912 bytes (537 MB) copied, 1.19414 s, 450 MB/s
1.19 秒で処理が完了しました。 vmstat の状態を見ると、ディスク読み込みが発生し IO ウェイトが若干高まるとともに、バッファキャッシュの使用量が 500MB 強ほど増加したことが分かります。
procs -----------memory----------- ---swap-- -----io---- --system-- -----cpu------
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 1255484 2492 534276 0 0 0 0 30 36 0 0 100 0 0
2 0 0 1088200 164236 534260 0 0 161740 0 2593 3053 0 29 66 5 0
0 0 0 715712 526908 534276 0 0 362676 0 6927 8131 0 80 16 4 0
この状態で、再度読み込みを試します。
# dd if=/dev/vda of=/dev/null bs=1M count=512
512+0 records in
512+0 records out
536870912 bytes (537 MB) copied, 0.109901 s, 4.9 GB/s
次は 0.11 秒と劇的に高速化されました。
まとめ
- ファイルを書き込むとページキャッシュが確保される
- ファイルを読み込むとページキャッシュが確保される
- HDD へのアクセス中(IO 中)は、procs の b と、cpu の wa が上昇する
- HDD への書き込み中は io の bo が、読み込み中は io の bi が上昇する
- ページキャッシュやバッファキャッシュから読み込むときは io の bi は増えず、大変高速である
- メモリが足りなくなるとスワップアウトされ、パフォーマンスが低下する
- メモリが足りなくなるとページキャッシュやバッファキャッシュは解放される
なお、一般的に書き込みより読み込みのほう負荷が低いと思われがちですが、上記のとおり書き込みは遅延対応が行えることから HDD 動作を必ずしも待たなくて良いのに対し、読み込みはキャッシュに存在しなければ HDD へのアクセスが発生するため、頻繁にアクセスされるファイルの容量分だけメモリが搭載されていれば、ディスク IO が大きく改善します。
ですので、スワップが発生しないように維持することはもちろんのこと、空きメモリと IO の発生状況(bi の項目)をチェックし、性能を維持することが重要となります。