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

# 前書き
Linux 4.5から正式導入されたcgroup v2を用いてプロセス群の実メモリー総占有量を制限する手順を解説する。cgroup v2を使うメリットは、cgroup v1では設定した制限を超過するとプロセスがいきなり殺されるが、v2ならプロセスを殺さずに実メモリ占有量を抑えられることである。また[cgroup v1は徐々に非推奨扱いになる](https://github.com/systemd/systemd/blob/master/docs/CGROUP_DELEGATION.md)。以下の手順は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がテクノロジープレビューとして導入されている](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/8/html/8.0_release_notes/technology_previews)。[Fedora 31からcgroup v2が標準となった](https://www.redhat.com/sysadmin/fedora-31-control-group-v2)。それでdockerが使えなくて困っている話を見かけるが**[バージョン20.10以降ではdockerでも問題なくcgroup v2で使える](https://docs.docker.com/engine/release-notes/#runtime-1)**。また[docker互換のpodmanがCgroup V2対応](https://www.linuxuprising.com/2019/11/how-to-install-and-use-docker-on-fedora.html)なのでそちらを使えば問題ない。[Ubuntuには公式のインストール手順がある](https://podman.io/getting-started/installation.html)が、それでうまくいかない場合やDebianの場合でも[別の手順](https://qiita.com/kakinaguru_zo/items/a7a22388a94d51d8cc54) でインストールできる。LXCをCGroup V2で使う手順は[ここ](https://qiita.com/kakinaguru_zo/items/8c82954a1bb0a1ef9a40)。[DebianのデフォルトもCGroup V2にしようという動きがある](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=943981)。

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

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

  1. `/etc/default/grub` の `GRUB_CMDLINE_LINUX_DEFAULT=` に[`systemd.unified_cgroup_hierarchy=1`](https://www.freedesktop.org/software/systemd/man/systemd.html#systemd.unified_cgroup_hierarchy) を追加する
  2. `update-grub` を実行して再起動する
  3. [もしcgroup v1を完全に禁止したいなら`cgroup_no_v1=all`も追加する](https://www.kernel.org/doc/html/v4.17/admin-guide/kernel-parameters.html)。これはあまりお勧めしない

参考：[本記事著者が他に使っているカーネルオプション](https://qiita.com/kakinaguru_zo/items/e3bf2f3faac0192ec063)

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

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

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

```text:/etc/systemd/system/user.slice
[Unit]
Before=systemd-logind.service
[Install]
WantedBy=multi-user.target
```

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


# 一般ユーザーによる個別プロセスの実メモリ制限(他の節と独立に使える)
## 一般ユーザーが実メモリ（とその他の）制限を使えるようにするためのシステム設定
以下のファイルを新規に作成する

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

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

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

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

```shell-session
$ 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で完全に修正された](https://github.com/systemd/systemd/issues/3388)。

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

## `systemd` 239以降でユーザーごとに占有実メモリを制限する手順
以下のファイルを作って置くだけでよい。簡単

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

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

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

```text:/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］](http://gihyo.jp/admin/serial/01/linux_containers/0038)がよいと思う。`memory.high`の説明は[オリジナルの英語文書](https://www.kernel.org/doc/Documentation/cgroup-v2.txt)が現状では一番わかりやすいのではないかしら？

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

* [CGroup V2 を一般（非root）ユーザーからシェルで操作する](https://qiita.com/kakinaguru_zo/items/ed5941639daafa3ef9d9) という記事を書いた。
* [一般ユーザーからCGroup V2でLXCを使う](https://qiita.com/kakinaguru_zo/items/8c82954a1bb0a1ef9a40) という記事を書いた。