Linux
systemd
cgroups
cgroup-v2
systemd-run

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以上ではないと下記の手順はそのままでは使えないはず。


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/user@.service.d/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/user@1000.service/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 に設定した数値が記入され制限が反映されます。すべてのユーザーに同じ制限を与えるには以下のようにやるとできます。


  • 以下のset-memhigh.conf を新たに以下の場所に作る。+はroot権限でスクリプトを実行し、%iはこの文脈ではユーザーIDになる。(ディレクトリ名間違いを修正しました)


/etc/systemd/system/user@.service.d/set-memhigh.conf

[Service]

Type=simple
ExecStartPost=+/root/set-memoryhigh.sh %i



  • set-memoryhigh.sh を以下の場所に作り、chmod 755する。


/root/set-memoryhigh.sh

#!/bin/bash

exec >>/var/tmp/log.txt 2>&1 # /var/tmp/log.txtにログが残ります
set -x
for d in /sys/fs/cgroup /sys/fs/cgroup/user.slice /sys/fs/cgroup/user.slice/user-$1.slice; do
for
i in memory pids; do
echo "+${i}" >>${d}/cgroup.subtree_control
done
done

/bin/echo "10G" >> /sys/fs/cgroup/user.slice/user-$1.slice/memory.high # 10Gは適当に変えて下さい
exit 0

以上の作業でログインしたときに/sys/fs/cgroup/user.slice/user-ユーザーID.slice/memory.highに適切な制限が書き込まれていてカーネルがその制限を強制する。制限を超えたら即座にプロセスを殺したい場合はmemory.highと並行してmemory.maxに適切な値を書き込むことが出来る。memory.maxの動作はcgroup v1のメモリ制限とほぼ同じである。


一時的なメモリーの制限

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

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

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


rootユーザー権限による特定プロセスの自動的な実メモリ制限(他の節と独立に使える)

以下のように書いたシェルスクリプトをcrontabで10分ごとにroot権限で実行すると、合計占有メモリ25GBに制限された「砂場」に'python3'または'caffe'で始まるプロセスを自動的に入れて、実行優先度を下げてくれる


/root/memory-sandbox.sh

#/bin/bash

mkdir -p /sys/fs/cgroup/memory-sandbox
echo '+memory' >/sys/fs/cgroup/cgroup.subtree_control
echo 25G >/sys/fs/cgroup/memory-sandbox/memory.high
pgrep '^python3|^caffe' >/sys/fs/cgroup/memory-sandbox/cgroup.procs
renice -n 19 -p `pgrep '^python3|^caffe'`
exit 0

なお systemdの説明書 にはcgroupを勝手にいじくり回してはいけないと書いてあるから上記のやり方はsystemdで禁止されていて、将来のsystemdで動かなくなっても文句は言えない。


さらなる情報

cgroup v2の日本語による説明として第38回 Linuxカーネルのコンテナ機能 ― cgroupの改良版cgroup v2 [2]がよいと思う。memory.highの説明はオリジナルの英語文書が現状では一番わかりやすいのではないかしら?またフェイスブックのエンジニアがFBのサーバーでcg v2を用いてよい成果を得ていると発表していた。またsystemdの作者がsystemdによるcgroupの扱い方の説明を更新している