Linux上のcgroups v2によるプロセスのリソース制御:Dockerを使わずにCPU、メモリ、I/Oを制限する方法

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

クイックテスト:3分で500MBেরメモリ制限を設定する

お急ぎで、すぐに結果を確認したいですか? プロセスが使用できるメモリを最大500MBに制限してみましょう。Ubuntu 22.04+、Fedora、AlmaLinux 9などの現代的なLinuxディストリビューションでは、cgroups v2のすべての機能が/sys/fs/cgroupディレクトリに集約されています。

まず、新しい管理グループ(例:han-che-app)を作成します:

sudo mkdir /sys/fs/cgroup/han-che-app

このグループに500MBのメモリ上限を設定するのは非常に簡単です:

echo "500M" | sudo tee /sys/fs/cgroup/han-che-app/memory.max

適用するには、プロセスのPID को cgroup.procsファイルに書き込むだけです。よりプロフェッショナルな方法としては、systemd-runを使用して、制限を適用した状態でPythonスクリプトを起動する方法があります:

systemd-run --user --scope -p MemoryMax=500M python3 crawler_data.py

これで、スクリプトが突然サーバーのメモリを「食いつぶす」心配をすることなく、安心して実行できるようになります。

cgroups v2:次世代のリソース管理フレームワーク

cgroups(Control Groups)は、プロセスグループに対してCPU、メモリ、帯域幅、I/Oなどのリソースを割り当てるLinuxカーネルの中核機能です。煩雑だったv1とは異なり、cgroups v2はすべてを一つに統合しました。ディレクトリ構造(階層構造)に従って動作するため、非常に明快です。

実際、CentOS 7などで動作する古いプロジェクトでは、cgroups v1の最適化はその分散した構造のせいでシステム管理者を悩ませてきました。新しいカーネルバージョンに移行したことで、管理は格段に楽になりました。/sys/fs/cgroup内に作成する各フォルダーは「安全地帯」のような役割を果たし、リソースを大量に消費するプロセスがシステム全体に影響を与えるのを防ぎます。

Dockerがあるのに、なぜ直接cgroupsを使うのか?

多くの人は、Dockerの--memory--cpusといったパラメータで十分だと考えがちです。しかし、本番環境では必ずしもすべてをコンテナ化するわけではありません。レガシーアプリケーション、バックグラウンドで動作するcronジョブ、あるいはデータベースの中核などは、パフォーマンスを最大限に引き出すためにVM上で直接実行されることがよくあります。

cgroups v2を直接使用することには、以下のような実質的なメリットがあります:

  • Dockerのオーバーヘッド(わずかではありますが)を完全に排除できる。
  • OS上で直接動作するシステムサービス(Nginx、MySQLなど)を厳密に制御できる。
  • DockerやKubernetesが内部で動かしている仕組みそのものを深く理解できる。

アプリケーションの重要なパラメータ設定

1. CPU使用率の制限(CPU Quota)

cgroups v2の世界では、CPUはcpu.maxファイルを通じて制御されます。このファイルは、使用時間の上限と周期(デフォルトは100ms)の2つの値を受け取ります。

例えば、あるプロセスを1CPUコアの最大20%までに制限したい場合:

# 100ms周期のうち20msを使用する
echo "20000 100000" | sudo tee /sys/fs/cgroup/han-che-app/cpu.max

2. メモリ管理(Memory Limit)

システムのハングアップを避けるために、特に注意すべき2つのしきい値があります:

  • memory.high:警告しきい値。この値に達すると、システムはプロセスにメモリの解放(reclaim)を促しますが、すぐにはプロセスを強制終了しません。
  • memory.max:絶対的な上限。この数値を超えると、カーネルは即座にOOM Killerを起動し、プロセスを終了させます。
echo "1G" | sudo tee /sys/fs/cgroup/han-che-app/memory.high
echo "1.5G" | sudo tee /sys/fs/cgroup/han-che-app/memory.max

3. ディスク読み書き速度の制限(I/O)

この機能は、バックアップスクリプトや大容量ファイルの圧縮などに非常に役立ちます。バックアッププロセスがディスク帯域を占有してしまい、ウェブサイトがデータにアクセスできなくなるような事態は避けたいものです。

まず、lsblkを使用してディスクID(major:minor)を確認します。ディスクが8:0で、読み込み速度を10MB/sに制限したい場合は以下のようになります:

echo "8:0 rbps=10485760" | sudo tee /sys/fs/cgroup/han-che-app/io.max

Systemdによるプロフェッショナルなデプロイ

/sys/fs/cgroup内のファイルを直接操作するのは、クイックデバッグに適しています。本番環境では、Systemdのサービスファイルに直接設定を記述するのが標準的な方法です。

サービスファイル(例:/etc/systemd/system/my-app.service)を開き、[Service]セクションに以下の行を追加します:

[Service]
ExecStart=/usr/bin/python3 /opt/my-app/main.py
# CPUを50%に制限
CPUQuota=50%
# メモリ制限:ハード制限 1GB、ソフト制限 800MB
MemoryMax=1G
MemoryHigh=800M
# IO読み書き速度を10MB/sに制限
IOReadBandwidthMax=/dev/sda 10M

最後に、systemctl daemon-reloadを実行してサービスを再起動するだけです。システムが残りの処理をクリーンに自動実行してくれます。

実践的なアドバイス:数字に騙されないために

多くのトラブルシューティングを経て、cgroups v2を扱う際の貴重な教訓をいくつか得ました:

  1. 常にバッファを確保する: Javaアプリケーションが起動に1GBのメモリを必要とする場合、memory.maxをちょうど1GBに設定してはいけません。予期せぬオーバーヘッドのために20〜30%ほど余裕を持たせてください。
  2. memory.highを優先する: カーネルのOOM Killerによっていきなり「斬り捨て」られる前に、アプリケーション自身にメモリを解放する機会を与えてください。
  3. memory.eventsを観察する: ここには貴重な情報が詰まっています。アプリケーションがメモリ不足でいつ「九死に一生を得た」のかを正確に教えてくれます。

かつて、cgroup内で動作するJavaアプリケーションで困難な事例に遭遇しました。ヒープサイズを1GBに設定していたにもかかわらず、アプリが頻繁にクラッシュしたのです。調べてみると、JVMがオフヒープとスタックにさらに約200MB消費していることがわかりました。memory.eventsを確認し、oom_killのカウンターが急増しているのを見てようやく原因を理解し、数値を適切に調整しました。

アプリケーションの特性を理解していれば、cgroups v2は決して難しくありません。この記事が、サーバー上の「手に負えない」プロセスを管理するための強力なツールになれば幸いです!

Share: