Linux cgroup v2で実メモリ占有量を制限する


前書き

Linux 4.5から正式導入されたcgroup v2を用いてプロセス群の実メモリー総占有量を制限する手順を解説する。cgroup v2を使うメリットは、cgroup v1では設定した制限を超過するとプロセスがいきなり殺されるが、v2ならプロセスを殺さずに実メモリ占有量を抑えられることである。またcgroup v1は徐々に非推奨扱いになる。以下の手順はUbuntu 18.04とDebian buster、およびstretch-backportsからsystemd 237をインストールしたDebian stretchで確認した。systemdのバージョンは最低236以上でLinuxカーネルバージョンは最低4.5以上ではないと下記の手順はそのままでは使えないはず。

Debianはsystemdのパッケージバージョン247.2-2でcgroup v2に切り替えが行われた。Redhat 8でcgroup v2がテクノロジープレビューとして導入されているFedora 31からcgroup v2が標準となった。それでdockerが使えなくて困っている話を見かけるがバージョン20.10以降ではdockerでも問題なくcgroup v2で使える。またdocker互換のpodmanがCgroup V2対応なのでそちらを使えば問題ない。Ubuntuには公式のインストール手順があるが、それでうまくいかない場合やDebianの場合でも別の手順 でインストールできる。LXCをCGroup V2で使う手順はここDebianのデフォルトもCGroup V2にしようという動きがある

cgroup v1をsystemdにマウントさせない下準備(この手順は必須)

systemd はデフォルトでは /sys/fs/cgroup 以下にcgroup v1とv2の両方のファイルシステムをマウントし、あるプロセスがcgroup v1で制御されているとcgroup v2では制御出来ないため、cgroup v2のファイルシステムだけマウントする。

  1. /etc/default/grubGRUB_CMDLINE_LINUX_DEFAULT=systemd.unified_cgroup_hierarchy=1 を追加する
  2. update-grub を実行して再起動する
  3. もしcgroup v1を完全に禁止したいならcgroup_no_v1=allも追加する。これはあまりお勧めしない

参考:本記事著者が他に使っているカーネルオプション

systemd 239のバグに対応する設定ファイル(240以降では不要)

ここに書いてあるバグ報告に書いてあるように、以下に述べることの多くはバージョン239で動かない。原因は user-$UID.slice というsystemdのユニットがうまく初期化されていないようで、

/etc/systemd/system/user-nonexistent.slice
[Unit]
Before=systemd-logind.service
[Slice]
Slice=user.slice
[Install]
WantedBy=multi-user.target
/etc/systemd/system/user.slice
[Unit]
Before=systemd-logind.service
[Install]
WantedBy=multi-user.target

という2つのファイルを作成し、systemctl enable user.slice user-nonexistent.slice で起動時にuser.sliceuser-nonexistent.slice が実行されるようにすると回避できる。

一般ユーザーによる個別プロセスの実メモリ制限(他の節と独立に使える)

一般ユーザーが実メモリ(とその他の)制限を使えるようにするためのシステム設定

以下のファイルを新規に作成する

/etc/systemd/system/[email protected]/delegate-cgroup.conf
[Service]
# 次の行は以下で述べる systemd-run --user を用いたメモリ制限のために必要
Delegate=memory pids

一つのプロセスの占有実メモリを制限したいとき

個別のプロセスの占有実メモリを制限するには例えば以下のように行う。systemd-run --user を用いるためには dbus-user-session パッケージがインストールされていることが必要である。

$ systemd-run --user --no-block -p MemoryHigh=5G google-chrome

複数のプロセスの合計占有実メモリを制限したいとき

$ systemd-run --user --scope -p "Delegate=memory pids" -p MemoryHigh=5G bash
# 上記のコマンドでエラーになったら次のコマンドを代わりに試してください
$ systemd-run --user -t -p "Delegate=memory pids" -p MemoryHigh=5G bash

とすると新しく起動したbashならびにそこから起動されたすべてのプロセスの合計占有実メモリが5Gに制限された環境が出来るので、実メモリの過剰消費が起きる危険があるプログラムをそのbashから起動する。systemd-run --user により作られたCGroupの制御ファイルは /sys/fs/cgroup/user.slice/user-1000.slice/[email protected]/run-u0.service 付近にあり、そこにあるファイルの所有者はsystemd-runを起動したユーザーで書き込み権限も与えられているため、そこにあるファイルに書き込むことでCGroupによる制限を好きなように操作出来る。

systemd 237まで修正されなかったsystemd-run --user --scopeのバグ

筆者の環境(Ubuntu-Mate 18.04, systemd 237)では、GUIでMATE端末を起動してそこからsystemd-run --user --scopeすると問題なく起動するが、CTRL-ALT-F1でCUIに切り替えてそこからsystemd-run --user --scopeを起動すると失敗して、journalctl -xeに「Failed to add PIDs to scope's control group: Permission denied」というログが残る。systemdをcgroup v2専用にしたときにsystemd-run --user --scope が動作しないバグは1年以上修正されていなくてsystemd 238で完全に修正された

一般ユーザーの合計占有実メモリの制限(他の節と独立に使える)

systemd 239以降でユーザーごとに占有実メモリを制限する手順

以下のファイルを作って置くだけでよい。簡単

/etc/systemd/system/user-.slice.d/hoge.conf
[Slice]
MemoryHigh=90%

systemd 237でユーザーごとに占有実メモリを制限する手順

user-.slice.d という書き方に対応していないから以下のようにする。

/etc/systemd/system/user-1000.slice
[Slice]
Slice=user.slice
MemoryHigh=90%

1000 のところはユーザーIDで、ユーザーごとにファイルを作成して下さい。systemctl daemon-reloadを実行すると次回のログインから/sys/fs/cgroup/user.slice/user-1000.slice/memory.high に設定した数値が記入され制限が反映されます。

一時的なメモリーの制限

既にログインしているユーザーの使用メモリーを一時的に制限したいなら

systemctl set-property --runtime user-1000.slice 'MemoryHigh=80%'

というコマンドをroot権限で実行すればよい。1000は制限したいユーザーのUIDに置き換える。

さらなる情報

cgroup v2の日本語による説明として第38回 Linuxカーネルのコンテナ機能 ― cgroupの改良版cgroup v2 [2]がよいと思う。memory.highの説明はオリジナルの英語文書が現状では一番わかりやすいのではないかしら?

またResource Control @ FB はCgroup V2とPSI (Pressure Stall Information)を組み合わせてリソース配分をうまくやる実践について紹介している。FB技術者によるウェブサイトはcgroup2: Control and maximize resource utilization