TenForward

技術ブログ。はてなダイアリーから移転しました

Linux 4.20 で導入の PSI 機能(2)

前回 はとりあえず /proc/pressure の中身を見てみましたが、PSI 機能は cgroup v2 とも連携しています。というわけで、今回は cgroup v2 をマウントしてみましょう(前回以上に中身なし)。

まずは cgroup v2 をマウントして、コントローラーを子 cgroup で使えるように登録します。このあたりは連載 をどうぞ。

$ sudo mount -t cgroup2 -o nsdelegate cgroup2 /sys/fs/cgroup/
$ echo "+io +memory +cpu +pids" | tee /sys/fs/cgroup/cgroup.subtree_control
+io +memory +cpu +pids
$ cat /sys/fs/cgroup/cgroup.subtree_control
cpu io memory pids

子 cgroup を作って中身を覗いてみましょう。

$ sudo mkdir test01
$ ls
cgroup.controllers      cgroup.stat             cpu.pressure     io.max          memory.high       memory.pressure      pids.current
cgroup.events           cgroup.subtree_control  cpu.stat         io.pressure     memory.low        memory.stat          pids.events
cgroup.max.depth        cgroup.threads          cpu.weight       io.stat         memory.max        memory.swap.current  pids.max
cgroup.max.descendants  cgroup.type             cpu.weight.nice  memory.current  memory.min        memory.swap.events
cgroup.procs            cpu.max                 io.latency       memory.events   memory.oom.group  memory.swap.max

"pressure" という名前が付くファイルが登場していますね。

$ for f in $(ls *.pressure); do echo "$f: "; cat $f; done
cpu.pressure: 
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
io.pressure: 
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0
memory.pressure: 
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0

はい。前回の /proc/pressure 以下に登場したファイルと同じような中身ですね。

Linux 4.20 で導入の PSI 機能(1)

以前から Facebook のスライドなんかでは見かけた PSI という機能が 4.20 カーネルで入ったようですね。

CONFIG_PSI:

Collect metrics that indicate how overcommitted the CPU, memory,
and IO capacity are in the system.

If you say Y here, the kernel will create /proc/pressure/ with the
pressure statistics files cpu, memory, and io. These will indicate
the share of walltime in which some or all tasks in the system are
delayed due to contention of the respective resource.

In kernels with cgroup support, cgroups (cgroup2 only) will
have cpu.pressure, memory.pressure, and io.pressure files,
which aggregate pressure stalls for the grouped tasks only.

For more details see Documentation/accounting/psi.txt.

これを有効にすれば良さそう。さっそく Y で 4.20 を make。/proc/pressure なんてディレクトリが現れて、そこにファイルが 3 つほど置かれていました。

$ uname -a
Linux discovery 4.20.0-plamo64 #2 SMP PREEMPT Mon Jan 7 16:31:47 JST 2019 x86_64 GNU/Linux
$ for f in $(ls /proc/pressure/); do echo "$f: "; cat /proc/pressure/$f; done
cpu: 
some avg10=0.00 avg60=0.00 avg300=0.00 total=60048550
io: 
some avg10=0.00 avg60=0.00 avg300=0.00 total=30250330
full avg10=0.00 avg60=0.00 avg300=0.00 total=28401529
memory: 
some avg10=0.00 avg60=0.00 avg300=0.00 total=146509
full avg10=0.00 avg60=0.00 avg300=0.00 total=100482

cgroup2 をマウントすれば、cgroup にも同じようなファイルが現れるらしいがとりあえず今日はここまで(内容なさすぎ)。

af-graft を LXC コンテナで試してみた

Twitter でコンテナ用の面白い仕組みを見つけたので試してみました。中身ほぼゼロです。

論文も公開されているようですね。

README を見る限りは普通に LXC で使えそうなのでロクにドキュメント読まず(コラ)やってみました。

とりあえず Ubuntu 16.04 ホストを準備します(AWS 上に準備しました)。そして LXC をインストールします。こういうときは LXD でなく設定ファイルちょいちょいといじって起動させられる LXC 便利。

host:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.5 LTS
Release:    16.04
Codename:   xenial
host:~$ sudo apt install lxc

コンテナを作ります。

host:~$ lxc-create -t download -n c1 -- -d ubuntu -r xenial -a amd64
host:~$ sudo egrep '(lxc.net*|mount)' /var/lib/lxc/c1/config
lxc.network.type = empty
lxc.mount.entry = /home/ubuntu/af-graft home/ubuntu/af-graft none bind,relative,create=dir 0 0
  • lxc.network.type(LXC 3.0では lxc.net.0.type)は empty でループバックのみ
  • lxc.mount.entry はホスト側でコンパイルしたバイナリをコンテナ内でもそのまま使いかかったので書いてるだけで特に重要ではありません
  • CAP_NET_ADMIN が必要ですが、LXC コンテナではデフォルト許可なので capability はいじらず

ってところです。

ここでコンテナを起動します。コンテナには Apache がインストールされていて、コンテンツとして簡単な index.html を準備しました。

host:~$ sudo lxc-start -n c1
host:~$ sudo lxc-attach -n c1
root@c1:~# 
root@c1:~# cat /var/www/html/index.html
<h1>c1</h1>

ここからが本番。エンドポイントを作成します。

root@c1:~# af-graft/iproute2-4.10.0/ip/ip graft add nx4 type ipv4 addr 0.0.0.0 port 80
root@c1:~# af-graft/iproute2-4.10.0/ip/ip graft add nx6 type ipv6 addr :: port 80
root@c1:~# af-graft/iproute2-4.10.0/ip/ip graft show
nx4 type ipv4 addr 0.0.0.0 port 80 
nx6 type ipv6 addr :: port 80 

はい、できましたね。apache 起動してみましょう。

root@c1:~# LD_PRELOAD=af-graft/tools/libgraft-hijack.so GRAFT_CONV_PAIRS="0.0.0.0:80=nx4 :::80=nx6" apachectl start
libgraft-hijack.so:466:socket(): overwrite family 10 with AF_GRAFT (4)
libgraft-hijack.so:597:setsockopt(): wrap setsockopt() level=1, optname=15
libgraft-hijack.so:466:socket(): overwrite family 2 with AF_GRAFT (4)
libgraft-hijack.so:466:socket(): overwrite family 10 with AF_GRAFT (4)
libgraft-hijack.so:597:setsockopt(): wrap setsockopt() level=1, optname=2
libgraft-hijack.so:597:setsockopt(): wrap setsockopt() level=1, optname=9
libgraft-hijack.so:597:setsockopt(): wrap setsockopt() level=1, optname=15
libgraft-hijack.so:540:bind(): convert bind :::80 to nx6

起動しました。試しに netstat してみると

root@c1:~# netstat -napt
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name

何もありません。

では、アクセスしてみましょう。

  • ホストから
host:~$ curl http://localhost/
<h1>c1</h1>
  • Internet 経由で
$ curl http://afgrafttest.ten-forward.ws/
<h1>c1</h1>

ま、動いて当たり前なのでしょうが、試してみましたってことでご報告。論文なんかにパフォーマンス比較してありますが、手元でも時間があればやってみたいですね。macvlan とか ipvlan との比較も含めて(論文で何と比較してるのかは見てない)。

cgroup v2 の nsdelegate オプション(2)〜 cgroup namespace 作成後の cgroup root を権限委譲の境界として扱う

cgroup v2 の nsdelegate オプション第 2 弾です。

前回紹介したのは、cgroup namespace を作ると cgroup root を超えたプロセスの移動が禁止されるという機能でした。

これはユーザに関わらず関係するお話でしたが、今回は一般ユーザで namespace(つまりコンテナ)を作ったときのお話です。

私の連載の第 40 回 で紹介したとおり、cgroup v1 に比べて cgroup v2 は一般ユーザでプロセスを移動できる条件が厳しくなりました。ただ、これは実際のユースケースに合っている、というのは連載で書いたとおりです。

一般ユーザに cgroup ツリーを権限委譲する際には、権限委譲する root cgroup に当たる cgroup には、

という権限を与えました。

cgroup 内でコントローラを使えるようにしていても、権限委譲の root cgroup 内のコントローラ関連のファイルには権限は与えません。コンテナに与えるリソースを定義するのはあくまでホストの管理者であって、コンテナの管理者が自身に与えられたリソースを変更できるとマズいから、というのは納得の行くところだと思います。

これを実現するために、前述のようにホスト側の管理者がパーミッションの調整を行っていたわけですが、nsdelegate オプションをつけると、これが自動的に行われます。

nsdelegate の有無で処理がどう変わるか見てみましょう。処理は

  1. cgroup v2 をマウントする(オプション有無)
  2. cgroup.subtree_control にコントローラを登録
  3. cgroup 作成
  4. cgroup ディレクトリとそれ以下のファイルに所有権を与える
  5. cgroup にシェルの PID 登録
  6. cgroup namespace 作成
  7. 作成した cgroup のコントローラを制御するファイル(ここでは pids.max)に制限値を書き込む

という処理をやってみました。

nsdelegate を付けない場合

では、まず普通に cgroup v2 をマウントして処理をすすめましょう。

# mount -t cgroup2 cgroup2 /sys/fs/cgroup/ (1)
# echo "+io +pids +memory" > /sys/fs/cgroup/cgroup.subtree_control (2)
# mkdir /sys/fs/cgroup/test01 (3)
# chown karma /sys/fs/cgroup/test01   (4)
# chown karma /sys/fs/cgroup/test01/* (4)
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs (5)
4013
$ unshare --user --cgroup --map-root-user (6)
root@plamo70b2:~# ls -l /sys/fs/cgroup/test01 (権限を確認)
  : (snip)
-rw-r--r-- 1 root nogroup 0  9701:03 pids.max

このように pids.max には書き込み権がありますね。

一応 cgroup root にいるか確認します。

# cat /proc/$$/cgroup 
0::/

ここで pids.max に制限値として 50 を書き込みます。

# echo 50 > /sys/fs/cgroup/test01/pids.max (7)
# cat /sys/fs/cgroup/test01/pids.max 
50

登録できました。普通の cgroup v2 のコントローラ操作ですね。

nsdelegate を付けた場合

nsdelegate をオプションで与える以外は同じ操作をしてみます。

# mount -t cgroup2 -o nsdelegate cgroup2 /sys/fs/cgroup/ (1)
# echo "+io +pids +memory" > /sys/fs/cgroup/cgroup.subtree_control  (2)
# mkdir /sys/fs/cgroup/test01 (3)
# chown karma /sys/fs/cgroup/test01   (4)
# chown karma /sys/fs/cgroup/test01/* (4)
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs (5)
4013
$ unshare --user --cgroup --map-root-user (6)
# ls -l /sys/fs/cgroup/test01/
  : (snip)
-rw-r--r-- 1 root nogroup 0  9701:25 pids.max

先ほどと同様に書き込み権はあります。

cgroup root にいることも確認しておきます。

# cat /proc/$$/cgroup 
0::/

先ほどと同様に制限値を書き込みます。

# echo 50 > /sys/fs/cgroup/test01/pids.max (7)
-bash: echo: 書き込みエラー: 許可されていない操作です

今回は先ほどと違ってエラーになりました。

このように権限委譲した場合、その root cgroup には限定的な権限のみを与えるのが nsdelegate のもうひとつの機能です。

つまり cgroup namespace (つまりコンテナ)を権限委譲の境界とし、その root cgroup (コンテナの root cgroup)には限定的な権限を与え、それ以下の cgroup のみ権限委譲対象のユーザ管理とするわけです。

LXDでコンテナごとに異なるユーザ権限でコンテナを起動する

$ lxc version
Client version: 3.0.1
Server version: 3.0.1

な環境の Ubuntu 18.04 上で試してます。

以下で何の前提もなく書いてるサブ ID のお話は、私の連載

に書いていますのでそちらをどうぞ。

LXD でコンテナを起動すると、デフォルトでは非特権コンテナが起動します。

$ lxc launch ubuntu:18.04 c1
Creating c1
Starting c1
$ lxc list
+------+---------+--------------------+-----------------------------------------------+------------+-----------+
| NAME |  STATE  |        IPV4        |                     IPV6                      |    TYPE    | SNAPSHOTS |
+------+---------+--------------------+-----------------------------------------------+------------+-----------+
| c1   | RUNNING | 10.56.2.183 (eth0) | fd42:dce3:4ae3:1cb3:216:3eff:fe25:e3c3 (eth0) | PERSISTENT | 0         |
+------+---------+--------------------+-----------------------------------------------+------------+-----------+

誰の権限で起動しているか確認してみます。

$ ps auxf
  : (snip)
root     13607  0.0  0.1 530556  7344 ?        Ss   13:31   0:00 [lxc monitor] /var/lib/lxd/containers c1
100000   13626  0.1  0.2 159436  8816 ?        Ss   13:31   0:00  \_ /sbin/init
  : (snip)

このように uid: 100000 のユーザで起動しています。

ここでもうひとつコンテナを起動します。

$ lxc launch ubuntu:18.04 c2
Creating c2
Starting c2
$ ps aux
  : (snip)
root     14507  0.0  0.1 529148  7264 ?        Ss   13:34   0:00 [lxc monitor] /var/lib/lxd/containers c2
100000   14532  1.4  0.2  77420  8660 ?        Ss   13:34   0:00 /sbin/init
  : (snip)

こちらも uid: 100000 のユーザで起動します。

このようにデフォルトでは同一ホスト上で起動するコンテナはすべて同じユーザ権限で起動します。マルチテナントで、コンテナごとにユーザが異なる場合、セキュリティを考えると異なるユーザ権限でコンテナが実行されている方が望ましいでしょう。

LXD ではこれを制御する設定 security.idmap.isolated があります。デフォルトではこれは false ですので、コンテナもしくはプロファイルで true としておくと、異なる uid/gid の範囲を使ってコンテナを起動します。

この辺りは、公式の userns-idmap あたりに詳しいです。

/etc/subuid, /etc/subgid の設定

デフォルトではコンテナには 65536 個の ID を割り当てるので、Ubuntu でユーザを登録すると、subuid/subgid として 65536 個の ID を使えるように設定されています(LXDはコンテナをrootユーザから起動するのでrootに対して)。

$ cat /etc/sub{u,g}id | grep root
root:100000:65536
root:100000:65536

十分なサブ ID が確保されていない場合、

Error: Failed container creation: Not enough uid/gid available for the container.

みたいなエラーでコンテナを作成できません。

コンテナごとに異なる ID の範囲を割り当てる場合、コンテナごとに 65536 必要ですから、sub{u,g}id でも必要な個数設定しておく必要があります。

とりあえずサブ ID はユーザごとに異なっている必要もありませんので、ケチケチ設定する必要もないので、適当に 200000 個くらい設定しましょう。(LXD がどういう計算しているかわからないけど、コンテナ 2 つ起動するから 131072 とか設定すると上記エラーが出ます)

$ cat /etc/sub{u,g}id | grep root
root:100000:200000
root:100000:200000

このあと、LXD を再起動しておきます。(サブ ID のマップを起動時に読み込むから)

$ sudo systemctl restart lxd

もう一つ気をつけること。ドキュメントにも書かれていないと思います。この辺りで発言があります。

/etc/sub{u,g}id に設定する ID の個数ですが、設定した最初の 65536 個(security.idmap.sizeの設定)は security.idmap.isolated を設定していないコンテナ専用です。

つまり先の例のように設定した場合、100000 〜 165535 までの ID は security.idmap.isolated を設定していないコンテナが使いますので、security.idmap.isolated を設定したコンテナを起動したい場合は、さらに 65536 個の ID が必要です。

ですので、ID の個数として /etc/sub{u,g}id に 65536 を設定してある環境で、ひとつもコンテナが起動していないところに、security.idmap.isolated を設定したコンテナを起動しようとしても失敗します。

少なくとも 131073 以上の設定が必要です。まあ、ケチることはないので先の例のように多めに確保しておきましょう。

profile の設定

default プロファイルの設定を変えてもいいですが、同じ ID 範囲で起動するコンテナを起動したい場合のためにそれは置いておいて、新しいプロファイルを作ります。

$ lxc profile copy default secure
$ lxc profile set secure security.idmap.isolated true
$ lxc profile show secure
config:
  security.idmap.isolated: "true"
  : (snip)

コンテナの起動

$ lxc launch ubuntu:18.04 c1 --profile=secure
Creating c1
Starting c1
$ lxc launch ubuntu:18.04 c2 --profile=secure
Creating c2
Starting c2
$ ps aux
  : (snip)
root     17072  0.0  0.1 604544  7324 ?        Ss   14:03   0:00 [lxc monitor] /var/lib/lxd/containers c1
165536   17093  0.2  0.2 159472  8892 ?        Ss   14:03   0:00  \_ /sbin/init
  : (snip)
root     17935  0.0  0.1 529148  7196 ?        Ss   14:03   0:00 [lxc monitor] /var/lib/lxd/containers c2
231072   17953  0.2  0.2 159420  8840 ?        Ss   14:03   0:00  \_ /sbin/init
  : (snip)

はい、違うユーザ権限で起動していますね。