サーバーが遅い理由 #2 メモリを増やしても速くならないとき — ページキャッシュ・スワップ・ワーキングセット

読了 6分

遅くなったサーバーに対して最初に出てくる対処が「メモリを増やそう」です。ところが 32GB を 64GB にしたのに応答時間がそのまま、というケースは珍しくありません。お金は出ていったのに、なぜ速くならなかったのでしょうか? 1 編 が CPU 使用率の錯覚を扱ったとすれば、今回はメモリ容量の錯覚です。

結論から言えば、メモリ増設はメモリがボトルネックのときだけ効き、メモリがボトルネックである状況は思ったより狭いのです。増設の前にその狭い条件に該当するかを判定する方法をつかむことが、この記事の目標です。

余ったメモリは遊んでいません — ページキャッシュ #

まず Linux のメモリの計上方法から押さえる必要があります。free -h を見ます。

free -h
$ free -h
               total        used        free      shared  buff/cache   available
Mem:            31Gi        12Gi       1.2Gi       0.5Gi        18Gi        18Gi
Swap:          8.0Gi       2.1Gi       5.9Gi

free が 1.2GB しかなく不足に見えますが、buff/cache 18GB の大部分はページキャッシュです。カーネルが一度読んだファイルをメモリに載せておき、次の読み取りをディスクなしで処理する領域であり、プロセスがメモリを要求すればすぐに回収して渡す領域でもあります。だから実際の余裕は available(18GB)で読みます。この計上方法は ハードウェア中級 #3 で詳しく扱いました。

この構造が増設効果の最初の分かれ道です。メモリを増やすとページキャッシュが大きくなり、ディスク読み取りがキャッシュヒットに変わって速くなります。裏返せば、頻繁に読むデータがすでにキャッシュに収まっているなら、キャッシュをさらに大きくしてもヒット率は上がりません。 ヒット率 98% のシステムでメモリを倍にしても、得られるのは残り 2% の一部だけです。

ワーキングセット — 増設が効く唯一の条件 #

判定基準になる概念がワーキングセット(working set)です。システムが実際に繰り返し触るデータの大きさを指します。全データが 500GB でも、毎日触る部分が 10GB ならワーキングセットは 10GB に近い値です。

  • ワーキングセット > メモリ: キャッシュから追い出されたデータをディスクから読み直す作業が繰り返されます。増設が劇的に効く唯一の区間です。
  • ワーキングセット < メモリ: すでにキャッシュがワーキングセットを収めています。増設しても体感は変わりません。

ワーキングセットがメモリを超えているかは直接測れます。信号は継続的なディスクの読み取りです。書き込みはいずれディスクに書き込まれるものですが、定常状態の読み取りはキャッシュが吸収するはずだからです。vmstatbi(ブロック読み取り)が負荷の間ずっと高いままだったり、iostat -x で読み取り IOPS が絶えず出ているなら、キャッシュがワーキングセットを収めきれていないという意味です。逆にディスク読み取りがほとんどないシステムなら、遅さの原因はメモリではありません。1 編の診断手順に戻って別のリソースを見るべきです。

スワップ — あることより動きが重要です #

free でスワップ使用量 2.1GB を見て「スワップを使っているからメモリ不足」と判定しがちですが、これも錯覚です。カーネルは暇なとき、長く使われていないページをあらかじめスワップアウトしておくことがあります。問題はスワップにあることではなく、スワップを出入りしていることです。

vmstat 1 — si/so カラム
$ vmstat 1
procs -----------memory---------- ---swap-- -----io----
 r  b   swpd   free   buff  cache   si   so    bi    bo
 2  1 2201M  1.1G   218M   17.9G  840  1120  2400  3100

si(swap in)と so(swap out)が負荷中に継続して 0 でないなら、アクティブなページがスワップとメモリを行き来するスラッシングです。このときはメモリ増設が正しい対処です。逆に swpd が大きくても si/so が 0 に張り付いているなら、古いページが眠っているだけで、増設しても何も変わりません。

vm.swappiness を 0 まで下げてスワップを封じるチューニングが流行っていますが、順序が重要です。スラッシングの原因がワーキングセット超過なら、swappiness を下げても回収の圧力がページキャッシュ側へ移るだけで、悪くすると OOM キラーを呼び込みます。スワップ設定はワーキングセット判定が終わったあとの微調整です。

増設したのに変わらない、よくある理由二つ #

ワーキングセット判定とは別に、増設分がそもそも届かないケースが二つあります。

コンテナのメモリ上限 — ホストのメモリを増やしても、コンテナに memory limit がかかっていればコンテナ内プロセスの上限はそのままです。コンテナ内の free はホストの値を見せるので錯覚しやすい点に注意してください。cgroup 基準の実際の上限と使用量は、コンテナランタイム側(docker stats、Kubernetes なら kubectl top pod)や cgroup のファイルから読む必要があります。この区別は ハードウェア中級 #3 で扱いました。

アプリケーションが自分で決めた上限 — JVM の -Xmx、データベースのバッファプールサイズ(shared_buffersinnodb_buffer_pool_size)、言語ランタイムのヒープ設定が代表的です。これらの値が固定されていると、サーバーにメモリをいくら足してもプロセスは古い上限のまま動きます。増設後はこれらの設定を新しい容量に合わせて一緒に引き上げてこそ、増設分が実際に使われます。

メモリは余っているのに OOM が起きる場合 #

増設の議論によく混ざる別個の症状も一つ整理しておきます。available が十分なのに OOM キラーがプロセスを殺したなら、多くはシステム全体ではなく cgroup(コンテナ)の上限超過です。カーネルログ(dmesg)の OOM メッセージに記録された cgroup パスを確認すれば、システム OOM かコンテナ OOM かはすぐ区別できます。コンテナ OOM なら、対処はホストの増設ではなく該当コンテナの limit 調整です。

診断手順のまとめ #

症状が「メモリを増やしたのに(増やそうとしているのに)効果がない(なさそう)」のとき、手順は次のとおりです。

  1. free -havailable — まず余裕を正確に読みます。free ではなく available です。
  2. vmstatsi/so — 負荷中のスワップの出入りが続くならワーキングセット超過です。増設が効く区間です。
  3. vmstatbi / iostat の読み取り IOPS — 負荷の間ずっとディスク読み取りが続くなら、キャッシュがワーキングセットを収めきれていないという意味です。やはり増設候補です。
  4. どちらでもなければメモリは無実 — 1 編の診断手順に戻り、CPU の待ち、ストレージ、ロックを見ます。
  5. 増設済みなら伝達経路を確認 — コンテナの limit とアプリケーションのヒープ・バッファプール設定を新しい容量に合わせて上げたかを確認します。

まとめ #

  • 余ったメモリはページキャッシュとしてすでに働いています。余裕は free ではなく available で読みます。
  • 増設が効く条件は事実上一つ、ワーキングセットがメモリより大きいときです。信号は継続的なスワップの出入り(si/so)と絶え間ないディスク読み取りです。
  • スワップは使用量ではなく動きで判定します。swappiness の調整はワーキングセット判定のあとの微調整です。
  • コンテナの limit や JVM ヒープ、DB バッファプールのように上限が別に決まっている場所へは、増設分は自動的に届きません。
  • メモリが余っているのに起きる OOM は多くがコンテナの上限超過です。dmesg の cgroup パスで区別します。

次回はストレージです。SSD を使っているのに遅い理由を、書き込み増幅、fsync、キュー深度で追跡します。

X