制限をかけて観測値と照合する
このラボは本物のVM上で動きます
この箱はPodではなく、KubeVirtが起動した仮想マシンです。Linuxカーネルが別に動き、systemdが実際にサービスを管理し、dockerは模倣ではなく本物のDockerエンジンです。docker runで起動したコンテナは実際にプロセスになり、docker execもdocker logsもそのまま動作します。
以前は、このラボはPodの中で動いていました。カーネルの権限をすべて下ろした箱なので、コンテナを起動するステップが塞がれていて、そのためイメージのアーカイブを直接展開してみるという回り道で学んでいました。もう回り道は必要ありません。
知っておくことが2つあります。
- 最初の起動に1分ほどかかります。VMが起動してDockerをインストールするためです。Podのラボ(通常40秒)より遅くなります。
- ブラウザのプレビューはありません。VMに入ってくる接続は、採点用のポート1つだけが開いています。Webサーバーを起動したなら、VMの中で
curlで確認してください。
目標
メモリ・CPU・PIDの上限を自分でかけてみて、リクエストした値(docker inspectのHostConfig)と、コンテナが実際に見る値(/sys/fs/cgroup/*)を並べて比較します。終了コード137を自分で作ってみて、OOM Killと区別する方法を整理します。
なぜ重要なのか
運用で出てくる質問は、ほとんどいつも「制限をかけたのに、なぜ守られないのか」か、「CPUは暇なのに、なぜ遅いのか」です。1つ目は、リクエスト値と強制される値が違うことがある(rootless環境では、systemd delegationがないとリクエストだけが記録されます)ことを知らないために生じ、2つ目は、CPUのcgroupがプロセスを殺さずに眠らせることを知らないために生じます。そしてどちらも、docker statsやダッシュボードではなく、cgroupのファイルを直接読んで初めて確定できます。そのためこのラボは、値を2か所から読んで突き合わせる習慣を作ることが目的です。片方だけを読むと、必ず間違った結論に達します。
ステップ
/root/int2ディレクトリを作成し、stat -fc %T /sys/fs/cgroupの出力を、そのまま/root/int2/version.txtに保存します(cgroup2fsならv2、tmpfsならv1)。/sys/fs/cgroup/cgroup.controllersファイルの内容を、そのまま/root/int2/controllers.txtに保存します。alpine:3.20イメージで、dk-memコンテナを、メモリ上限64MiB(67108864バイト)でかけて、バックグラウンドで実行します。採点の時点で、running状態のままである必要があります。dk-memの中で/sys/fs/cgroup/memory.maxを読んで、その値をそのまま/root/int2/memmax.txtに保存します。alpine:3.20イメージで、dk-cpuコンテナを、CPU 0.5コアの上限でかけて、バックグラウンドで実行します。dk-killedコンテナを起動したあと、自分でSIGKILLを送って終了コード137で終わらせます(OOMであってはいけません)。そして/root/int2/exit137.txtに、137が128+9でできているという分解と、OOMKillも同じ137を出すという点を、あわせて書きます。alpine:3.20イメージで、dk-pidsコンテナを、PID上限64でかけて実行します。/root/int2/limits.mdに3行を書きます。dk-memの行には実際のバイト値67108864を、dk-cpuの行には0.5(または500m)を、dk-pidsの行には64を含めます。
参考
- メモリの上限は
--memory=64m、CPUは--cpus=0.5、PIDは--pids-limit=64でかけます。 - リクエスト値の確認は、
docker inspect <이름> | jq -r '.[0].HostConfig.Memory'のように読みます(プレースホルダーはコンテナ名です)。 - 直接シグナルを送るコマンドは
docker killで、デフォルトのシグナルはSIGKILLです。 - 終了コードとOOMかどうかは、
docker inspect <이름> | jq -r '.[0].State.ExitCode, .[0].State.OOMKilled'で一緒に見られます(プレースホルダーはコンテナ名です)。 - よくある間違い1: ステップ3・5・7で
--rmを付けると、コンテナが消えて採点する対象がなくなります。 - よくある間違い2: ステップ6でメモリを爆発させて殺すと、
OOMKilledがtrueになって失敗します。このステップは、人が送ったSIGKILLである必要があります。 - よくある間違い3: ステップ4の値が、ステップ3でリクエストした数字と違っていても、直そうとしないでください。見たとおりに書くのが正解です。
cgroupのバージョンを判別する
/root/int2ディレクトリを作成し、stat -fc %T /sys/fs/cgroupの出力を、そのまま/root/int2/version.txtに保存します(cgroup2fsならv2、tmpfsならv1)。
マウントされたファイルシステムの種類を尋ねるstatのオプションがあります。結果の文字列を解釈してv1/v2と書くのではなく、出てきた文字列をそのまま保存する必要があります。
利用可能なコントローラーの一覧
/sys/fs/cgroup/cgroup.controllersファイルの内容を、そのまま/root/int2/controllers.txtに保存します。
cgroup v2は、ルート階層に、利用可能なコントローラー名を1行に並べたファイルを置きます。手で書き写さずに、そのファイルの内容をそのままコピーしてください。空白の違いは許容されますが、項目が抜けてはいけません。
メモリ上限64MiBをリクエストする
alpine:3.20イメージで、dk-memコンテナを、メモリ上限64MiB(67108864バイト)でかけて、バックグラウンドで実行します。採点の時点で、running状態のままである必要があります。
メモリ上限のフラグは、m、gのような接尾辞を受け付けます。64MiBはちょうど67108864バイトで、採点はこの数字を見ます。コンテナは採点の時点で起動している必要があるので、すぐに終わるコマンドで起動してはいけません。
コンテナが実際に見るmemory.max
dk-memの中で/sys/fs/cgroup/memory.maxを読んで、その値をそのまま/root/int2/memmax.txtに保存します。
コンテナの中で/sys/fs/cgroup/memory.maxを読みます。値が67108864ではなくmaxと出ることもありますが、それは誤答ではなく、このステップの核心です。リクエスト値と強制される値は、違うことがあります。見たとおりに保存してください。
CPU 0.5コアの上限
alpine:3.20イメージで、dk-cpuコンテナを、CPU 0.5コアの上限でかけて、バックグラウンドで実行します。
コア数を小数で渡すフラグがあります。内部的にはquotaがperiodの半分になり、これは「コアを半分に切る」ではなく、「毎周期の半分だけ実行する」という意味です。
終了コード137を作って解釈する
dk-killedコンテナを起動したあと、自分でSIGKILLを送って終了コード137で終わらせます(OOMであってはいけません)。そして/root/int2/exit137.txtに、137が128+9でできているという分解と、OOM Killも同じ137を出すという点を、あわせて書きます。
メモリを爆発させて殺すのではなく、自分でSIGKILLを送る必要があります(OOMKilledがtrueなら、このステップは失敗します)。そしてメモファイルには、137を128と9に分解した説明と、OOM Killもまったく同じ137を出すという事実を、あわせて書いてください。
PID上限でフォーク爆弾を防ぐ
alpine:3.20イメージで、dk-pidsコンテナを、PID上限64でかけて実行します。
プロセス数の上限をかけるフラグが、別にあります。メモリやCPUの上限では、フォーク爆弾を防げません。プロセス1つ1つは小さくても、数でカーネルのリソースを枯渇させるからです。
制限の要約表を作る
/root/int2/limits.mdに3行を書きます。dk-memの行には実際のバイト値67108864を、dk-cpuの行には0.5(または500m)を、dk-pidsの行には64を含めます。
3行とも、推測ではなくdocker inspectで確認した値を書く必要があります。メモリはバイトの数字のまま、CPUは0.5とわかるように、PIDは数字のまま入っている必要があります。