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

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

Ubuntuの次の長期サポート版のリリースが近づいています。それに合わせて、LXC関連のプロダクトもメジャーバージョンアップし、新しい長期サポート版をリリースしました。筆者はlinuxcontainers.orgやLXC/LXD関連の翻訳をやっていますので、一気に翻訳対象が増えて大変です。一度には出来ないので毎日少しずつ進めています。

さて、前回は、cgroup v2で使うファイルや、基本操作、親cgroupから子cgroupで使うコントローラを指定する方法を紹介しました。

今回も引き続き、cgroup v2が持つ基本的な機能を紹介していきましょう。

まずは、cgroup v2の特徴のひとつである、プロセスが末端のcgroupにしか所属できないことを紹介します。そのあと、cgroupが持つ状態通知機能について紹介します。この機能については、これまで連載で紹介していなかったv1の機能を含めて紹介します。

なお、この連載ではこれまでcgroupで各種のリソース制限を行う仕組みを「サブシステム」と呼んでいました。しかし、cgroup v2の文書では "Controller" が使われており、最近は「コントローラ」と呼ばれることが多いので、今回から「コントローラ」に統一しました。呼び方だけの話です。

今回の実行例は、cgroup v1についてはUbuntu 16.04LTS上で、cgroup v2については自由にcgroupがマウントできるPlamo Linux 6.2(カーネル4.11.12)上で実行しています。

子cgroupを持つ場合の制約

第37回で紹介したように、cgroup v1が持つ問題点のひとつであった「子cgroupのタスクと親cgroupの内部タスクの競合」問題を解決するために、リソース分配を行う場合は末端のcgroupにしかプロセスが所属できなくなりました。

この動きを紹介しましょう。

プロセスが所属した状態で子cgroupで使うコントローラを登録しようとした場合の動き

図1 root cgroupに子cgroupで使うコントローラを登録
図1 root cgroupに子cgroupで使うコントローラを登録

まずはroot cgroupで、子cgroupで使うコントローラを登録します図1⁠。

# echo "+io +memory +pids" > /sys/fs/cgroup/cgroup.subtree_control(子cgroupで使えるコントローラの登録)

上のようにio、memory、pidsコントローラを登録しました。続いてcgroupを作成します。

# mkdir /sys/fs/cgroup/test01(test01 cgroupの作成)
# cat /sys/fs/cgroup/test01/cgroup.controllers(test01で使えるコントローラの確認)
io memory pids

test01 cgroupを作成すると、先ほど登録したio、memory、pidsコントローラが使えるようになっています。

図2 子cgroupを作成しプロセスを追加
図2 子cgroupを作成しプロセスを追加

このtest01にプロセスを登録してみましょう図2⁠。

# echo $$ > /sys/fs/cgroup/test01/cgroup.procs(プロセスの追加)
# cat /sys/fs/cgroup/test01/cgroup.procs(追加したプロセスの確認)
3129
3139

無事に登録できましたね。ここまではこれまで紹介してきたcgroup v2で行う操作と同じです。

この状態で、test01の子cgroupで使うコントローラをcgroup.subtree_controlファイルに登録してみましょう。

# echo "+io +memory +pids" > /sys/fs/cgroup/test01/cgroup.subtree_control 
bash: echo: write error: Device or resource busy

エラーになりました図3⁠。

図3 プロセスが登録されているcgroupに子で使うコントローラは登録できない
図3 プロセスが登録されているので子cgroupで使うコントローラを登録しようとするとエラー

このように、自身にプロセスが所属している状態で、子cgroupでコントローラが使えるように設定しようとしてもエラーになります。

それでは、test01にプロセスが所属しない状態にして、再度コントローラを登録してみます。

# echo $$ > /sys/fs/cgroup/cgroup.procs(プロセスをrootに登録しtest01から削除)
# cat /sys/fs/cgroup/test01/cgroup.procs(test01にはプロセスがない状態)

test01に登録したプロセスをroot cgroupに移動させました図4⁠。

図4 
図4 cgroupからプロセスをroot cgroupに移動し削除

この状態で先ほどと同様にtest01に、子cgroupで使うコントローラを登録してみましょう。

# echo "+io +memory +pids" > /sys/fs/cgroup/test01/cgroup.subtree_control(登録してもエラーはでない)
# cat /sys/fs/cgroup/test01/cgroup.subtree_control(コントローラは登録されている)
io memory pids

無事、cgroup.subtree_controlファイルにコントローラが登録できました図5⁠。

図5 プロセスを削除したので子で使うコントローラが登録できた
図5 プロセスが所属していないcgroupに子で使うコントローラを登録

ここまでで、cgroupにプロセスが登録された状態では、子cgroupで使うコントローラを登録できないことが確認できました。

子cgroupで使えるコントローラを登録した状態でプロセスを登録しようとした場合の動き

次は、子cgroupでコントローラが使える状態では、そのcgroupにプロセスを登録できないことを確認してみましょう。

図6 子で使うコントローラが登録された状態で子cgroupを作成
図6 さらに子cgroupを作成

先ほどの例で作成したtest01に子cgroupを作ってみましょう。test01cgroup.subtree_controlファイルには、子cgroupで使えるコントローラが登録された状態のままです。

# mkdir /sys/fs/cgroup/test01/test02(test02の作成)
# cat /sys/fs/cgroup/test01/test02/cgroup.controllers
(test02ではtest01で登録したコントローラが使えるようになっている)
io memory pids

test02という名前のcgroupを作りました。test02で使えるコントローラを確認すると、test01cgroup.subtree_controlで設定したコントローラが表示されています図6⁠。

つまりtest01 cgroupは子孫にリソースを分配している状態です。

この状態で、test01にプロセスを登録してみます。

# echo $$ > /sys/fs/cgroup/test01/cgroup.procs
bash: echo: write error: Device or resource busy

プロセスを登録しようとすると、上のようにエラーとなります図7⁠。test01は子孫にリソースを分配しているからです。

図7 リソースを分配しているcgroupにはプロセスを登録できない
図7 リソースを分配しているcgroupにはプロセスを登録できない

ここまでの実行例で、次のような制約があることがわかりました。

  • 自身にプロセスが登録された状態で、子cgroupに対してコントローラが使えるように設定しようとするとエラーになる
  • 子cgroupに対してコントローラが使えるように設定した状態で、自身にプロセスを登録しようとするとエラーになる

第37回で紹介したとおり、コントローラを使ってリソース制限をする場合は、ツリー末端のcgroupにしかプロセスが登録できないようになっていることが確認できました。

子cgroupで使えるコントローラを登録していない場合の動き

それでは、子孫でリソース制限ができるように設定していない状態cgroup.subtree_controlにコントローラが登録されていない状態)で、ツリー末端以外のcgroupにプロセスを所属させようとしたらどうなるでしょう?

確認してみましょう。

# cat /sys/fs/cgroup/test01/cgroup.subtree_control(test01では子孫用にコントローラ登録なし)

このように、test01ではcgroup.subtree_controlは空で、子孫ではリソース制限はできない状態になっています。この状態でtest01の子cgroup test02にプロセスを登録します。

# echo 3129 > /sys/fs/cgroup/test01/test02/cgroup.procs(test01/test02にプロセスを登録)
# cat /sys/fs/cgroup/test01/test02/cgroup.procs(test02に登録されている)
3129

子cgroupであるtest02にプロセスが登録された状態で、その親であるtest01にプロセスを登録してみましょう。

# echo 3108 > /sys/fs/cgroup/test01/cgroup.procs(test01にもプロセスを登録)
# cat /sys/fs/cgroup/test01/cgroup.procs(エラーなく登録されている)
3108

上の実行例のように、特に制約を受けることなく、プロセスが登録できました図8⁠。つまり、子孫でコントローラが使えるように設定していない状態では、ツリー末端以外のcgroupにもプロセスが所属できます。

図8 子cgroupを持つがリソース分配をしていない場合はプロセスが所属可能
図8 子cgroupを持つがリソース分配をしていない場合はプロセスが所属可能

まとめると、

  • 自身にプロセスが所属していないときだけ、子孫にリソースを分配できる(子でコントローラが使えるように設定できる)
  • コントローラによるリソース制御を行わない場合は、ツリー末端以外の任意のcgroupにもプロセスが所属できる

という制約がcgroup v2には存在します。ただし、root cgroupにはデフォルトですべてのプロセスが所属しますし、rootでリソース制限をすることはありませんので制約はありません。つまり、

  • root cgroupには以上の制約はない

となります。

cgroup状態通知

cgroupで、コントローラを使ってリソース制限を行う以外に、プロセスを管理するためにcgroupを使う場合があります。最近のLinuxでは、systemdが広く使われていますので、どちらかというとプロセスの管理に使われることのほうが一般的ですね。

プロセスの管理を行う場合、cgroup内にタスクやプロセスがなくなったことを知ることができれば便利です。このような場合に使える通知を行う仕組みがcgroup v1にもv2にも存在します。

cgroup v1、v2それぞれで試してみましょう。

cgroup v1での状態通知

cgroup v1では、あるcgroupにタスクが存在しなくなった場合に、あらかじめ登録してあるプログラムを実行できます。

表1 cgroup v1の状態通知に関係するファイル
ファイル名 notify_on_release release_agent
説明 cgroupにタスクが存在しなくなった場合にrelease_agentに登録したプログラムを実行するかどうか cgroupにタスクが存在しなくなった場合に実行するプログラム
0 or 1 プログラムのフルパス
デフォルト値 親cgroupの値を引き継ぐ。rootでは0 なし
ファイルの場所 全cgroup root cgroup

cgroupにタスクが存在しなくなったときに、release_agentに登録したプログラムを実行する場合には、notify_on_releaseファイルに"1"を書き込みます。デフォルトは上位cgroupの値がコピーされます。

# cat /sys/fs/cgroup/pids/notify_on_release
0 (root cgroupでは0が設定されている)
# mkdir /sys/fs/cgroup/pids/test01 (test01を作成)
# cat /sys/fs/cgroup/pids/test01/notify_on_release
0 (cgroup作成直後は親cgroupの値を継承)

root cgroupでは"0"がデフォルト値です。このまま子cgroup "test01"を作成したところ、そのまま親であるroot cgroupの値を引き継ぎ、"test01"のnotify_on_releaseの値も"0"に設定されました。

root cgroupの値を"1"に変更してみましょう。

# echo 1 > /sys/fs/cgroup/pids/notify_on_release
(root cgroupのnotify_on_releaseを1に変更)
# mkdir /sys/fs/cgroup/pids/test02 (test02を作成)
# cat /sys/fs/cgroup/pids/test02/notify_on_release
1 (親の値を継承している)
# cat /sys/fs/cgroup/pids/test01/notify_on_release
0 (親の値が変わっても既存の子孫には反映されない)

"1"に変更したあとに、新たに子cgroup "test02"を作ると、"test02"のnotify_on_releaseは"1"になりました。ただし、このとき、先に作成した"test01"のnotify_on_releaseの値は変わっていません。あくまで作成時に親の値を参照して設定するだけです。

それではrelease_agentに登録するプログラムを準備しましょう。このプログラムが実行される場合は、引数としてroot cgroupからの相対パスが渡されます。次のようなシェルスクリプトを準備しました。

#!/bin/sh
rmdir /sys/fs/cgroup/pids/$1
logger "cgroup $1 is removed."

cgroupにタスクが存在しなくなった通知を受け取った場合には、そのcgroupを削除し、syslogにログを出力します。

それでは試してみましょう。

さきほどのシェルスクリプトを/usr/local/bin/rmcg.shに置き、実行権限をつけました。このスクリプトをrelease_agentに登録します。

# echo "/usr/local/bin/rmcg.sh" > /sys/fs/cgroup/pids/release_agent
(release_agentにプログラムを登録)
/usr/local/bin/rmcg.sh
# cat /sys/fs/cgroup/pids/release_agent 
(登録された)
/usr/local/bin/rmcg.sh

登録されたので、さきほど作成したnotify_on_releaseが有効な"test02"にプロセスを登録して、すぐに削除してみましょう。

# echo $$ > /sys/fs/cgroup/pids/test02/tasks 
(test02 cgroupにプロセスを登録)
# grep $$ /sys/fs/cgroup/pids/test02/tasks 
1729 (登録された)
# echo $$ > /sys/fs/cgroup/pids/tasks 
(test02 cgroupからプロセスを削除=root cgroupに移動)

では"test02" cgroupがどうなったか確認してみましょう。

# ls -ld /sys/fs/cgroup/pids/test02
ls: cannot access '/sys/fs/cgroup/pids/test02': No such file or directory
(test02 cgroupが存在しない)
# tail -n1 /var/log/syslog 
Mar 19 20:35:35 xenial root: cgroup /test02 is removed.
(loggerコマンドによりsyslogにログが書き込まれている)

"test02" cgroupにタスクが存在しなくなったので、release_agentが呼び出され、"test02" cgroupが削除され、syslog出力されています。

同じ操作を"test01"に対して行っても、"test01"は"notify_on_release`が"0"でしたので、何も起こりません。

# cat /sys/fs/cgroup/pids/test01/notify_on_release 
0 (test01のnotify_on_releaseは0)
# echo $$ > /sys/fs/cgroup/pids/test01/tasks
(test01にプロセスを登録)
# echo $$ > /sys/fs/cgroup/pids/tasks 
(test01からプロセスを削除)
# ls -ld /sys/fs/cgroup/pids/test01
drwxr-xr-x 2 root root 0 Mar 19 20:25 /sys/fs/cgroup/pids/test01
(test01は消去されていない)

実際には、cgroupごとにnotify_on_releaseの値を変えて、処理を変えることはないかもしれませんが、このようにcgroupごとにタスクがなくなったときの処理の有無を設定できます。実行するプログラムはroot cgroupのrelease_agentに登録するだけですので変えられません。

cgroup v1が持つ通知は以上のような仕組みです。v1では、cgroupにタスクがいなくなるたびにプロセスを起動しますのでコストが高くなります。

cgroup v2

それではcgroup v2の機能を見てみましょう。

cgroup v2ではrootではないcgroupには、cgroup.eventsというファイルが存在します。このファイルの中身を見れば、自身または子孫にプロセスが所属しているかどうかがわかります。

表2 cgroup v2の状態通知に関係するファイル
ファイル名 cgroup.events
説明 cgroup内のプロセスの有無を示すパラメータを含む
プロセスが存在するとき populated 0存在しないとき populated 1
デフォルト値 populated 0
ファイルの場所 root以外

このファイルにはpopulatedという項目の行だけが存在しており、この値はプロセスが存在していれば"1"、存在していなければ"0"です。

# ls /sys/fs/cgroup/test01/ (子cgroupが存在しない)
cgroup.controllers  cgroup.events  cgroup.procs  cgroup.subtree_control
# cat /sys/fs/cgroup/test01/cgroup.procs  (プロセスが所属していない)
# cat /sys/fs/cgroup/test01/cgroup.events (populatedの値が0)
populated 0

この例では"test01"は子cgroupを持たず、プロセスも所属していないので、populatedの値は"0"です。この"test01"にプロセスを所属させて、cgroup.eventsの中身を見てみましょう。

# echo $$ > /sys/fs/cgroup/test01/cgroup.procs (プロセスを登録)
# grep $$ /sys/fs/cgroup/test01/cgroup.procs   (登録されたことを確認)
3081
# cat /sys/fs/cgroup/test01/cgroup.events (populatedの値が1になっている)
populated 1

populatedの値が"1"になりましたね。

ここで別のシェルを開いてinotifywatchコマンドでcgroup.eventsファイルを監視し、今度は"test01"からプロセスを削除してみましょう。

# inotifywait -m /sys/fs/cgroup/test01/cgroup.events 
Setting up watches.
Watches established.
(ここでtest01からプロセスをrootに移動)
/sys/fs/cgroup/test01/cgroup.events MODIFY cgroup.events

"test01"からプロセスが削除されると、cgroup.eventsファイルに"MODIFY"というinotifyイベントが発生しました。このイベントは"test01"の子cgroupに対する変化でも発生します。

# mkdir /sys/fs/cgroup/test01/test02 (test01配下にtest02を作成)

# echo $$ > /sys/fs/cgroup/test01/test02/cgroup.procs
(test02にプロセスを登録)

上の例のように"test01"の下に"test02"を作成し、"test02"にプロセスを登録すると、次のように"test01"のcgroup.eventsにinotifyイベントが発生します。

# inotifywait -m /sys/fs/cgroup/test01/cgroup.events 
Setting up watches.
Watches established.
/sys/fs/cgroup/test01/cgroup.events MODIFY cgroup.events
(test01自身ではなく子のtest02の変化もtest01で知れる)

もちろん、このときtest02内のcgroup.eventsファイルにも同じイベントが発生しています。

このように、cgroup.eventsファイルに発生するイベントを監視して何らかの処理を行うことができます。cgroupを監視するプログラムがcgroup.eventsを監視し、イベントを受信した処理を行えますので、v1に比べるとコストが低くなりました。

まとめ

今回は、親子cgroup間の内部タスクの競合を解決するためにcgroup v2で導入された制約を紹介しました。

また、cgroup内のタスク状態が変化した場合の通知機能について、cgroup v1、v2の両方を実際に試しながら紹介しました。

cgroup v1が持っていて、今でも使われている機能はそのままに、v1が持っていた問題点をうまく解決していることがおわかりいただけたのではないでしょうか。

その分cgroup v2は、いろいろな制約があり複雑になったと思われるかもしれません。この複雑さは、cgroupを管理するプログラムが理解して担当すれば良いという考え方なのだと思います。

実際、現在のほとんどのディストリビューションではsystemdがcgroupをすべて管理しています。cgroup v2もsystemdの存在を念頭に置いているのだろうと思います。cgroup v2の通知機能もinotifyを使っており、常時起動しているプログラムから監視をすることが前提となっています。

今回まででcgroup v2のコントローラに関わらない基本機能で、どのようなケースでも使う機能の紹介が済みました。次回は、基本機能ですが、特権を持たないプロセスがcgroupを扱う場合の機能を紹介する予定です。

おすすめ記事

記事・ニュース一覧