おひさしぶりです。今年はこれまで結局一回もこの連載の記事を書きませんでした。年末が近づきAdvent Calendarの季節になり、毎年参加しているAdvent Calendarの案内が今年もあったので、また記事でAdvent Calendarに参加しようと思い戻ってきました。この記事はLinux Advent Calendar 2021の21日目の記事です。
仕事が忙しかったりして、なかなかこの連載に手が回らなかった一方で、今年は2013年以来開催してきたコンテナの勉強会をオンラインで2回開催できました。4月にSeccompをテーマにした勉強会、10月に「Rustとコンテナ」をテーマにした勉強会を開催しました。どちらもこの連載で触れてきたような基本的な内容からさらに深い内容、この連載では全く触れていない内容まで、非常に広く深いお話が聞ける勉強会になりました。どちらも動画がアーカイブ公開されていますので、ご興味のあるテーマがあればぜひご覧になってください。
さて、これまで7回にわたって、この連載でcgroup v2について紹介してきました。これまではcgroupを作ったり、それぞれのcgroupでどのようなコントローラを有効にしてリソース制限を行うか? といった、cgroupコア部分が持つ、cgroup自身の階層構造を構成する機能を中心に解説してきました。
今回から、実際のリソース制限を行うコントローラをcgroup v2でどのように使うか? といったところを紹介していきたいと思います。
コントローラの使い方と言っても、ほとんどのコントローラでは基本はcgroup v1のときと大きく変わりません。そこで、今回は操作方法を細かく説明するのではなく、cgroup v2のコントローラを全体的に見ながら、cgroup v1から制御方法が変わったところを紹介したり、cgroup v1から動きが変わったところをつまみ食いしながら軽く説明をしていきたいと思います。同時に最近追加された新しいコントローラについても紹介します。
cgroup v2で使えるコントローラ
現時点(5.15カーネル時点)でcgroup v2で使えるコントローラは次の通りです。
表1 cgroup v2で使えるコントローラ(5.15カーネル)
コントローラ名 |
使えるようになったバージョン |
cpu |
4.15 |
cpuset |
5.0 |
device |
4.15 |
freezer |
5.2 |
hugetlb |
5.6 |
io |
4.5 |
memory |
4.5 |
misc |
5.13 |
perf_event |
4.11 |
pids |
4.5 |
rdma |
4.11 |
5.13カーネルを使っており、デフォルトでcgroup v2のみがマウントされているUbuntu 21.10の環境では、root cgroup配下のcgroup.controllers
は次のようになっています。
Ubuntu 21.10のような新しい環境では、上記のようにデフォルトでcgroup v2のみを使うようになっています。しかし現時点では、多くのシステムでcgroup v1のみがマウントされているか、cgroup v1とv2の両方がマウントされていると思います。
第37回で説明したとおり、cgroup v1とv2両方がマウントされている場合は、cgroup v1側で有効になっていないコントローラのみがcgroup v2で使えます。このような環境で特定のコントローラをcgroup v2で使いたい場合は、起動時にカーネルパラメータcgroup_no_v1=
を使ってcgroup v2で使いたいコントローラを指定します。all
を指定するとcgroup v1側では全コントローラが無効になります。
ここでcgroupを1つ作成し、/sys/fs/cgroup/cgroup.controllers
に出現しているコントローラを全部、子cgroupで使えるようにしてみましょう。このあたりの操作方法については第38回をご覧ください。
このように、cgroup.subtree_control
ファイルに子cgroupで使いたいコントローラを、コントローラ名の前に+
をつけて書き込むと登録できました。登録すると、子cgroupでコントローラが有効になり、cgroupディレクトリ内にはコントローラ用のファイルが出現するのでした。この例ではcgroup.controllers
内に書かれているコントローラすべてを書いています。実際は、Ubuntu 21.10ではデフォルトでいくつかコントローラは登録されていますので、このように全部を追加する必要はありません。
次のように、全コントローラをcgroup.subtree_control
に登録すると大量のファイルが出現します。
ちなみにroot cgroup内を見てみると、次のようになっています。
子cgroupよりはファイル数が大幅に少ないことがわかります。root cgroupはリソース制限が行えないので、基本的にはコントローラ用のファイルは出現しません。しかし、一部のコントローラ用のファイルについては、全体の統計情報を見れるようにroot cgroupに出現します。
rootでもroot以外でも、cgroup自体を操作したり、cgroup自体の情報を見るファイルはcgroup.
で始まり、コントローラ用のファイルはコントローラ名が頭に付きます。
deviceコントローラ
ここで改めて先ほども見たcgroup.controllers
ファイルを見てみましょう。このファイルには使えるコントローラが一覧されています。
先の表1とcgroup.controllers
ファイルを見比べると、deviceがないことに気づいた方もいらっしゃるのではないでしょうか。
cgroup.controllers
内に記載がないからといって、cgroup v2でdeviceコントローラが使えないわけでも、Ubuntuカーネルでdeviceコントローラが有効になっていないわけではありません。
cgroup v2からは、deviceコントローラには他のコントローラのようなインターフェースファイルがなくなり、最近流行の(?)eBPFプログラムをcgroupにアタッチして制限を行うようになりました。デバイスへのアクセスを行おうとすると、そのアタッチしたプログラムへ制御が移り、プログラムが成功もしくは失敗を返して制御を行います[1]。
ネットワーク系コントローラ
cgroup v2で使えるコントローラを眺めると、cgroup v1にあったネットワーク系のコントローラがないことに気づきます。
cgroup v1には、ネットワーク処理の優先度を指定するnet_prioコントローラと、cgroup内のプロセスが作成したパケットにクラスIDを指定して、tcやiptablesで制御を行うnet_clsコントローラが存在しました。
このcgroup v1のnet_clsコントローラでIDを指定するには、
このように、対象とするcgroup配下のIDを指定するためのファイルnet_cls.classid
ファイルにIDを登録する必要がありました。
そして、このIDを登録したcgroupに対してiptablesを使って、次のようにフィルタリングを定義してフィルタリングできました。
現在はiptablesでcgroup v2の対応が進み、net_clsでIDを指定しなくても、直接cgroupのパスを指定してフィルタリングが行えるようになりました。
特にフィルタリングしない環境でping
が通っている環境があるとします。まずはcgroupを作成し、そのcgroupにシェルのPIDを登録します。
そして、iptablesのcgroupモジュールで、cgroup v2のcgroupとしてのパスを--path
で指定してフィルタリングを設定します。
ここでは、さきほどpingが通っていた192.168.122.1
に対するフィルタリングを、/test
cgroupに対して--path
を使って設定します。
ここで指定するパスは、対象とするPIDの/proc/[PID]/cgroup
内で確認できるcgroupとしてのパスです。今回の例だと/test
ということになります。
フィルタリングが設定された状態で、先ほどと同様にping
を実行してみます。
このようにフィルタリングされているのでエラーとなります。
もちろん、フィルタリングを削除すると元通りにping
での疎通ができます。
また、tc
プログラムでeBPFプログラムをロードし、cgroupのパスをeBPFのデータ構造に保存し、tc
がロードしたeBPFプログラム内でcgroupのパスを取得して制御の対象かどうかを判定できるようになっているようです。
このあたりのサンプルプログラムがカーネルソースのサンプルプログラムに含まれていますので、詳しくはそちらをご参照ください[2]。
暗黙のうちに常に有効になるコントローラ
このほかに、cgroup v2では暗黙のうちに有効になるコントローラが存在します。
cgroupのコントローラを実装する際にはcgroup_subsys
という構造体を実装します。この構造体のメンバ変数にimplicit_on_dfl
というbool
の変数が存在します。
この変数がtrue
に設定されたコントローラはcgroup v2では、次のように動作します。
- cgroup v2ツリー上の全cgroupで暗黙のうちに有効となる
cgroup.controllers
とcgroup.subtree_control
にはコントローラ名がリストされない
ただし、cgroup v2でコントローラを使うには、cgroup v1ではコントローラが有効化されていないことが条件でした。つまりcgroup v1でコントローラが有効になっていない場合のみ、1つ目に挙げたようにcgroup v2のツリー全体の各cgroupでコントローラが有効になります。
また第39回で、cgroup v2でコントローラを有効にしてリソース分配を行う際には、末端のcgroupにしかプロセスが所属できないという制約があることを説明しました。cgroup v2ツリーのすべてのcgroupでコントローラが有効になるということは、implicit_on_dfl
がtrue
になっているコントローラは、この制約を受けないということです。
つまり、リソース制御のための制約の範疇外となることからもわかるように、この暗黙のうちに常に有効になるコントローラは、リソース制御を行うコントローラではなく、ユーティリティ系のコントローラのために設けられた機能です。
執筆時点(5.15カーネル)では、このimplicit_on_dfl
がtrue
になっているコントローラはperf_event
コントローラのみです。
perfは主にLinuxでカーネルの性能に関する情報を収集し、分析するために使います。このperfを使う際に、cgroup内で動作しているプロセスに関するイベントのみをcgroupのパスを指定し、フィルタリングして取得できます。
ここで、cgroup v2では特別な操作なしで、perf_event
コントローラの機能を使えることを簡単に試して見ておきましょう。
まず、root cgroup直下にtest
というcgroupを作成し、この/test
cgroupを指定してperf stat
コマンドを実行します。perf stat
コマンドでcgroupを指定するには--cgroup
または-G
オプションを使います。
ここで別のシェルを起動し、シェルのPIDを作成した/test
cgroupに登録します。登録されたのを確認してyes
コマンドを実行します。
ここでperf stat
を実行しているシェルに戻り、perf stat
を停止します。
このように/test
cgroupを指定してイベントの取得ができています。
ここで、再度/test
cgroupを指定してperf stat
を実行してから、さきほどのシェルを/test
cgroupから削除し、yes
コマンドを実行してみましょう。
そして/test
cgroupを指定して実行しているperf stat
コマンドを停止してみましょう。
/test
cgroupの外でyes
コマンドを実行したので、perf stat
ではデータが収集できていません。
このように、cgroup v2を使うとperf_event
コントローラが常に全cgroupで有効になるため、特別な操作を必要とせずにperf
コマンドでcgroupごとのデータを収集できます。
perf stat
以外でも、perf record
、perf report
コマンドでもcgroupごとのデータを収集して表示させたりできます。
miscコントローラ
表1やここまでの実行例で、この連載ではこれまで全く紹介していないコントローラがあることに気づいた方もいらっしゃるのではないでしょうか。
そうです、misc
コントローラです。このコントローラはcgroup v1でも使えます。以下ではcgroup v2での実行例を示しますが、cgroup v1でも同じように使えます。misc
コントローラは5.13カーネルで導入されました。Ubuntu 21.10でroot cgroupにあるcgroup.controllers
ファイルを見ると、次のようにmisc
という文字列が見えます。
このコントローラは、ホスト上にある有限個のリソースの使用をcgroupで制限したい場合に使います。例えばシステム上に全部でリソースが10個あるとして、それを特定のcgroupやcgroupツリー内では3個まで使わせたい、といった制限に使用できます。
この機能を使いたい場合は、規則に沿ってカーネル内で実装を行うと、misc
コントローラ用のインターフェースファイルに実装に従ってリソース制限を行うためのキーと値のペアが出現します。このようにシンプルなリソース制限を行うための汎用的な入れ物(コントローラ)が準備されたということです。
Ubuntu 21.10環境では、デフォルトではmisc
コントローラはroot cgroupにあるcgroup.subtree_control
には登録されていませんので、子cgroupで使えるようにするにはまず登録が必要です。
この状態で、子cgroupを作成すると、miscコントローラように次のようにインターフェースファイルが2つ出現します。
また、この他にroot cgroupに1つ、miscコントローラ用のファイルがあります。
これらのインターフェースファイルは表2のような機能を持っています。
表2 miscコントローラ用インターフェースファイル
ファイル名 |
機能 |
misc.capacity |
root cgroupのみに出現する読み取り専用ファイル。miscコントローラで制御するホスト上のリソース名と、そのリソースの総量が表示されます |
misc.current |
root cgroup以外に出現する読み取り専用ファイル。cgroupとその子孫のcgroupで使われている量が表示されます |
misc.max |
root cgroup以外に出現する読み書き可能なファイル。cgroupとその子孫で使えるリソースの制限値を表示・設定します |
misc.events |
root cgroup以外に出現する読み取り専用ファイル。cgroupとその子孫で制限値を超えようとした回数(5.16カーネルから) |
miscコントローラは、執筆時点(5.15カーネル)では仮想マシンのメモリを暗号化して保護するAMDのSEVや、SEV-ESで使われるASIDというアドレス空間のID数を制限するための機能のみが使っています。
筆者の手元にはこの機能が使える環境がありませんので、出現しているファイルの中身を見ても空です。
このように具体例を示せませんので、説明のために仮想的な例を挙げて説明したいと思います。ここでは、例としてmiscコントローラでres_a
、res_b
という2つのホスト上のリソースが制御できるようになっているとしましょう。
ホスト上ではres_a
が10個、res_b
が5個使用できるとします。このとき、root cgroupにあるmisc.capacity
の中身は次のようになるでしょう。
test
cgroupで、res_a
を3個、res_b
は使っていない場合、/sys/fs/cgroup/test/misc.current
は次のようになります。
test
cgroupでは、res_a
は制限なし、res_b
は3個までという制限を設定したい場合は、次のように書き込みます。
実際のmiscコントローラの定義と使用
ここまで仮想的な例で説明しましたので、最後に執筆時点でのmiscコントローラに関係する定義をコード上で確認しておきましょう。
miscコントローラでリソースを制御したい場合、まずはinclude/linux/misc_cgroup.h
内のenum misc_res_type
でリソースを定義します。
ここではMISC_CG_RES_SEV
とMISC_CG_RES_SEV_ES
が定義されています。
同様にkernel/cgroup/misc.c
内のmisc_res_name
で、先のenumと同じ順序でリソース名を定義します。
このように、現時点ではsev
とsev_es
というリソースが定義されています。リソースをコントロールしたいモジュールなどは、リソースを使用する前にmisc_cg_set_capacity
関数を呼び出し、システム上に存在するリソースの総量を設定します。設定した値がmisc.capacity
に表示されます。
sev
、sev_es
の場合は、arch/x86/kvm/svm/sev.c
内のsev_hardware_setup
関数内で、このcapacityを設定しています。
リソースを使用する場合は、misc_cg_try_charge
関数でリソースを確保するようで、sev.c
内でもsev_asid_new
関数でリソースを1つ確保するようになっています。
まとめ
今回はcgroup v2で使えるコントローラのcgroup v1からの変化と、5.13カーネルで新たに加わったコントローラであるmiscコントローラについて説明しました。
cgroup v2では、net_clsコントローラのように周辺のツールでのサポートが拡張された結果、cgroup v2ではコントローラ自体が現れなかったり、cgroup v1から採用されているファイルを用いた制限値の設定や確認ではなく、eBPFを使った制限を行うように変化していることがおわかりいただけたと思います。
今回の記事を書くに当たり、udzuraさんにレビューをしていただきました。そして、udzuraさんにcgroupを指定してperf
コマンドを使う方法を教えていただきました。ありがとうございました。
次回も、もう少しcgroup v2でのコントローラについて紹介していく予定です。