Linux メモリオーバーコミットを完全制御:本番環境のデータベース・AIワークロード向け vm.overcommit_memory 設定ガイド

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

背景と理由 — 深夜2時の緊急コール

深夜2時14分。PostgreSQLサーバーがOOMエラーでクラッシュした。Slackのアラートが鳴り止まない。サーバーには16GBのRAMがあるのに、なぜかカーネルが4GBしか使っていないプロセスを強制終了した。この経験があるなら、原因はもうわかるはずだ——メモリオーバーコミットの誤設定だ。

Linuxはプロセスがメモリを要求した瞬間に物理RAMを割り当てるわけではない。アプリケーションがmalloc()を呼び出してポインタを受け取っても、カーネルはまだ実際のメモリを確保していない——約束をしただけだ。これがメモリオーバーコミットだ。カーネルはまだ物理的に存在しないかもしれないメモリを提供すると約束し、すべてのプロセスが予約したメモリをすべて使うわけではないと見込んでいる。

このギャンブルはデスクトップでは問題ない。しかし、PostgreSQL、Redis、PyTorchの推論サービスが動く本番サーバーでは、あっという間に夜が台無しになる。

これを制御するカーネル設定がvm.overcommit_memoryで、3つのモードがある:

  • モード0(デフォルト):ヒューリスティックオーバーコミット。カーネルが経験則で判断し、ほとんどのオーバーコミットは許可するが明らかに無理な要求は拒否する。問題は「明らかに無理」の基準がワークロードによって大きく異なる点だ。
  • モード1:常にオーバーコミット。利用可能なメモリに関係なく、すべてのアロケーション要求に対してカーネルがYESと答える。高速で楽観的だが、本番データベースには危険だ。
  • モード2:厳格なオーバーコミット。カーネルがハードなコミット上限を計算し、それを超える要求を拒否する。より予測可能で保守的。ステートフルなサービスに適した選択だ。

補助設定のvm.overcommit_ratio(デフォルト:50)は、モード2がどれだけ積極的にコミットを許可するかを制御する:

CommitLimit = (TotalRAM × overcommit_ratio / 100) + SwapTotal

スワップなしの16GBマシンでモード2・比率50の場合、コミット可能なメモリの合計はわずか8GBだ。12GBを要求するデータベースプロセスは最初から拒否される——深夜3時にトランザクション中にサイレントOOMキルされるよりはるかにましだ。

何かを変更する前にベースラインを確認する

10台以上のLinux VPSを3年間管理してきて、1つの習慣が身についた。何かを変更する前にベースラインを理解すること。このステップを省略すると、ルーティンの設定変更がインシデントに発展する。以下のコマンドは1分もかからず、システムの現状を正確に把握できる:

# 現在のオーバーコミットモードを確認(0、1、または2)
cat /proc/sys/vm/overcommit_memory

# オーバーコミット比率を確認(モード2でのみ関係する)
cat /proc/sys/vm/overcommit_ratio

# コミット上限と実際のコミット済みメモリを確認
grep -E 'CommitLimit|Committed_AS' /proc/meminfo

CommitLimitCommitted_ASの差が最も重要な数値だ。Committed_ASCommitLimitに近づいているか超えている場合、free -hが4GBの「空き」を表示していても、すでにOOMのリスクがある。

OOMキラーがサイレントで動いていないかも確認しよう:

# カーネルリングバッファの最近のOOMイベント
dmesg | grep -i "oom\|killed process" | tail -20

# journalctl経由の永続的なOOMログ
journalctl -k | grep -i "oom\|killed" | tail -20

知らなかったエントリがあれば、OOMキラーがバックグラウンドで動いていた証拠だ。さらに負荷をかける前にベースラインを修正しよう。

設定 — ワークロードタイプに合わせたチューニング

データベースサーバー(PostgreSQL、MySQL、MariaDB)

データベースは積極的なオーバーコミットの最悪の被害者だ。PostgreSQLのshared_buffersとMySQLのinnodb_buffer_pool_sizeは、単に投機的に予約するだけでなく実際に使用する大きなメモリ領域を確保する。モード0のヒューリスティックではこれらを確実に保護できない。

データベースサーバーには、慎重に計算された比率でモード2が必要だ:

# 例:16GB RAM、4GBスワップ、最大14GBのコミットを許可したい場合
# CommitLimit = (16 × ratio/100) + 4
# 比率を解く:(14 - 4) / 16 × 100 = 62.5 → 安全マージンとして70を使用

# 即時適用(実行時のみ、再起動後は無効)
sysctl -w vm.overcommit_memory=2
sysctl -w vm.overcommit_ratio=70

# CommitLimitが正しく再計算されたか確認
grep CommitLimit /proc/meminfo

再起動後も設定を維持するには、専用のsysctlドロップインファイルに書き込む——可能な限り/etc/sysctl.confを直接編集しないこと:

# 専用の設定ファイルを作成
cat > /etc/sysctl.d/99-memory-overcommit.conf << 'EOF'
vm.overcommit_memory = 2
vm.overcommit_ratio = 70
EOF

# 再起動せずに適用
sysctl --system

# 確認
sysctl vm.overcommit_memory vm.overcommit_ratio

モード2を適用したら、データベースサービスを再起動しよう。shared_buffersinnodb_buffer_pool_sizeが新しいCommitLimitを超えるように設定されている場合、サービスは起動に失敗する——これが望ましい動作だ。起動時の失敗は回復可能だ。クエリの途中でOOMキルされることは回復できない。

AIと機械学習のワークロード

AI推論と学習ジョブは異なる動作をする。PyTorchとTensorFlowは投機的に、バースト的に、そしてモード2のコミット計算を混乱させるような方法でメモリを確保することが多い。MLワークロードに厳格なオーバーコミットを強制すると、計算途中で正当なアロケーション失敗が頻発する。

MLワークロードには全く異なるアプローチが必要だ。モード1とcgroupレベルのメモリ制限を組み合わせる方が、カーネルレベルのオーバーコミット制限よりも効果的だ:

# カーネルレベルではMLフレームワークが自由にアロケーションできるようにする
sysctl -w vm.overcommit_memory=1

# 代わりにsystemdリソース制御で特定のサービスを制限する
systemctl set-property ai-inference.service MemoryMax=12G MemoryHigh=10G
systemctl daemon-reload
systemctl restart ai-inference.service

AIワークロードをDockerで実行している場合は、コンテナレベルでハードなメモリ制限を設定しよう

# コンテナのメモリとスワップのハード制限
docker run \
  --memory="10g" \
  --memory-swap="10g" \
  --name inference-server \
  my-model-image python serve.py

docker-compose.ymlの場合:

services:
  inference:
    image: my-model-image
    deploy:
      resources:
        limits:
          memory: 10g

これにより、AIフレームワークが必要なオーバーコミットの柔軟性を確保しつつ、OSが強制する上限を維持できる。OOMキラーはホスト上の他のものに手を付ける前にコンテナをターゲットにする。

検証と監視

sysctlの変更が意図通りに機能したと思い込んではいけない。すぐに検証し、実際の負荷下で監視する。この習慣は、私がこれまでに行ったどの設定変更よりも多くの本番インシデントを防いできた。

設定変更後すぐに実行できる簡単なヘルスチェックスクリプトから始めよう:

#!/bin/bash
echo "=== メモリオーバーコミット状態 ==="
echo "モード   : $(cat /proc/sys/vm/overcommit_memory)"
echo "比率     : $(cat /proc/sys/vm/overcommit_ratio)%"
echo ""
grep -E 'MemTotal|MemAvailable|SwapTotal|CommitLimit|Committed_AS' /proc/meminfo
echo ""

LIMIT=$(grep CommitLimit /proc/meminfo | awk '{print $2}')
COMMITTED=$(grep Committed_AS /proc/meminfo | awk '{print $2}')
PCT=$((COMMITTED * 100 / LIMIT))
echo "コミット使用率: ${PCT}% (上限比)"

if [ "$PCT" -gt 85 ]; then
  echo "警告: メモリコミットが危険なレベルです — OOMリスク高"
fi

本番環境では、node_exporter(Prometheus)がすでにこれらの値をnode_memory_CommitLimit_bytesnode_memory_Committed_AS_bytesとしてエクスポートしている。OOMキラーより先に気づけるよう、アラートルールを追加しよう:

# prometheus/rules/memory.yml
groups:
  - name: memory_overcommit
    rules:
      - alert: MemoryCommitHigh
        expr: node_memory_Committed_AS_bytes / node_memory_CommitLimit_bytes > 0.80
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.instance }} のメモリコミットが高い"
          description: "コミット済みメモリが上限の {{ $value | humanizePercentage }} です"

負荷テスト中は、これら2つの値を並行して監視しよう——freetop単体より明確な状況が把握できる:

# ターミナル1 — OOMキルイベントを監視
watch -n 2 'dmesg | grep -c "Out of memory"'

# ターミナル2 — コミット比率をリアルタイムで監視
watch -n 2 'grep -E "CommitLimit|Committed_AS" /proc/meminfo'

実際のワークロードを実行しよう——データベースリストア、モデル推論バッチ、トラフィックスパイクの再現など。Committed_ASCommitLimitに向かって急上昇し、その直前で横ばいになれば、比率は適切に調整されている。ピーク負荷時に上限の50%を超えない場合は、比率を引き締めてOOMのリスクをさらに減らそう。

最後に:vm.overcommit_memoryvm.overcommit_ratioをサーバープロビジョニングのチェックリストに追加しよう。新しいインスタンスはすべて、アプリケーションをデプロイする前にベースラインの/etc/sysctl.d/99-memory-overcommit.confを書き込む。この記事の冒頭で紹介した深夜2時のインシデントは、そのステップを省略したばかりのサーバーで起きた。二度と繰り返さない。

Share: