TT Lab
はじめる
学ぶ 学習パス コース

コンテナの内部原理

制限をかけて観測値と照合する

TT Labで続きを見る

このラボは本物のVM上で動きます

この箱はPodではなく、KubeVirtが起動した仮想マシンです。Linuxカーネルが別に動き、systemdが実際にサービスを管理し、dockerは模倣ではなく本物のDockerエンジンです。docker runで起動したコンテナは実際にプロセスになり、docker execもdocker logsもそのまま動作します。

以前は、このラボはPodの中で動いていました。カーネルの権限をすべて下ろした箱なので、コンテナを起動するステップが塞がれていて、そのためイメージのアーカイブを直接展開してみるという回り道で学んでいました。もう回り道は必要ありません。

知っておくことが2つあります。

目標

メモリ・CPU・PIDの上限を自分でかけてみて、リクエストした値(docker inspectのHostConfig)と、コンテナが実際に見る値(/sys/fs/cgroup/*)を並べて比較します。終了コード137を自分で作ってみて、OOM Killと区別する方法を整理します。

なぜ重要なのか

運用で出てくる質問は、ほとんどいつも「制限をかけたのに、なぜ守られないのか」か、「CPUは暇なのに、なぜ遅いのか」です。1つ目は、リクエスト値と強制される値が違うことがある(rootless環境では、systemd delegationがないとリクエストだけが記録されます)ことを知らないために生じ、2つ目は、CPUのcgroupがプロセスを殺さずに眠らせることを知らないために生じます。そしてどちらも、docker statsやダッシュボードではなく、cgroupのファイルを直接読んで初めて確定できます。そのためこのラボは、値を2か所から読んで突き合わせる習慣を作ることが目的です。片方だけを読むと、必ず間違った結論に達します。

ステップ

  1. /root/int2ディレクトリを作成し、stat -fc %T /sys/fs/cgroupの出力を、そのまま/root/int2/version.txtに保存します(cgroup2fsならv2、tmpfsならv1)。
  2. /sys/fs/cgroup/cgroup.controllersファイルの内容を、そのまま/root/int2/controllers.txtに保存します。
  3. alpine:3.20イメージで、dk-memコンテナを、メモリ上限64MiB(67108864バイト)でかけて、バックグラウンドで実行します。採点の時点で、running状態のままである必要があります。
  4. dk-memの中で/sys/fs/cgroup/memory.maxを読んで、その値をそのまま/root/int2/memmax.txtに保存します。
  5. alpine:3.20イメージで、dk-cpuコンテナを、CPU 0.5コアの上限でかけて、バックグラウンドで実行します。
  6. dk-killedコンテナを起動したあと、自分でSIGKILLを送って終了コード137で終わらせます(OOMであってはいけません)。そして/root/int2/exit137.txtに、137が128+9でできているという分解と、OOM Killも同じ137を出すという点を、あわせて書きます。
  7. alpine:3.20イメージで、dk-pidsコンテナを、PID上限64でかけて実行します。
  8. /root/int2/limits.mdに3行を書きます。dk-memの行には実際のバイト値67108864を、dk-cpuの行には0.5(または500m)を、dk-pidsの行には64を含めます。

参考

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は数字のまま入っている必要があります。