LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術

第41回Linuxカーネルのコンテナ機能 ― cgroupの改良版cgroup v2[5]

前回の最後で「次回以降はしばらく私ではなくudzuraさんにお書きいただく予定です」と書きました。しかし、年末に近づきAdvent Calendarの募集が始まるようになり、Advent Calendarの存在をすっかり忘れていたことに気づきました。

毎年、Advent Calendarに参加していましたので今年も何か参加しようと思って登録だけはしました。その後に書く内容を考えたのですが、思いついた内容がcgroup v2関連でしたので、これは連載の記事にしよう思いました。というわけで今回も前回までに続いて加藤による記事となります。この記事はLinux Advent Calendar 2018の14日目の記事です。

今回は4.13、4.14カーネルでcgroup v2に追加された機能について紹介します。

nsdelegate オプション

第38回でcgroup v2のマウント方法を紹介しました。第38回で使用していた4.11カーネルの時点では、cgroup v2をマウントする際に指定できるオプションはありませんでした。

その後、4.13カーネルでnsdelegateというマウントオプションがひとつだけ追加されました。現時点でもcgroup v2のマウントで指定できるオプションは、このnsdelegateのみです。

このオプションは、Linuxを起動したあとの初期の名前空間でcgroup v2をマウントするときのみ指定できます。すでにcgroup v2をマウントしている場合は、再マウントするにより有効にできます。それ以外の名前空間でマウントしても無視されます。

このオプションは、カーネル付属文書のcgroup v2文書では「cgroup名前空間を権限委譲の境界とみなします」と書かれています。このように、この連載の第34回で紹介したcgroup名前空間と連携して動作します。

それでは、この「権限委譲の境界とみなす」とはどういうことなのかを説明していきましょう。

cgroup間のプロセスの移動

cgroup名前空間は第34回で説明したとおり、新しく作成した名前空間内では、プロセスが所属しているcgroupがツリーのrootに見えるという機能でした。

詳しくは第34回を参照していただくとして、簡単に紹介しておきましょう。

次の例は4.14.44カーネル上のcgroup v2で試しています。シェルを"test01" cgroupに登録し、その子プロセスで新たにcgroup名前空間を作成します。そして、"/test01"にいるはずのプロセスから所属しているcgroupを確認すると、root/にいるように見えました。

$ sudo mkdir /sys/fs/cgroup/test01 (test01 cgroupを作成)
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs (カレントシェルをtest01に登録)
27668
$ sudo unshare --cgroup /bin/bash (cgroup名前空間を作成)
# echo $$ (自身のPIDを確認)
27700
# cat /proc/self/cgroup (自分が所属するcgroupを確認)
0::/

念のため、作成した名前空間の外から確認してみると次のように確かに"/test01"にいることが確認できます。

$ sudo cat /proc/27700/cgroup 
0::/test01

nsdelegateオプションを指定しない場合

上記の例はnsdelegateを指定せずにcgroup v2をマウントしていますので、そのままおさらいとして、第34回でも紹介したように、"test01"と同じ階層に作成した"test02"というcgroupにプロセスを移動して、所属するcgroupを確認してみましょう。

# mkdir /sys/fs/cgroup/test02 (root直下にtest02を作成)
# echo $$ > /sys/fs/cgroup/test02/cgroup.procs (カレントシェルをtest02に移動)
# cat /proc/self/cgroup (現在のcgroupを確認)
0::/../test02

元々、root直下の"/test01"にいたプロセスを、同じくroot直下の/test02に移動させました。すると、名前空間内から自身が所属するcgroupを確認すると"/../test02"に所属していると表示されました。rootの親cgroupの配下の"test02" cgroupに所属しているということで、少し変な所属に見えます。

名前空間を作るという操作は、コンテナを作成するということですから、実際のケースでは、コンテナをまたいでプロセスを移動させるという操作は考えづらいです。しかし、上の例のようにコンテナをまたいでプロセスを移動できました。このような不自然な動きを拒否するのに、新たに追加されたnsdelegateオプションが使えます。

早速試してみましょう。

nsdelegateオプションを指定した場合

すでにcgroup v2をマウントしている場合、再マウントを行い、nsdelegateオプションを指定してマウントできます。そして、改めて"test01" cgroupにプロセスを追加してから、cgroup名前空間を作成してみましょう。

$ sudo mount -t cgroup2 -o remount,nsdelegate /sys/fs/cgroup/
(nsdelegateオプションを指定して再マウント)
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 
27668
$ sudo unshare --cgroup /bin/bash (cgroup名前空間を作成)
# cat /proc/self/cgroup (自分が所属するcgroupを確認)
0::/

ここまでは先のnsdelegateを指定していないときの例と同じです。それではカレントシェルのPIDを"test02"に移動させてみましょう。

# echo $$ > /sys/fs/cgroup/test02/cgroup.procs 
bash: echo: write error: No such file or directory
# ls /sys/fs/cgroup/test02/cgroup.procs 
/sys/fs/cgroup/test02/cgroup.procs

上のように"No such file or directory"とエラーになりました。しかし"test02" cgroupは存在しています。

つまり"/test01"をrootとしてcgroup名前空間が作られているので、その名前空間の中からは、rootを超えて、つまり名前空間の境界を超えて、別階層のcgroupにプロセスを移動できなくなっているというわけです。これがnsdelegateオプションが持つ機能のひとつです。

ちなみに、cgroup名前空間の外からであれば、権限があれば上記の操作はエラーになることなく移動できます。

図1 nsdelegateオプションを付けたときのプロセスの移動
図1 nsdelegateオプション

cgroupの権限委譲

連載の第40回で説明したとおり、cgroup v2では一般ユーザがcgroupを管理する場合、次のような権限が必要でした。

  • cgroup(ディレクトリ)への書き込み権限
  • cgroup内のcgroup.procsファイルへの書き込み権限

そして、一般的には権限委譲するcgroup、つまり権限委譲対象のユーザにとってのcgroup root内に存在するコントローラ関連のファイルには書き込み権限を与えません。図2のように、コンテナにリソースを割り当てるのは、あくまでコンテナが稼働しているホストの管理者であるという考え方です。

図2 リソース分配の権限
図2 リソース分配の権限

この権限委譲を行うには、従来は第40回で説明したように、アクセス権を管理者自身が設定する必要がありました。nsdelegateオプションが導入されてからは、管理者がアクセス権を細かく設定する代わりに、nsdelegateオプション付きでcgroup v2をマウントするという方法が採れるようになりました。

nsdelegateオプションを指定しない場合

まずはnsdelegateを指定しない方法を改めて紹介します。ここではあえて対象となるcgroupで、一般ユーザに全権限を与えるようにしてみます。

準備として、マウントオプションなしでcgroup v2をマウントし、子cgroupでiopidsmemoryコントローラが使えるように設定します。

$ sudo mount -t cgroup2 cgroup2 /sys/fs/cgroup/ (マウント)
$ echo "+io +pids +memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control 
(子cgroupでio, pids, memoryコントローラが使えるように設定)
+io +pids +memory
$ cat /sys/fs/cgroup/cgroup.subtree_control
io memory pids (設定された)

このあたりの操作は第38回で紹介したとおりです。

ここで"test01" cgroupを作成します。そして一般ユーザである"gihyo"ユーザに対して、"test01"ディレクトリとディレクトリ内に存在するファイルすべてにアクセス権を与えます。

$ sudo mkdir /sys/fs/cgroup/test01 (cgroup作成)
$ sudo chown gihyo /sys/fs/cgroup/test01
(ディレクトリ所有権をgihyoに)
$ sudo chown gihyo /sys/fs/cgroup/test01/*
(ディレクトリ内のファイル所有権をgihyoに)

通常はディレクトリとcgroup.procsファイルにのみアクセス権を与えることが一般的です。ここでは、あとのnsdelegateオプションを指定した場合との比較のためにあえてこのようにしています。

カレントシェルのPIDを"test01" cgroupに登録します。

$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 
(カレントシェルをtest01に登録)
4352

ここでユーザ名前空間とともにcgroup名前空間を作成します。現在のユーザである"gihyo"ユーザをユーザ名前空間内の"root"ユーザにマッピングするために--map-root-userオプションを指定しています。

$ unshare --user --cgroup --map-root-user /bin/bash
(cgroup名前空間を作成しbashを実行)
# cat /proc/self/cgroup (cgroup名前空間内でrootにいることを確認)
0::/

ここではpids.maxに制限を設定することにして、このファイルと"test01"ディレクトリに書き込み権があることと、cgroup名前空間内にいることを確認します。その後、pids.maxファイルに制限値を設定してみます。

# ls -l /sys/fs/cgroup/test01/pids.max
(pids.maxファイルに書き込み権があることを確認)
-rw-r--r-- 1 root nogroup 0 11月 30日  19:51 /sys/fs/cgroup/test01/pids.max
# echo 50 > /sys/fs/cgroup/test01/pids.max (制限値の書き込み)
# cat /sys/fs/cgroup/test01/pids.max 
50
(書き込めたことを確認)

無事にファイルに制限値を書き込めたことが確認できました。アクセス権から考えて予想できる動きではないかと思います。

nsdelegateオプションを指定した場合

それではnsdelegateオプションを指定してcgroup v2をマウントして試してみましょう。

マウント後、先の例と同じように、子cgroupでiopidsmemoryコントローラが使えるようにします。

$ sudo mount -t cgroup2 -o nsdelegate cgroup2 /sys/fs/cgroup/
(マウント)
$ echo "+io +pids +memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
(子cgroupでio, pids, memoryコントローラが使えるように設定)
+io +pids +memory
$ cat /sys/fs/cgroup/cgroup.subtree_control
io memory pids (設定された)

そして、この後も先の例と同じように、子cgroupとして"test01" cgroupを作成し、ディレクトリ内のファイルの所有権を一般ユーザである"gihyo"ユーザにします。

$ sudo mkdir /sys/fs/cgroup/test01 (cgroup作成)
$ sudo chown gihyo /sys/fs/cgroup/test01
(ディレクトリ所有権をgihyoに)
$ sudo chown gihyo /sys/fs/cgroup/test01/*
(ディレクトリ内のファイル所有権をgihyoに)

ここでも、先のnsdelegateオプションを指定しないときの例と同様に、"test01" cgroup内のすべてのファイルにアクセス権を与えました。そしてカレントシェルを登録します。

$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 
(カレントシェルをtest01に登録)
4399

次に、先の例と同じくユーザ名前空間とcgroup名前空間を作成します。

$ unshare --user --cgroup --map-root-user /bin/bash
(cgroup名前空間を作成しbashを実行)
# cat /proc/self/cgroup 
0::/ (cgroup名前空間内でrootにいることを確認)

所有権を設定していますので"test01"直下に存在するpid.maxファイルには書き込み権があります。

# ls -l /sys/fs/cgroup/test01/pids.max 
(pids.maxファイルに書き込み権があることを確認)
-rw-r--r-- 1 root nogroup 0 11月 30日  20:02 /sys/fs/cgroup/test01/pids.max
# echo 50 > /sys/fs/cgroup/test01/pids.max 
-bash: echo: write error: Operation not permitted
(書き込めない)

上の例のように、cgroup名前空間内のrootにあたるcgroup内に存在するコントローラ用のファイルに書き込もうとするとエラーになりますEPERMが返ります⁠⁠。

cgroup v2コアで使用する4.14カーネルで追加されたファイル

第38回でcgroup v2コアで使用するファイルについて紹介しました。いずれもcgroup.で始まるファイルでした。

その後、cgroup v2の開発が進むとともに、第38回で紹介したファイル以外に追加されたファイルがありますので、それらを今回紹介しましょう。

いずれも4.14カーネルで追加されました。

表1 cgroup v2コアで使用するファイル(追加分)
ファイル名 説明 可能な操作 デフォルト値
cgroup.type そのcgroupの現在のタイプ。root以外に存在 読み書き -
cgroup.threads そのcgroupに属するスレッドの登録、確認 読み書き -
cgroup.max.depth そのcgroupが持てる子孫の深さ 読み書き max
cgroup.max.descendants そのcgroupが持てる子孫の最大数 読み書き max
cgroup.stat cgroupのステータス 読み込み -

表1が4.14カーネルで追加されたファイルです。実際にファイルが出現していることを確認しましょう。4.14.44カーネルで試しています。 

$ uname -r
4.14.44-plamo64
$ sudo mount -t cgroup2 cgroup2 /sys/fs/cgroup/
$ ls /sys/fs/cgroup/ (cgroup rootにあるファイルを確認)
cgroup.controllers      cgroup.procs            cgroup.threads
cgroup.max.depth        cgroup.stat
cgroup.max.descendants  cgroup.subtree_control
(cgroup.threads, cgroup.max.depth, cgroup.max.descendants, cgroup.statが存在)
$ sudo mkdir /sys/fs/cgroup/test01
$ ls /sys/fs/cgroup/test01
cgroup.controllers  cgroup.max.descendants  cgroup.subtree_control  io.max
cgroup.events       cgroup.procs            cgroup.threads          io.stat
cgroup.max.depth    cgroup.stat             cgroup.type
(cgroup rootでなければcgroup.typeファイルが存在)

それでは、これらの追加されたファイルそれぞれについて説明していきましょう。

スレッドモード

4.14カーネルでは、cgroup v2に大きな変更が加えられました。これは、スレッド単位でcgroupを制御する、スレッドモードというモードです。

表1で紹介した、4.14で追加されたファイルのうち、次の2つのファイルがこのスレッドモードに関係します。

  • cgroup.type
  • cgroup.threads

cgroup.typeファイルは、cgroupがプロセスを扱うのか、スレッドを扱うのかなど、そのcgroupが持つ機能を表す文字列が書かれます。

cgroup.threadsファイルは、cgroup.procsファイルのスレッド版で、スレッドをcgroupに追加・削除する際、cgroupに属するスレッドを確認する際に使うファイルです。cgroup v1に存在したtasksファイルと同様のファイルです。

スレッドモードを説明すると長くなりますので、スレッドモードの説明とこれらのファイルの使い方については、この連載の後の回で詳しく紹介したいと思います。

cgroupの子孫の制御

4.14カーネルでは、スレッドモードの他に、cgroupの子孫を制御する機能が追加されました。

これは、cgroupの階層が深くなったり子孫数が多くなると、パフォーマンスに影響を与える可能性があるためです。特に権限委譲を行うと、ホストの管理者が知らないところでシステム上に存在するcgroupの数が増えてしまう可能性がありますので、そのような場合に有効な機能でしょう。

cgroup.max.depth

cgroup.max.depthファイルは、cgroupの子孫の深さを制限します。デフォルト値はmaxであり、制限がないことを表します。実際に試しながら確認しましょう。

"test1" cgroupを作成し、cgroup.max.depthファイルに2を書き込み、深さを2に制限します。つまり"test1"から見ると孫cgroupまでは許されます。しかし、ひ孫を作る所でエラーになるはずです。

$ sudo mkdir /sys/fs/cgroup/test1 (cgroupの作成)
$ echo 2 | sudo tee /sys/fs/cgroup/test1/cgroup.max.depth 
(子孫の深さを2に制限)
2
$ cat /sys/fs/cgroup/test1/cgroup.max.depth (設定を確認)
2

制限を2に設定しました。設定が確認できましたので、子孫のcgroupを作成して、設定したとおりに制限されているかを確認しましょう。

$ sudo mkdir /sys/fs/cgroup/test1/test2
(子cgroupを作成)
$ sudo mkdir /sys/fs/cgroup/test1/test2/test3
(孫cgroupを作成)
$ sudo mkdir /sys/fs/cgroup/test1/test2/test3/test4
mkdir: cannot create directory ‘/sys/fs/cgroup/test1/test2/test3/test4’: Resource temporarily unavailable
(ひ孫cgroupを作成しようとするとエラーになる)

"test1" cgroupの子として"test2"、孫として"test3"までは作成できました。しかし、さらにひ孫として"test4"を作成しようとしたところでエラーになりました。

cgroup.max.depthの制限にひっかかった場合は、エラーとしてEAGAINが返ります。

ここで"test2"、"test3" cgroup内のcgroup.max.depthファイルはどうなっているのか確認してみましょう。

$ cat /sys/fs/cgroup/test1/test2/cgroup.max.depth 
max
$ cat /sys/fs/cgroup/test1/test2/test3/cgroup.max.depth 
max
(制限値を書き込んでいないcgroupではmaxのまま)

このように特に設定していないcgroupの制限は"max"となっています。この場合でも上位の"test1"で設定した制限が効いていますので、上位で設定した制限にひとつでもひっかかるとエラーになることがわかります。

図3 cgroup.max.depth
図3 cgroup.max.depth

cgroup.max.descendants

cgroup.max.descendantsファイルは、cgroupが持てる子孫の数を制限します。デフォルト値はcgroup.max.depthと同じく"max"であり、制限がないことを表します。試してみましょう。

"test1"のcgroup.max.descendantsファイルに2を書き込み、子孫数を2に制限します。

$ sudo mkdir /sys/fs/cgroup/test1 (cgroupの作成)
$ echo 2 | sudo tee /sys/fs/cgroup/test1/cgroup.max.descendants 
(子孫の数を2に制限)
2
$ cat /sys/fs/cgroup/test1/cgroup.max.descendants (設定を確認)
2

制限を2に設定しました。設定が確認できましたので、子cgroupを作成して、設定したとおりに制限されているか確認しましょう。

$ sudo mkdir /sys/fs/cgroup/test1/test2
(ひとつめの子cgroupを作成)
$ sudo mkdir /sys/fs/cgroup/test1/test3
(ふたつめの子cgroupを作成)
$ sudo mkdir /sys/fs/cgroup/test1/test4
mkdir: cannot create directory '/sys/fs/cgroup/test1/test4': Resource temporarily unavailable
(3つめの子cgroupを作成しようとするとエラーになる)

3つめのcgroupを作るところでエラーになりました。cgroup.max.depthと同様にEAGAINが返ります。

図4 cgroup.max.descendants
図4 cgroup.max.descendants

cgroup.stat

cgroupの統計情報が含まれているファイルで、読み取り専用です。現時点ではcgroupの子孫情報のみ含まれています。

nr_descendants
利用できる子孫cgroupの数
nr_dying_descendants
消滅途中の子孫cgroupの数

「消滅途中」とは、ユーザがcgroupを削除しても、システム負荷に応じてcgroupが消滅するまでに時間がかかるので、子孫のうち、その削除途中の状態にあるcgroup数がカウントされます。

先のcgroup.max.descendantsの例で、2つ子cgroupを作成したあとのcgroup.statファイルを確認してみましょう。

$ cat /sys/fs/cgroup/test1/cgroup.stat 
nr_descendants 2
nr_dying_descendants 0

子cgroupをふたつ作成したので、nr_descendantsが2となっています。

まとめ

今回はcgroup v2に追加された新機能を紹介しました。

nsdelegateマウントオプションは、cgroup名前空間と連携して、自動的に権限委譲の境界を定義する機能です。この機能により、名前空間をまたいでプロセスは移動できません。そして、名前空間のrootではコントローラのリソース制限値を変更できません。

そして、これまでは無限に子孫を増やせたcgroupに、子孫の数を制限する機能が追加されました。システム上のcgroupの数や階層が増えてパフォーマンスに影響を与えるのを防げます。

次回からは、前回書いたとおりにudzuraさんにCRIUについて書いていただく予定です。先日開催されたJapanContainerDays v18.12で、udzuraさんのCRIUに関するセッションを聞きました。非常に面白い内容でしたので、今後の記事が楽しみです。

おすすめ記事

記事・ニュース一覧