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

第49回cgroup v2のリソース分配の方法とインターフェースファイルの操作

前回はcgroup v2で使えるコントローラのうち、v1から大きく変わったコントローラや、新たに実装されたコントローラについて説明しました。

今回はcgroup v2で行われるリソース制御のタイプ、cgroupで使うインターフェースファイルに関係する規則や、コントローラでリソース制限を設定する際に関係するファイルのフォーマットや書き込み方について説明したいと思います。

cgroupとcgroup内ファイルの命名規則

cgroup v1には、cgroupで使用するファイルやその内部の書式について、明確な規則はありませんでした。cgroup v2では、ファイルに関する規則がきちっと定められました。この決まりを説明していきましょう。

今回の実行例は、前回と同じくUbuntu 21.10環境で実行しています。

まず、cgroup自体の動きに影響するcgroupコアに関係するインターフェースファイルは、"cgroup."で始まります。各コントローラ用のインターフェースファイルは"(コントローラ名)."で始まります。

"(コントローラ名)."で始まる名前では、コントローラ内に複数の種類のコントローラを持っているような場合はドット.が複数出現することもあります。例えばこの後の例で使うioコントローラのうち、BFQというIOスケジューラで使うコントローラで制限値を設定するファイル名は"io.bfq."で始まります。

あるcgroupに子cgroupを作成する際には、そのcgroupのインターフェースファイルが出現するディレクトリ内にディレクトリを作ります。この際に、インターフェースファイルと同じ名前を持つディレクトリ(cgroup)を作ることができます。

このように、問題が発生するにもかかわらず、cgroup名としてインターフェースファイルと同じ名前をつけることはないと思います。しかし、cgroupコアは作成するcgroup名に対するエラーチェックは行いませんので、作成するユーザ側で注意する必要があります。

逆にcgroupのインターフェースファイルは、cgroup名として使われそうなワークロードに関係する一般的な単語は使用しないように実装されています。

コントローラ用のインターフェースファイル名で、"(コントローラ名)"の後のドット.に続く文字列はリソースを分配する方法によって決まっています。この決まりについてはこの後、リソース分配を行う方法を紹介するところで一緒に紹介していきます。

コントローラがリソース分配を行う方法

cgroup v1では、各種コントローラで色々なリソース分配が行われていました。しかし、そこでリソースを分配する方法としてどのようなモデルを使うか、設定する値としてどのような値を使うかについては、明確な基準はありませんでした。

cgroup v2では、リソースを分配するモデルやそこで設定する値について基準が示されましたので、それに従ってコントローラが実装されています。

ウェイト(Weights)

ウェイトは第4回で説明したCPUコントローラの相対配分として紹介した機能で使われているようなリソース配分です。

子cgroupで指定されている数値をすべて足して、合計に対してそれぞれに割り当てた値の割合で分配されます。

例えば、cgroup AとBがあり、それぞれに100と50が設定されているとすると、Aには2/3、Bには1/3のリソースが分配され、AにはBに割り当てる2倍のリソースが分配されます。その時点で使えるリソースをすべて割合に従い、それぞれに分配します。

ウェイトとして設定できる値は1〜10000の間で、デフォルト値は100と決まっています。

ウェイトでリソース分配を行う場合の、コントローラ用のインターフェースファイル名は"(コントローラ名).weight"です。このようなファイルの例としてcpu.weightがあります。

cpu.weightを使って、ウェイトタイプの制限値を設定する様子を見てみましょう。

まず、作成するcgroupでCPUコントローラが使えるようにします。Ubuntu 21.10環境では、デフォルトでメモリとpidsコントローラは使えるように設定されていますので、加えてCPUコントローラを使えるようにしておきます。ここの実行例での操作については第38回をご覧ください。

$ echo "+cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control 
(子cgroupでCPUコントローラが使えるようにする)
+cpu
$ cat /sys/fs/cgroup/cgroup.subtree_control 
(cpuという文字列が書き込まれた)
cpu memory pids
$ sudo mkdir /sys/fs/cgroup/test
(root直下に"test" cgroupを作成)
$ cat /sys/fs/cgroup/test/cgroup.controllers
cpu memory pids ("test" cgroupでcpuコントローラが使えることを確認)

さて、test cgroupが作成できましたので、cpu.weightファイルを見てみましょう。

$ ls /sys/fs/cgroup/test/cpu.weight
/sys/fs/cgroup/test/cpu.weight (ファイルが存在する)
$ cat /sys/fs/cgroup/test/cpu.weight
100 (cgroup作成直後のデフォルト値は"100")

cgroup作成直後で、何も操作をしていない状態でcpu.weightファイルを確認すると、確かにデフォルト値である100という数値が書かれています。実際に操作をしてみましょう。

$ echo "10000" | sudo tee /sys/fs/cgroup/test/cpu.weight
10000
$ cat /sys/fs/cgroup/test/cpu.weight
10000
$ echo "1" | sudo tee /sys/fs/cgroup/test/cpu.weight
1
$ cat /sys/fs/cgroup/test/cpu.weight
1
$ echo "100000" | sudo tee /sys/fs/cgroup/test/cpu.weight
100000 (上限値以上の数値を書き込むとエラーになる)
tee: /sys/fs/cgroup/test/cpu.weight: Numerical result out of range

範囲内の110000を書き込むと正常に反映され、範囲外の100000を書くとエラーになることが確認できました。

制限(Limits)

制限(Limits)は文字通りリソース制限です。設定した上限を超えてリソースを使うことはできません。この制限値はオーバーコミットできます。つまり、子cgroupに割り当てる制限値の合計が、親が利用できるリソース量を超えるような設定ができます。

この制限は、絶対的な制限となるハードリミットとして実装されていることもありますし、ベストエフォートで動作するソフトリミットとして実装されていることもあります。両方とも実装されている場合もあります。

制限として、0以上の数値と"max"という文字列が指定できます。"max"は制限しないという意味になり、デフォルト値は"max"です。

制限でリソース制限を行う場合の、コントローラ用のインターフェースファイル名は、"(コントローラ名)."に続けて次のような文字列が付いた名前になります。

  • ハードリミットとして絶対的な割り当て制限を行う場合:max(例:memory.max
  • ソフトリミットとしてベストエフォートのリソース制限を行う場合:high(例:memory.high

ここで例としてあげたファイル名はmemory.maxmemory.highで、両方ともメモリコントローラです。つまりメモリコントローラはハードリミットとソフトリミットの両方が実装されているということです。

cgroupを作成して、memory.maxmemory.highファイルを見てみましょう。

$ sudo mkdir /sys/fs/cgroup/test2 (cgroupを作成)
$ cat /sys/fs/cgroup/test2/memory.max 
max (デフォルト値はmax)
$ cat /sys/fs/cgroup/test2/memory.high
max (デフォルト値はmax)

いずれも作成直後のデフォルト値はmaxとなっており、制限がかかっていない状態になっています。ファイルを書き換えてみましょう。

$ echo "128M" | sudo tee /sys/fs/cgroup/test2/memory.max
128M (128MBに制限)
$ cat /sys/fs/cgroup/test2/memory.max 
134217728 (制限値が書き込まれた)
$ echo "max" | sudo tee /sys/fs/cgroup/test2/memory.max
max (制限を外すためにmaxという文字列を書き込んだ)
$ cat /sys/fs/cgroup/test2/memory.max 
max (制限値がmaxに書き換わった)

まず"128M"という文字列を書き込み制限値を設定した後に、制限値を外すために"max"という文字列を書き込むと、memory.maxファイルの中身がmaxと書き換わりました。

保護(Protections)

保護(Protections)は、cgroupに所属するプロセスに対して、設定した量までのリソースを与えることを保証します。ただし、ツリー構造のすべての祖先のcgroupが保護レベル以下である場合だけです。

制限と同様に保護もオーバーコミットでき、オーバーコミットした場合は、親が使える量までしか子では保護されません。

また、強い保護とベストエフォートな保護のどちらの保護にすることもできるのも制限と同様です。両方とも実装されている場合もあります。

保護として、0以上の数値maxという文字列が設定できます。デフォルト値は0です。つまりデフォルトでは保護されるリソース値は設定されていません。

保護でリソース分配を行う場合の、コントローラ用のインターフェースファイル名は、"(コントローラ名)."に続けて次のような文字列が付いた名前になります。

  • リソースの絶対的な割り当て保証保護を行う場合:min(例:memory.min
  • リソースのベストエフォートのリソース割り当て保証保護を行う場合:low(例:memory.low

例としてあげたメモリコントローラは、制限と同様に絶対的な保護とベストエフォートの保護の両方が実装されています。

ここでもmemory.minmemory.lowを使って簡単に動きを見ておきましょう。cgroupを作成し、デフォルト値を確認します。

$ sudo mkdir /sys/fs/cgroup/test3 (cgroupを作成)
$ cat /sys/fs/cgroup/test3/memory.min 
0 (デフォルト値は0)
$ cat /sys/fs/cgroup/test3/memory.low
0 (デフォルト値は0)

いずれもデフォルト値は0になっていることが確認できました。数値以外の値として書き込める"max"を書き込み、その後数値を設定してみましょう。

$ echo max | sudo tee /sys/fs/cgroup/test3/memory.min 
(memory.minにmaxという文字列を書き込む)
max
$ cat /sys/fs/cgroup/test3/memory.min 
max (maxに書き換わっている)
$ echo 128M | sudo tee /sys/fs/cgroup/test3/memory.min
128M (制限値を128MBに設定する)
$ cat /sys/fs/cgroup/test3/memory.min
134217728 (制限値が書き込まれた)

このように"max"で最大値が設定できましたし、数値での制限値も設定できました。

割り当て(Allocations)

割り当て(Allocations)は、有限なリソースを独占的に割り当てます。つまりオーバーコミットできません。子に割り当てられるリソースの合計は親の割り当て量を超えられません。

割り当てとして、0以上の数値maxという文字列が設定できます。デフォルト値は0です。つまりデフォルトではリソースがない状態で、そこから有限なリソースを必要分だけ子に割り当てていくというモデルになります。

現時点ではこのモデルが実装されたコントローラはないようです。

このタイプでの制限では設定値を超えることはできませんので、ファイル名も"(コントローラ名).max"となるようです。

インターフェースファイル内のフォーマット

cgroupを制御するために使われるインターフェースファイルには、格納されているデータに応じて色々なフォーマットがあります。インターフェースファイルについてもcgroup v2ではフォーマットがいくつかあらかじめ定められており、そのうちのどれかのフォーマットになっています。それを紹介していきましょう。

改行で区切られた値

まずは1行に1つ値が書かれたいるタイプのファイルです。

今回、ここまでに例としてあげたインターフェースファイルは制限値を設定するためのファイルでした。制限値を1つ設定するだけでしたので、値や文字列が1つだけ設定されており最後は改行されていました。このようなファイルもこのタイプに入ります。

1つ以上の値が書かれる場合は改行で区切られて1行に1つ値が入ります。例えば、cgroupを操作する際の基本的な操作である、プロセスをcgroupに所属させたり、所属しているプロセスを取得するために使われるcgroup.procsファイルです。

このタイプのファイルは、一度に1つの値しか書き込めません。

$ cat /sys/fs/cgroup/test3/memory.max 
max (値が1つだけ書かれたファイル)
$ cat /sys/fs/cgroup/user.slice/user-1000.slice/session-1.scope/cgroup.procs 
(複数の値が1行に1つ書かれたファイル)
740
768
769
1125

スペース区切りの複数の値

1 行に複数の値が書かれる場合があります。例えばcgroup.subtree_controlcgroup.controllersのようなファイルです。

このタイプのファイルは、読み込み専用のファイルであるか、もしくは一度に複数の値を書き込めるファイルのどちらかです。

$ cat /sys/fs/cgroup/test3/cgroup.controllers 
memory pids
$ cat /sys/fs/cgroup/cgroup.subtree_control 
memory pids

キーと値

設定項目とそれに対する値を設定するようなケースです。例えば "key_a" という設定項目に対する値が "val_a" である場合、

key_a val_a

のようになります。1 つのファイルに複数、設定したり表示したりする項目がある場合は、この組が複数行になります。

このタイプは、統計を表示するようなファイルに良くあります。例えば、cpu.statmemory.statのようなファイルです。

$ sudo mkdir /sys/fs/cgroup/test3 (cgroupの作成)
$ echo "+cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control 
(子cgroupでcpuコントローラを使えるようにする)
+cpu
$ echo $$ | sudo tee /sys/fs/cgroup/test3/cgroup.procs 
(プロセスを所属させる)
794
$ cat /sys/fs/cgroup/test3/cpu.stat 
usage_usec 3272
user_usec 0
system_usec 3272
nr_periods 0
nr_throttled 0
throttled_usec 0
(cgroupのCPUの統計情報がキーと値のペアで1行に1つずつ表示される)

書き込みを行うファイルの例としては、ioコントローラ用でウェイトを設定するためファイルがこのタイプに相当します。ioコントローラーはデバイスごとに制限が設定できるためです。

このタイプのファイルに値を書き込むには、キーと値のペアをスペース区切りで渡します。

$ echo "+io" | sudo tee /sys/fs/cgroup/cgroup.subtree_control 
(子cgruopでioコントローラを使えるようにする)
+io
$ cat /sys/fs/cgroup/test3/io.weight 
(io.weightの設定を表示)
default 100
$ echo "default 200" | sudo tee /sys/fs/cgroup/test3/io.weight 
(io.weightにキーと値のペアを書き込み)
default 200
$ cat /sys/fs/cgroup/test3/io.weight 
(値が変わった)
default 200

上記の例では、元々キーdefaultに対する値が100という設定がされていたところに、default 200という文字列を書き込むことでdefaultに対する値が変更されていることがわかります。

ネストしたキー

キーに対する値が1つだけの場合は、前で説明したようなファイルになります。一方で、1つのキーに対してさらにキーと値を複数持つ場合があります。この場合は次のような構造になります。

KEY0 SUB_KEY0=VAL00 SUB_KEY1=VAL01
KEY0 SUB_KEY0=VAL10 SUB_KEY1=VAL11

例えば、IOに関する統計情報を見るためのファイルio.statは次のようになっています。

$ cat /sys/fs/cgroup/test3/io.stat 
252:0 rbytes=184320 wbytes=0 rios=5 wios=0 dbytes=0 dios=0
253:0 rbytes=184320 wbytes=0 rios=5 wios=0 dbytes=0 dios=0

これは253:0というデバイスに対するrbyteswbytesrioswiosdbytesdiosという複数の項目名とそれに対する値を表示しています。見たとおり読み込み、書き込みのバイト数やIO数が表示されています。dで始まるのは破棄(discard)されたバイト数、IO数です。

書き込む際もファイルを読んだ場合に表示されたのと同様のフォーマットで書き込みます。例えばIOの制限値を設定するio.maxファイルには次のように書き込みます。

$ cat /sys/fs/cgroup/test3/io.max 
(test3 cgroupのio.maxは何も設定されていない)
$ echo "253:0 riops=200 wiops=200" | sudo tee /sys/fs/cgroup/test3/io.max 
(読み書きのIOPSを200に制限する)
253:0 riops=200 wiops=200
$ cat /sys/fs/cgroup/test3/io.max 
253:0 rbps=max wbps=max riops=200 wiops=200
(読み書きのIOPSが200に設定されている)

上の例では、253:0というデバイスに対する読み書きのIOPSriopswiops200に設定しています。明示的に設定していない読み書きバイト数はデフォルトである制限なしmaxに設定されています。

ちなみに253:0はデバイスに対する「メジャー番号:マイナー番号」で、例で使っている環境では/dev/vdaです。

$ ls -l /dev/vda
brw-rw---- 1 root disk 252, 0 Mar 11 12:57 /dev/vda

上の例で設定した値を削除したい場合は、デフォルトのmaxに設定すれば削除されます。

$ echo "253:0 riops=max wiops=max" | sudo tee /sys/fs/cgroup/test3/io.max 
253:0 riops=max wiops=max
(制限を削除するためにデフォルトのmaxを設定する)
$ cat /sys/fs/cgroup/test3/io.max 
$
(設定の行自体が削除され、何も設定されていない状態に戻った)

デフォルト値の変更

ここまでで説明したインターフェースファイルのうち、キーと値の組を設定するような設定項目では、デフォルト値が変更できる場合があります。先の例だとio.weightの場合がそのようなケースに該当します。

このデフォルト値の設定についても紹介しておきましょう。

先の例ではio.weightを紹介しましたが、ここではio.bfq.weightファイルを使います[1]⁠。

io.bfq.weightファイルを使うためには、少し準備が必要です。cgroupは、先の例で使用したtest3 cgroupを引き続き使います。Ubuntu 21.10環境では、ここまでの操作ではio.bfq.weightはcgroup内にはありません。bfqモジュールをロードしましょう。

$ sudo modprobe -v bfq
(bfqモジュールをロードする)
insmod /lib/modules/5.13.0-35-generic/kernel/block/bfq.ko
$ ls /sys/fs/cgroup/test3/io.bfq*
/sys/fs/cgroup/test3/io.bfq.weight
(io.bfq.weightファイルが出現)

bfqモジュールをロードしたので、io.bfq.weightファイルが出現しました。

さらに準備を続けます。ここで例として使用している筆者の環境は、ブロックデバイスとして/dev/vda/dev/vdbがあります。この両方のIOスケジューラがどうなっているかを確認します。

$ cat /sys/block/vda/queue/scheduler 
[mq-deadline] bfq none
$ cat /sys/block/vdb/queue/scheduler 
[mq-deadline] bfq none
(/dev/vda,vdbそれぞれのスケジューラの確認)

上記のようにmq-deadlineが選択されています(ブラケットで囲まれている文字列が選択されていることを示します⁠⁠。これをbfqを使うように設定してみます。

$ echo bfq | sudo tee /sys/block/vda/queue/scheduler
bfq
(IOスケジューラの変更)
$ echo bfq | sudo tee /sys/block/vdb/queue/scheduler
bfq
(IOスケジューラの変更)
$ cat /sys/block/vd?/queue/scheduler
mq-deadline [bfq] none
mq-deadline [bfq] none
(bfqに変更された)

両方ともbfqに変更されました。ここまでの準備を行わないと、io.bfq.weightファイルにデフォルト値以外を設定できません。

さて準備は済みましたので、目的のファイルio.bfq.weightの中身を見てみましょう。

$ cat /sys/fs/cgroup/test3/io.bfq.weight 
default 100

デフォルト値が変更できる場合、上記のようにデフォルト値を表すキーとしてdefaultという文字列が使われます。値は、ウェイトでの分配を行うためのファイルですので、デフォルト値として100が設定されています。

このデフォルト値を変更するには、キーと値のところで紹介したとおり、"default (設定したい値)"のようなキーと値の組を渡します。

$ echo "default 200" | sudo tee /sys/fs/cgroup/test3/io.bfq.weight
(デフォルト値を200に変更)
default 200
$ cat /sys/fs/cgroup/test3/io.bfq.weight 
default 200 (変更された)

このようにデフォルト値だけが設定されている場合は、すべての対象にこのデフォルト値が適用された状態になっています。

さて、ここで/dev/vdbをデフォルト値から値を変更してみましょう。この場合はネストしたキーのところで紹介したように、キーとして/dev/vdb「メジャー番号:マイナー番号」を書き込みます。

$ echo "252:16 300" | sudo tee /sys/fs/cgroup/test3/io.bfq.weight
252:16 300
(/dev/vdbの設定値を300に変更する)
$ cat /sys/fs/cgroup/test3/io.bfq.weight 
default 200
252:16 300
(300に変更された)

このように新しい行が追加され、/dev/vdbを表す252:16というキーに対して300という値が設定されています。

このように設定した値をデフォルト値に戻すにはどうすれば良いでしょう? やってみましょう。

$ echo "252:16 default" | sudo tee /sys/fs/cgroup/test3/io.bfq.weight
252:16 default
$ cat /sys/fs/cgroup/test3/io.bfq.weight 
default 200

このように、デフォルト値に戻すには値としてdefaultという値を書き込みます。すると、上のように先ほど追加された252:16 300という行が消えて、再びデフォルト値の設定だけに戻ります。

設定する値をデフォルト値に変更するためにインターフェースファイルに書き込むときは、値としてdefaultを使います。しかし、値を確認するためにインターフェースファイルを読み込むときには、値としてdefaultと書かれた行は出現しません。

まとめ

今回は、cgroup v2で明確に定められたリソース分配の種類や、インターフェースファイルのフォーマット、操作方法について説明しました。

機能の説明というより、規約の説明のような回となったので、これまでと違ってちょっと退屈に感じた方がいるかもしれませんね。

実は、今回の内容はカーネル付属文書にきちんと書かれている内容でした。そこに実例を少し足してわかりやすくすることを目指しました。cgroup v2の文書はかなり細かく、きちっと書かれていると思いますので、ぜひカーネル付属文書も参照してみてください。

おすすめ記事

記事・ニュース一覧