背景とリソース分離が重要となる理由
次のようなシナリオを想定してみましょう。メモリ8 GBのサーバー上で、最適化されていないCeleryワーカーや画像処理バッチジョブが突然6 GBのメモリリークを起こしました。適切なリソース境界が設定されていない場合、LinuxのOut-Of-Memory (OOM) Killerが突発的に介入します。しかし、問題のワーカーだけを隔離するのではなく、メモリを即座に解放するためにPostgreSQLやsshdを強制終了してしまう可能性があります。これがいわゆる典型的な「ノイジーネイバー(うるさい隣人)」問題です。
コントロールグループ(cgroups)は、ハードウェアリソースの測定・スロットリング・分離を行うことでこの課題を解決します。従来のCgroups v1ではコントローラーが断片化されたディレクトリツリーに分散していましたが、Cgroups v2では単一の統合階層構造が導入されました。各プロセスは必ず1つのcgroupパスにのみ属します。この構造により、予測可能なリソース割り当て、明確な権限委任、そして匿名メモリとファイルシステムページキャッシュの両方に対する正確なアカウンティング(使用量把握)が実現されます。
/sys/fs/cgroup/に対して直接低レベルなスクリプトを記述する必要はありません。最新のLinuxディストリビューションでは、Slice(スライス)と呼ばれるユニットを通じてこの管理をsystemdに委任できます。Sliceを使用するとサービスを論理グループに整理でき、わずか数行の宣言的なディレクティブを記述するだけで、ワークロード層全体にCPUウェイト、メモリ上限、ディスクI/O制限を適用できます。
導入とCgroups v2の有効化
Ubuntu 22.04+、Debian 11+、Fedora 31+、RHEL 9+などのモダンなディストリビューションでは、Cgroups v2がデフォルトで有効になっています。まずはサーバーの現在の稼働状況を確認しましょう。
ステップ1: 現在のCgroupバージョンの確認
statコマンドでcgroupファイルシステムの種類を確認します:
stat -fc %T /sys/fs/cgroup/
出力結果がcgroup2fsであれば、カーネルはすでに統合されたCgroups v2で動作しています。tmpfsが返された場合は、従来のCgroups v1で動作しています。
ステップ2: 統合階層の有効化(Cgroups v1の場合)
旧バージョンのディストリビューションで統合階層を有効にするには、GRUB経由で対応するカーネルパラメータを渡します:
sudo nano /etc/default/grub
GRUB_CMDLINE_LINUXの文字列にsystemd.unified_cgroup_hierarchy=1を追加します:
GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1"
GRUB設定を再生成し、マシンを再起動します:
# Ubuntu/Debianの場合
sudo update-grub
sudo reboot
# RHEL/AlmaLinux/Rockyの場合
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
sudo reboot
ステップ3: 利用可能なサブシステムコントローラーの確認
ルートcgroupで利用可能なアクティブコントローラーを確認します:
cat /sys/fs/cgroup/cgroup.controllers
出力にcpu、memory、io、pidsが含まれていることを確認してください。
設定: Systemd Sliceとリソース制約
Systemdはデフォルトでワークロードを以下の3つのルートスライスに自動分割します:
system.slice: バックグラウンドデーモンおよびシステムサービス。user.slice: インタラクティブなログインセッションおよびユーザーごとのプロセス。machine.slice: 仮想マシンおよびコンテナワークロード。
カスタムスライスを作成することで、重要なシステムサービスのリソースを枯渇させることなく、バックグラウンドジョブ、分析エンジン、マイクロサービスなどをサンドボックス化できます。
ステップ1: カスタムSliceユニットの定義
/etc/systemd/system/workload-workers.sliceにバックエンドワーカー専用のスライスを作成します:
sudo nano /etc/systemd/system/workload-workers.slice
以下の設定を追加して、CPUクォータ、メモリ制限、ディスク書き込み制限を適用します:
[Unit]
Description=Resource Limited Slice for Worker Services
Before=slices.target
[Slice]
# --- CPU割り当て ---
# スライス全体でCPU使用率を最大200%(フルコア2基分に相当)に制限
CPUQuota=200%
# CPUWeight (1-10000) はCPU競合時の相対的な優先度を設定
CPUWeight=500
# --- メモリ制約 ---
# MemoryHigh はプロセスをスロットリングし、ページキャッシュを積極的に回収
MemoryHigh=2G
# MemoryMax はハードリミット: 超過時はスライス内に限定したOOMを発動
MemoryMax=2.5G
# ディスクのスラッシングを防ぐためスワップメモリ使用量を制限
MemorySwapMax=512M
# --- I/Oスロットリング ---
# プライマリディスクの最大書き込み・読み込みスループットを制限
IOWriteBandwidthMax=/dev/sda 25M
IOReadBandwidthMax=/dev/sda 50M
# --- プロセス数の制限 ---
TasksMax=1024
ステップ2: カスタムSliceへのサービスの割り当て
ベンダーディレクトリ内のユニットファイルを直接編集する代わりに、systemdのドロップインオーバーライドを使用してサービスを割り当てます。例えば、queue-worker.serviceを新しいスライスに関連付けるには次のように実行します:
sudo systemctl edit queue-worker.service
オーバーライドファイル内にスライスの割り当てを追加します:
[Service]
Slice=workload-workers.slice
エディタを保存して終了します。systemdマネージャーの設定をリロードし、ワーカーを再起動します:
sudo systemctl daemon-reload
sudo systemctl restart queue-worker.service
ステップ3: サービスユニット内での直接リソース指定(代替案)
群全体ではなく単一のスタンドアロンサービスのみをスロットリングしたい場合は、該当ユニットの[Service]セクションに直接制限を定義することも可能です:
[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/app/worker.py
Restart=on-failure
# サービスレベルのCPUおよびメモリ制限
CPUQuota=80%
MemoryHigh=800M
MemoryMax=1G
OOMPolicy=stop
検証とリアルタイムモニタリング
制限を設定したら、高負荷下でシステムがどのように動作するかをテストします。
1. アクティブな所属Sliceの確認
systemctl statusを実行して、サービスが正しいスライスで動作しているか確認します:
systemctl status queue-worker.service
出力内のCGroup:行を確認します。/workload-workers.slice/queue-worker.serviceを直接指していれば正しく設定されています。
2. systemd-cgtopによるリアルタイムリソース使用量の監視
Systemdには、cgroup階層ごとに整理されたtopライクなモニターであるsystemd-cgtopが付属しています:
systemd-cgtop -m
対話型ショートカットを使用して実行中グループをソートできます:
- cキー: CPU使用率順にソート
- mキー: メモリ消費量順にソート
- iキー: ディスクI/O操作順にソート
3. 適用されたカーネル制限の直接確認
cgroup仮想ファイルシステムを参照して、カーネルによって適用された生の値を確認します:
# 現在のメモリ使用量をバイト単位で表示
cat /sys/fs/cgroup/workload-workers.slice/memory.current
# 設定されたハードメモリ制限を表示(バイト単位または 'max')
cat /sys/fs/cgroup/workload-workers.slice/memory.max
# CPU帯域幅クォータを表示(クォータと期間をマイクロ秒単位で表示)
cat /sys/fs/cgroup/workload-workers.slice/cpu.max
4. リソース境界のストレステスト
stress-ngを使用してスライス内でメモリ負荷を擬似的に発生させ、制限が期待通りに機能するか検証します:
systemd-run --slice=workload-workers.slice --unit=stress-test \
--wait stress-ng --vm 1 --vm-bytes 3G --timeout 20s
journalctl -u stress-test.serviceまたはdmesg -Tを使用してカーネルログを確認します。カーネルが2 GB(MemoryHigh)でメモリ割り当てをスロットリングし、2.5 GB(MemoryMax)でスライス内に隔離されたOOM Killを発動させていることが確認できます。データベース、Web、SSHサービスは一切影響を受けずに安全に保護されます。

