深夜2時の障害:単一プロセスがサーバーを「食い尽くした」夜
深夜2時、Zabbixのアラートが一斉に鳴り響きました。CentOS Stream 9(4 vCPU、16GB RAM)で稼働するWebサーバー群が完全に無応答となり、SSHもconnection timeoutが連続。ロードアベレージは48に達していました。
クラウドベンダーのレスキューコンソールを使い、苦労の末にようやくtopコマンドを実行できました。判明した原因は、キュー処理を行っていたPythonワーカーのメモリリークです。14GBものメモリを食い尽くした結果、LinuxのOOM Killerが発動し、あろうことかMariaDBとNginxを巻き添えにして強制終了させてしまっていたのです。
プロセスにリソースを無制限に使わせるデフォルト設定の運用には、極めて大きなリスクが伴います。CentOS Stream 9以降、Red Hatは完全にcgroups v2へと移行しました。これをsystemd slicesと組み合わせることで、アプリケーショングループごとにリソースを効率的かつ確実に分割・隔離できるようになります。
Linuxにおける3つのリソース制御手法
システム管理者が検討する主な手法には、以下の3つがあります。
- 手法1:cgroupfsによる手動操作(
/sys/fs/cgroup/配下にディレクトリを作成し、手作業でPIDを割り当てる)。 - 手法2:
ulimitまたはlimits.confの設定(ユーザーセッション単位での制限)。 - 手法3:cgroups v2とsystemd slicesの組み合わせ(サービスUnit単位で直接制御)。
各ソリューションのメリット・デメリット比較
1. cgroupfsによる手動操作
- メリット: initシステムに依存せず独立して動作する。
- デメリット: 運用保守が非常に煩雑。プロセスが子プロセスをforkした場合やクラッシュして再起動した際にPIDが変わり、割り当てた設定が失われる。
2. ulimit(/etc/security/limits.conf)の利用
- メリット: ターミナルから操作する対話型ユーザーアカウントの制限には手軽。
- デメリット: systemdが起動するデーモンプロセスには効果がない。さらに、
ulimit -vなどは柔軟なメモリ調整を行うのではなく、アプリケーションを即座にクラッシュさせてしまう。
3. cgroups v2とsystemd Unit & Slicesの組み合わせ
- メリット: CPU、メモリ、I/Oに対して単一の統合階層構造(single unified hierarchy)を使用。プロセスが何度再起動しても確実にグループ内に保持される。複数のサービスをまとめて共通の上限枠に収めることも容易。
- デメリット: 設定ディレクティブの構文が一新されている(例:cgroups v1の
MemoryLimitからMemoryMaxへ変更など)。
なぜCentOS Stream 9ではsystemd slices + cgroups v2が最適なのか?
CentOS Stream 9はLinuxカーネル5.14+およびsystemd v250+を採用しており、デフォルトでcgroups v2が有効化されています。旧来の手法を無理に適用しようとすると、設定が断片化して管理が破綻してしまいます。
systemd slicesを使用すれば、サーバーのリソースを明確な「スライス(区画)」に分割できます。例えば、すべてのワーカーやCronジョブをbackground.sliceに集約して最大20%のCPUと2GBのRAMに制限し、残りの80%のCPUと14GBのRAMをデータベースやWebサーバーが稼働するsystem.sliceのために確実に確保しておくことが可能です。
実践的な設定手順
ステップ1:cgroups v2の稼働状態を確認する
まず、システム上でcgroups v2が有効になっていることを確認します。
# cgroupファイルシステムを確認
stat -fc %T /sys/fs/cgroup/
# 正常な出力: cgroup2fs
# 有効化されているコントローラー一覧を確認
cat /sys/fs/cgroup/cgroup.controllers
# 想定される出力: cpuset cpu io memory pids
ステップ2:バックグラウンド処理用のSliceを作成して隔離する
バックグラウンドタスクを管理するためのbackground-worker.sliceファイルを作成します。
sudo nano /etc/systemd/system/background-worker.slice
リソース制限の閾値を定義します。
[Unit]
Description=バックグラウンドワーカー用リソース制限Slice
Before=slices.target
[Slice]
# CPU使用率を1コアの50%に制限
CPUQuota=50%
# メモリのソフトリミット: 1GBを超えるとカーネルが積極的に回収/ページアウトを実行
MemoryHigh=1G
# メモリのハードリミット: 1.5GBに達するとcgroup OOM KillerがSlice内のプロセスを強制終了
MemoryMax=1.5G
# フォーク爆弾(fork bomb)を防ぐためのプロセス数制限
TasksMax=100
ステップ3:ServiceをSliceに割り当てる
例としてdata-worker.serviceというサービスがあるとします。設定ファイルを開きます。
sudo systemctl edit --full data-worker.service
[Service]セクションにSlice=background-worker.sliceを追加します。
[Unit]
Description=Data Worker Background Service
After=network.target
[Service]
Type=simple
User=workeruser
Slice=background-worker.slice
ExecStart=/usr/bin/python3 /opt/worker/app.py
Restart=always
[Install]
WantedBy=multi-user.target
systemdの設定を再読み込みし、サービスを再起動します。
sudo systemctl daemon-reload
sudo systemctl restart data-worker.service
ステップ4:単一サービスをクイックに制限する(Drop-in override)
Sliceを新設せず、Nginxのような特定の単一サービスのみを手軽に制限したい場合は、overrideファイルを使用します。
sudo systemctl edit nginx.service
リソース調整パラメータを記述します。
### Editing /etc/systemd/system/nginx.service.d/override.conf
[Service]
# Nginxに最大2 vCPU(200%)の使用を許可
CPUQuota=200%
# メモリ上限を2GBに制限
MemoryMax=2G
# リソース競合時のCPU優先度ウェイト(デフォルトは100)
CPUWeight=200
変更を反映します。
sudo systemctl daemon-reload
sudo systemctl restart nginx
ステップ5:リソース使用状況のモニタリングと動作確認
各Sliceの実際のリソース消費状況は、以下のコマンドでリアルタイムに確認できます。
systemd-cgtop -m
対象Sliceが現在消費しているメモリ使用量や制限値に達したイベント回数を確認するには、以下を実行します。
# 現在のメモリ使用量を確認
cat /sys/fs/cgroup/background-worker.slice/memory.current
# MemoryHigh到達やOOM発生のイベント回数を確認
cat /sys/fs/cgroup/background-worker.slice/memory.events
cgroups v2を活用してプロアクティブにリソースの境界線を設けることで、システムの安定稼働を維持できます。万が一バックグラウンドプロセスでメモリリークが発生しても、主要な基幹サービスへの影響を完全に遮断し、安全に保護することが可能です。

