自宅のミニPCサーバーで10本以上のサービスを動かしていたら、最近なんだか全体的に 動作が重い。 「気のせいかな」と流していたのですが、実測してみたら明確にメモリがカツカツでした。 この記事は、その時の調査と対応の記録です。誰得でもないですが、 「swapが満杯」という表示に騙されかけた話として読んでください。
症状: なんとなく全部が遅い
分かりやすい障害ではなく、「なんとなくページの反応が遅い」「たまにタイムアウトする」という曖昧な劣化でした。こういう時は決め打ちせず、まず数字を取ります。freeでメモリ、uptimeでロードアベレージ、ps aux --sort=-rssでメモリ食いの上位を確認 — ここで出た数字がこちら(抜粋):
- メモリ24GBのうち使用13GB、空きは約370MB
- swapが2.7GBで「満杯」(残り196KB)← これが引っかかった
- ロードアベレージ(15分平均)が4コアに対して9.0 ← 2倍以上の過負荷
「swap満杯」の正体はディスクではなかった
swapが満杯 — 普通なら「ディスクスワップが埋まった」と解釈して、スワップファイルを 増やしたくなります。私も最初そう判断してswappinessを下げる設定を入れたのですが、 ここで一旦立ち止まって/proc/swapsを確認して、ハッとしました。
その「swap」は /dev/zram0 — zram、つまり圧縮RAMだったのです。このサーバーではディスクではなく、メモリ上に圧縮スワップ(zram)を置く設定にしていました。 zramは普通のRAMより遅いものの、ディスクスワップよりははるかに速いという優れもの。 つまり「2.7GBが満杯」というのは「メモリの中の圧縮領域まで使い切っている」という意味で、ディスクスワップが埋まっていたわけではありませんでした。
しかも厄介なことに、この構成ではswappinessを高め(100)に設定するのが意図的な正解です。 圧縮スワップを積極的に使うほどメモリが節約できるためで、私は最初それを知らずに 「100は異常値だ」と下げてしまい、あとで慌てて戻しました。swap関連のチューニングは、必ず「実際のデバイス構成」を確認してから — これがこの一件で一番の教訓です。
犯人探し: 誰がメモリを食っているか
上位プロセスを並べると、犯人ははっきりしていました。 1つのWebアプリが単体で2.2GBを常駐させていたのです。 そのアプリは「リクエストされたデータをメモリにキャッシュする」設計で、 それはそれで正しい挙動。つまりリークではなく、仕様としてメモリを育てるタイプでした。他にも翻訳モデルの常駐プロセス(約1GB)や、ファーミング系の常駐アプリ(約1.3GB)など、 「常駐するのが仕事」のプロセスが積み重なって、24GBを使い切っていたわけです。
やった対策(上から順に安全な順)
- オーバーフロー用のディスクスワップを追加: zramが満杯になったときの 受け皿として4GBのスワップファイルを追加。これは「緊急時の余白」を作るだけで、 普段は使われません
- メモリを育てるタイプのアプリを再起動: 2.2GBまで育ったアプリを 深夜(低トラフィック時)に再起動。キャッシュが温まるまでの数分はCPUを使いますが、 解放量が大きく、空きメモリが370MB→2.6GBに改善しました
- 「育てるタイプ」は定期的にリセットする運用へ: リークではないので、閾値を超えたら自動で夜間に再起動、という見張りを入れる方向に
やってはいけない対策も書いておきます。「とりあえずswappinessを下げる」は zram構成では逆効果になり得ますし、「キャッシュをdrop_cachesで捨てる」のも 一時しのぎ。まず「どのプロセスが、仕様として、どれだけ育つのか」を把握するのが先です。
まとめ
小さなサーバーで多くのサービスを動かすなら、メモリは「犯罪現場」のように扱うべきです。 数字を取り、デバイスの正体(zramかディスクか)を確認し、犯人を特定し、 「戻せる・効く」順に対策を打つ。ロードアベレージが高くてもCPUが暇なら、 それはメモリかI/Oを待っているサイン — この一連の流れは、この先も必ず役に立ちます。