WSL2、メモリ食い尽くされて固まるのもう勘弁なんで先回りで縛った話

WSL2、メモリ食い尽くされて固まるのもう勘弁なんで先回りで縛った話

WSL2でビルドとか複数の開発ツール同時に動かしてると、Linux側のプロセスが思ってた以上にメモリ抱え込んで、仮想マシンごと反応しなくなること、あるんだよな。swap足すとか、メモリ足りなくなったらプロセス落とすツール入れるとか、そういう手はもちろん効くんだけど、設定間違えると「守ってるつもりで全然守れてない」状態になる。これが厄介。

今回、earlyoomの判定条件をちゃんと見直して、プロセスごとの上限は先にcgroupで決めておく、って方向で整理した。

earlyoomのAND条件、ここ罠

earlyoomってのは、カーネルのOOM killerが動く前に、メモリ食ってるプロセスを先に落としてくれる仕組み。ただし設定次第では、空きメモリと空きswapの**両方**が閾値を下回ったときしか動かない。AND条件ってやつだ。

これ、swapを大きく増やすと逆に危ない。RAMがほぼ残ってなくても、swapに余裕がある限りearlyoomは終了処理を始めない。だから、プロセスがメモリ使い切るまで保護が遅れて、WSL2のVM自体が固まりかねない。swapは退避先を増やすだけで、プロセスの使用量に上限を付ける機能じゃないんだよな。

もう一個の落とし穴が、閾値オプションを片方だけ指定したときの既定値。SIGTERM用とSIGKILL用の閾値を別々に設定できるツールだと、片方省略した瞬間に意図しない値が勝手に補われることがある。ネットに転がってる設定例をそのまま貼るんじゃなくて、使ってるバージョンのマニュアルで、メモリとswapそれぞれの判定条件、あとシグナルの閾値、ちゃんと確認してくれ。

後から殺すより、先に上限置いとけ

earlyoomは「不足が起きたあとに誰かを終了させる」仕組みでしかない。一方cgroupのメモリ制限は「このグループはここまでしか確保できない」って境界を、プロセス実行する前に作れる。暴走する処理があっても、影響を一つのグループの中に閉じ込められるから、VM全体の生存性はこっちのほうが守りやすい。

systemdとcgroup v2が使える環境なら、ユーザー権限のscopeとして起動できる。

systemd-run --user --scope \
  -p MemoryMax=6G \
  -p MemorySwapMax=2G \
  --collect -- your-command --arg

MemoryMaxがグループのメモリ上限、MemorySwapMaxがswapに退避できる量の上限。値は処理の性質とWSL2全体の割り当てから決めればいい。上限に達したら対象scope内のプロセスが落ちることはあるけど、無制限に膨らんだプロセスがVM全体を巻き込む事態は避けやすくなる。

あと、systemd-run --userが使えるかどうかはコマンドの存在だけじゃ判断できないので注意な。ユーザーセッションのsystemd、D-Bus、cgroup v2が実際に利用可能か確認して、失敗したときは安全な素通しにするか、明示的にエラー出すか、運用に合ったフォールバックをちゃんと用意しておくこと。

クルー基盤で使ったらこうなった

うちみたいに複数のAIエージェントを同時に走らせる基盤だと、各CLIを個別のscopeでラップするやり方が使える。起動、再起動、モデル切り替えとか経路が複数あるとき、経路ごとに制限をバラバラに実装するとどこか抜けるので、共通のコマンド生成関数に一本化するのがポイントだ。

共通関数には、こういう役割を持たせる。

1. 設定からメモリ上限とswap上限を読み込む。 2. systemd-runとユーザーセッションが使えるか確認する。 3. 使えるなら、同じ制限を付けたscope起動コマンドを返す。 4. 使えない場合の挙動は、無制限実行に逃げずに設定で選べるようにする。

これなら起動経路が増えても、制限の付け忘れが減る。実際の導入では、低い上限でテスト用コマンドを実行して、対象プロセスだけが落ちること、ホスト側のメモリ状況が異常に悪化しないこと、ログに原因が残ること、この3つを順に確認した。地味だけどここ結構でかい。

対策の優先順位、こう考えてる

メモリ枯渇対策は、この順で考えると整理しやすい。

  • 第一に、重い処理をcgroupのMemoryMaxで局所化する。
  • 第二に、earlyoomのメモリ・swap判定とシグナル閾値を意図どおりに調整する。
  • 第三に、必要性を測ったうえでWSL2のメモリ割り当てやswapを見直す。

メモリ増やすだけだと、異常な処理が使える範囲まで一緒に広がる。上限、観測、終了条件、この3つを組み合わせて、失敗しても影響範囲が広がらない構成にするのが大事なんだよな。

導入前チェックリスト

  • cgroup v2とユーザーsystemdが有効か。
  • MemoryMaxMemorySwapMaxの単位・既定値を確認したか。
  • earlyoomがメモリとswapのどの条件で動くか確認したか。
  • すべての起動経路が同じ制限を通っているか。
  • 上限到達時に、対象処理の終了とログ記録を確認したか。
  • 制限機構自体が使えないときのフォールバックを決めたか。

earlyoomは最後の安全網としては役に立つ。でもシステム全体を守る主役にするなら、まずプロセス単位で使えるメモリを制限するほうが、挙動を予測しやすい。WSL2で重い処理を並列化するほど、「足りなくなったら殺す」だけじゃなく「最初から使い過ぎられない」境界を作っておくことが重要だ。