LinuxにおけるCgroups v2とsystemd Sliceによるリソース制限の管理

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

背景とリソース分離が重要となる理由

次のようなシナリオを想定してみましょう。メモリ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サービスは一切影響を受けずに安全に保護されます。

Share: