いまさら聞けないLinuxとメモリの基礎＆vmstatの詳しい使い方

さくらインターネット Advent Calendar最終日は、硬派にLinuxのメモリに関する基礎知識についてみてみたいと思います。
最近はサーバーを意識せずプログラミングできるようになり、メモリの空き容量について意識することも少なくなりましたが、いざ低レイヤーに触れなければいけないシチュエーションになった際に、OSを目の前に呆然とする人が多いようです。

基本的にLinux のパフォーマンスについて、メモリをたくさんつめばいいとか、スワップさせないほうが良い とか、このあたりは良く知られたことだと思います。 ただ、なんとなく ps コマンドや free コマンド などの結果を見るだけでなく、もう少しメモリのことについて掘り下げてみてみたいと思います。

# メモリとキャッシュ

Linux におけるメモリの状態を大きく分けると「使用中のメモリ」「キャッシュ」「空きメモリ」「スワップ」の 4 つに分けられます。
そして、スワップについては HDD に存在し、それ以外は実メモリに存在し、HDD は実メモリより低速です。

![図1.png](https://qiita-image-store.s3.amazonaws.com/0/103214/dd5ab7ad-31e9-cee0-9588-0d59f11d7aa9.png)

使用中のメモリとはカーネルやアプリケーションなどのプロセスによって使用されているメモリの事です。キャッシュとは 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 の項目)をチェックし、性能を維持することが重要となります。