前回は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回をご覧ください。
さて、test
cgroupが作成できましたので、cpu.weight
ファイルを見てみましょう。
cgroup作成直後で、何も操作をしていない状態でcpu.weight
ファイルを確認すると、確かにデフォルト値である100
という数値が書かれています。実際に操作をしてみましょう。
範囲内の1
と10000
を書き込むと正常に反映され、範囲外の100000
を書くとエラーになることが確認できました。
制限(Limits)
制限(Limits)は文字通りリソース制限です。設定した上限を超えてリソースを使うことはできません。この制限値はオーバーコミットできます。つまり、子cgroupに割り当てる制限値の合計が、親が利用できるリソース量を超えるような設定ができます。
この制限は、絶対的な制限となるハードリミットとして実装されていることもありますし、ベストエフォートで動作するソフトリミットとして実装されていることもあります。両方とも実装されている場合もあります。
制限として、0以上の数値と"max"という文字列が指定できます。"max"は制限しないという意味になり、デフォルト値は"max"です。
制限でリソース制限を行う場合の、コントローラ用のインターフェースファイル名は、"(コントローラ名)."
に続けて次のような文字列が付いた名前になります。
- ハードリミットとして絶対的な割り当て制限を行う場合:
max
(例:memory.max
)
- ソフトリミットとしてベストエフォートのリソース制限を行う場合:
high
(例:memory.high
)
ここで例としてあげたファイル名はmemory.max
とmemory.high
で、両方ともメモリコントローラです。つまりメモリコントローラはハードリミットとソフトリミットの両方が実装されているということです。
cgroupを作成して、memory.max
とmemory.high
ファイルを見てみましょう。
いずれも作成直後のデフォルト値はmax
となっており、制限がかかっていない状態になっています。ファイルを書き換えてみましょう。
まず"128M"という文字列を書き込み制限値を設定した後に、制限値を外すために"max"という文字列を書き込むと、memory.max
ファイルの中身がmax
と書き換わりました。
保護(Protections)
保護(Protections)は、cgroupに所属するプロセスに対して、設定した量までのリソースを与えることを保証します。ただし、ツリー構造のすべての祖先のcgroupが保護レベル以下である場合だけです。
制限と同様に保護もオーバーコミットでき、オーバーコミットした場合は、親が使える量までしか子では保護されません。
また、強い保護とベストエフォートな保護のどちらの保護にすることもできるのも制限と同様です。両方とも実装されている場合もあります。
保護として、0以上の数値とmaxという文字列が設定できます。デフォルト値は0です。つまりデフォルトでは保護されるリソース値は設定されていません。
保護でリソース分配を行う場合の、コントローラ用のインターフェースファイル名は、"(コントローラ名)."
に続けて次のような文字列が付いた名前になります。
- リソースの絶対的な割り当て保証(保護)を行う場合:
min
(例:memory.min
)
- リソースのベストエフォートのリソース割り当て保証(保護)を行う場合:
low
(例:memory.low
)
例としてあげたメモリコントローラは、制限と同様に絶対的な保護とベストエフォートの保護の両方が実装されています。
ここでもmemory.min
とmemory.low
を使って簡単に動きを見ておきましょう。cgroupを作成し、デフォルト値を確認します。
いずれもデフォルト値は0
になっていることが確認できました。数値以外の値として書き込める"max"を書き込み、その後数値を設定してみましょう。
このように"max"で最大値が設定できましたし、数値での制限値も設定できました。
割り当て(Allocations)
割り当て(Allocations)は、有限なリソースを独占的に割り当てます。つまりオーバーコミットできません。子に割り当てられるリソースの合計は親の割り当て量を超えられません。
割り当てとして、0以上の数値とmaxという文字列が設定できます。デフォルト値は0です。つまりデフォルトではリソースがない状態で、そこから有限なリソースを必要分だけ子に割り当てていくというモデルになります。
現時点ではこのモデルが実装されたコントローラはないようです。
このタイプでの制限では設定値を超えることはできませんので、ファイル名も"(コントローラ名).max"
となるようです。
インターフェースファイル内のフォーマット
cgroupを制御するために使われるインターフェースファイルには、格納されているデータに応じて色々なフォーマットがあります。インターフェースファイルについてもcgroup v2ではフォーマットがいくつかあらかじめ定められており、そのうちのどれかのフォーマットになっています。それを紹介していきましょう。
改行で区切られた値
まずは1行に1つ値が書かれたいるタイプのファイルです。
今回、ここまでに例としてあげたインターフェースファイルは制限値を設定するためのファイルでした。制限値を1つ設定するだけでしたので、値や文字列が1つだけ設定されており最後は改行されていました。このようなファイルもこのタイプに入ります。
1つ以上の値が書かれる場合は改行で区切られて1行に1つ値が入ります。例えば、cgroupを操作する際の基本的な操作である、プロセスをcgroupに所属させたり、所属しているプロセスを取得するために使われるcgroup.procs
ファイルです。
このタイプのファイルは、一度に1つの値しか書き込めません。
スペース区切りの複数の値
1 行に複数の値が書かれる場合があります。例えばcgroup.subtree_control
やcgroup.controllers
のようなファイルです。
このタイプのファイルは、読み込み専用のファイルであるか、もしくは一度に複数の値を書き込めるファイルのどちらかです。
キーと値
設定項目とそれに対する値を設定するようなケースです。例えば "key_a" という設定項目に対する値が "val_a" である場合、
のようになります。1 つのファイルに複数、設定したり表示したりする項目がある場合は、この組が複数行になります。
このタイプは、統計を表示するようなファイルに良くあります。例えば、cpu.stat
やmemory.stat
のようなファイルです。
書き込みを行うファイルの例としては、ioコントローラ用でウェイトを設定するためファイルがこのタイプに相当します。ioコントローラーはデバイスごとに制限が設定できるためです。
このタイプのファイルに値を書き込むには、キーと値のペアをスペース区切りで渡します。
上記の例では、元々キーdefault
に対する値が100
という設定がされていたところに、default 200
という文字列を書き込むことでdefault
に対する値が変更されていることがわかります。
ネストしたキー
キーに対する値が1つだけの場合は、前で説明したようなファイルになります。一方で、1つのキーに対してさらにキーと値を複数持つ場合があります。この場合は次のような構造になります。
例えば、IOに関する統計情報を見るためのファイルio.stat
は次のようになっています。
これは253:0
というデバイスに対するrbytes
、wbytes
、rios
、wios
、dbytes
、dios
という複数の項目名とそれに対する値を表示しています。見たとおり読み込み、書き込みのバイト数やIO数が表示されています。dで始まるのは破棄(discard)されたバイト数、IO数です。
書き込む際もファイルを読んだ場合に表示されたのと同様のフォーマットで書き込みます。例えばIOの制限値を設定するio.max
ファイルには次のように書き込みます。
上の例では、253:0
というデバイスに対する読み書きのIOPS(riops
とwiops
)を200
に設定しています。明示的に設定していない読み書きバイト数はデフォルトである制限なし(max
)に設定されています。
ちなみに253:0
はデバイスに対する「メジャー番号:マイナー番号」で、例で使っている環境では/dev/vda
です。
上の例で設定した値を削除したい場合は、デフォルトのmax
に設定すれば削除されます。
デフォルト値の変更
ここまでで説明したインターフェースファイルのうち、キーと値の組を設定するような設定項目では、デフォルト値が変更できる場合があります。先の例だとio.weight
の場合がそのようなケースに該当します。
このデフォルト値の設定についても紹介しておきましょう。
先の例ではio.weight
を紹介しましたが、ここではio.bfq.weight
ファイルを使います[1]。
io.bfq.weight
ファイルを使うためには、少し準備が必要です。cgroupは、先の例で使用したtest3
cgroupを引き続き使います。Ubuntu 21.10環境では、ここまでの操作ではio.bfq.weight
はcgroup内にはありません。bfq
モジュールをロードしましょう。
bfq
モジュールをロードしたので、io.bfq.weight
ファイルが出現しました。
さらに準備を続けます。ここで例として使用している筆者の環境は、ブロックデバイスとして/dev/vda
と/dev/vdb
があります。この両方のIOスケジューラがどうなっているかを確認します。
上記のようにmq-deadline
が選択されています(ブラケットで囲まれている文字列が選択されていることを示します)。これをbfq
を使うように設定してみます。
両方ともbfq
に変更されました。ここまでの準備を行わないと、io.bfq.weight
ファイルにデフォルト値以外を設定できません。
さて準備は済みましたので、目的のファイルio.bfq.weight
の中身を見てみましょう。
デフォルト値が変更できる場合、上記のようにデフォルト値を表すキーとしてdefault
という文字列が使われます。値は、ウェイトでの分配を行うためのファイルですので、デフォルト値として100
が設定されています。
このデフォルト値を変更するには、キーと値のところで紹介したとおり、"default (設定したい値)"
のようなキーと値の組を渡します。
このようにデフォルト値だけが設定されている場合は、すべての対象にこのデフォルト値が適用された状態になっています。
さて、ここで/dev/vdb
をデフォルト値から値を変更してみましょう。この場合はネストしたキーのところで紹介したように、キーとして/dev/vdb
の「メジャー番号:マイナー番号」を書き込みます。
このように新しい行が追加され、/dev/vdb
を表す252:16
というキーに対して300
という値が設定されています。
このように設定した値をデフォルト値に戻すにはどうすれば良いでしょう? やってみましょう。
このように、デフォルト値に戻すには値としてdefault
という値を書き込みます。すると、上のように先ほど追加された252:16 300
という行が消えて、再びデフォルト値の設定だけに戻ります。
設定する値をデフォルト値に変更するためにインターフェースファイルに書き込むときは、値としてdefault
を使います。しかし、値を確認するためにインターフェースファイルを読み込むときには、値としてdefault
と書かれた行は出現しません。
まとめ
今回は、cgroup v2で明確に定められたリソース分配の種類や、インターフェースファイルのフォーマット、操作方法について説明しました。
機能の説明というより、規約の説明のような回となったので、これまでと違ってちょっと退屈に感じた方がいるかもしれませんね。
実は、今回の内容はカーネル付属文書にきちんと書かれている内容でした。そこに実例を少し足してわかりやすくすることを目指しました。cgroup v2の文書はかなり細かく、きちっと書かれていると思いますので、ぜひカーネル付属文書も参照してみてください。