LabHub

ブログ

[containerd] コンテナライフサイクル管理

한국어English日本語

containerdコンテナライフサイクル管理

containerdにおけるコンテナの作成から終了までの全ライフサイクルを分析します。コンテナメタデータと実行プロセス(Task)の分離、shimによるプロセス管理、各種ランタイムクラスの統合方式を見ていきます。


1. ContainerとTaskの分離

1.1 核心概念

containerdはコンテナのメタデータと実行状態を分離します:

Container(メタデータ):
  - ID、イメージ参照、スナップショットキー
  - OCIランタイムスペック
  - ラベル、拡張データ
  - BoltDBに永続保存

Task(実行状態):
  - 実際に実行中のプロセス
  - PID、状態(created/running/stopped)
  - stdin/stdout/stderr
  - shimプロセスが管理

1.2 分離の利点

分離設計の利点:

1. コンテナメタデータはTaskなしで存在可能
   - コンテナを作成して後から起動可能
   - 停止したコンテナのメタデータを保持

2. containerd再起動に独立
   - Taskはshimが管理するためcontainerd再起動でも維持
   - 再起動後に既存shimに再接続

3. 多様なランタイムサポート
   - Containerオブジェクトはランタイムに非依存
   - Task作成時にランタイムを選択

2. コンテナ作成

2.1 作成プロセス

コンテナ作成フロー:

1. イメージからOCIスペック生成
        |
        v
2. スナップショット準備
   - イメージスナップショットチェーンにActiveスナップショット追加
   - コンテナの書き込み可能レイヤー
        |
        v
3. コンテナメタデータ保存
   - BoltDBにContainerレコード作成
   - ID、イメージ、スナップショット、ランタイム、スペックを保存
        |
        v
4. コンテナオブジェクト返却
   (まだプロセスは開始されていない)

2.2 OCIランタイムスペック

containerdはOCIランタイムスペックを生成してコンテナ実行環境を定義します:

OCIランタイムスペック主要セクション:

ociVersion: "1.0.2"

process:
  terminal: false
  user: uid=0, gid=0
  args: ["/bin/sh"]
  env: ["PATH=/usr/local/sbin:..."]
  cwd: "/"
  capabilities: ...
  rlimits: ...

root:
  path: "rootfs"
  readonly: false

hostname: "container-abc"

mounts:
  - destination: "/proc"
    type: "proc"
    source: "proc"
  - destination: "/dev"
    type: "tmpfs"
    source: "tmpfs"

linux:
  namespaces:
    - type: "pid"
    - type: "network"
    - type: "ipc"
    - type: "uts"
    - type: "mount"
  resources:
    memory:
      limit: 536870912
    cpu:
      shares: 1024
      quota: 100000
      period: 100000
  cgroupsPath: "/kubelet/pod-abc/container-xyz"

2.3 スペックジェネレーター(Spec Opts)

containerdのスペック生成パターン:

Spec OptsはOCIスペックを段階的に構築する関数チェーンです:

WithImageConfig(image)     -> イメージのCMD、ENV、WORKDIRを適用
WithHostNamespace(ns)      -> ホストネームスペース共有
WithMemoryLimit(limit)     -> メモリ制限設定
WithCPUs(cpus)             -> CPU制限設定
WithMounts(mounts)         -> マウントポイント追加
WithProcessArgs(args)      -> プロセス引数設定
WithRootfsPropagation(p)   -> rootfsマウント伝播設定
WithSeccompProfile(p)      -> Seccompプロファイル適用
WithApparmorProfile(p)     -> AppArmorプロファイル適用

3. Task実行

3.1 Task作成

Task作成フロー:

1. コンテナのランタイムタイプ確認
   (例:io.containerd.runc.v2)
        |
        v
2. shimバイナリ実行
   (containerd-shim-runc-v2 start)
        |
        v
3. shimがttrpcソケットアドレスを返す
        |
        v
4. containerdがshimにCreateリクエスト
   - OCIスペック転送
   - バンドルパス転送
        |
        v
5. shimがrunc createを実行
   - ネームスペース作成
   - cgroup設定
   - rootfsマウント
   - プロセス作成(まだ開始していない)
        |
        v
6. Task状態:Created

3.2 Task開始

Task開始:

1. containerdがshimにStartリクエスト
        |
        v
2. shimがrunc startを実行
   - コンテナプロセスのinitプロセス開始
   - exec.fifoで同期
        |
        v
3. Task状態:Running
   - PID割り当て
   - stdin/stdout/stderr接続

3.3 Task状態遷移

Task状態マシン:

  Created
     |
     | Start()
     v
  Running
     |
     +-- Kill(signal) -> シグナル送信
     |
     +-- Pause()  -> Paused
     |                 |
     |                 +-- Resume() -> Running
     |
     +-- プロセス終了 -> Stopped
     |
     v
  Stopped
     |
     | Delete()
     v
  (削除済み)

3.4 Exec(追加プロセス)

Exec動作:

既に実行中のコンテナに新しいプロセスを追加:

1. ExecProcess作成
   - 新しいプロセスのスペック定義(args、env、user)
   - execID割り当て
        |
        v
2. shimにExecリクエスト
        |
        v
3. runc exec実行
   - 既存コンテナのネームスペースに参入
   - 新しいプロセス開始
        |
        v
4. 独立してstdin/stdout/stderrを管理

使用例:kubectl exec、docker exec

4. Shimライフサイクル

4.1 Shim起動

Shim起動プロセス:

1. containerdがshimバイナリをfork/exec
   containerd-shim-runc-v2 -namespace k8s.io \
     -id container-abc \
     -address /run/containerd/containerd.sock \
     start
        |
        v
2. shimが自身をデーモン化
   - 親プロセスから分離(setsid)
   - containerdと独立して実行
        |
        v
3. ttrpc Unixソケット作成
   /run/containerd/s/abc123...
        |
        v
4. ソケットアドレスをstdoutに出力
   containerdがこのアドレスを読んで接続

4.2 Shimの役割詳細

Shimの主要責任:

1. プロセス管理:
   - コンテナプロセスの親役割
   - wait4()で終了ステータス収集
   - OOMイベント検知と報告

2. I/O管理:
   - stdin/stdout/stderr FIFO管理
   - ログドライバーとの接続
   - I/Oコピー(containerProcess <-> FIFO)

3. containerdとの通信:
   - ttrpcによるコマンド受信
   - イベント報告(TaskExitなど)
   - ステータス照会応答

4. containerd再起動対応:
   - containerd再起動時も実行継続
   - 再起動されたcontainerdが既存shimに再接続
   - 状態復旧

4.3 Shimシャットダウン

Shimシャットダウン:

1. Task Deleteリクエスト受信
        |
        v
2. コンテナリソースクリーンアップ
   - cgroup削除
   - ネームスペースクリーンアップ
   - rootfsアンマウント
        |
        v
3. ttrpcソケットクローズ
        |
        v
4. shimプロセス終了

5. Checkpoint/Restore

5.1 Checkpoint

Checkpoint動作:

実行中のコンテナの状態をスナップショットとして保存:

1. CRIU(Checkpoint/Restore in Userspace)呼び出し
        |
        v
2. プロセスメモリダンプ
   - メモリページ保存
   - ファイルディスクリプタ状態保存
   - ネットワーク接続状態保存
        |
        v
3. チェックポイントイメージ作成
   - CRIUイメージファイルセット
   - コンテナスペックと一緒に保存
        |
        v
4. オプションでコンテナ停止

使用事例:
  - ライブマイグレーション
  - 高速起動(事前ウォームアップ状態から復元)
  - デバッグ(特定時点の状態キャプチャ)

5.2 Restore

Restore動作:

1. チェックポイントイメージ読み込み
        |
        v
2. 新しいコンテナ環境準備
   - ネームスペース作成
   - rootfsマウント
        |
        v
3. CRIU restore実行
   - メモリページ復元
   - プロセス状態復元
   - ファイルディスクリプタ再接続
        |
        v
4. プロセス実行再開

6. ランタイムクラス

6.1 多様なランタイムサポート

containerdはshimインターフェースを通じて多様なランタイムをサポートします:

ランタイムクラス別比較:

+----------+---------------+-----------+----------+-----------+
| ランタイム| 分離レベル    | オーバヘッド| 起動時間| 互換性    |
+----------+---------------+-----------+----------+-----------+
| runc     | ネームスペース | 最小      | 高速    | 最高      |
| kata     | 軽量VM        | 中程度    | 中程度  | 高い      |
| gVisor   | ユーザーカーネル| 低い     | 高速    | 中程度    |
| Wasm     | Wasmサンドボックス| 最小   | 非常に速い| 限定的  |
+----------+---------------+-----------+----------+-----------+

6.2 runc

runc:

- デフォルトOCIランタイム
- Linuxネームスペースとcgroupベースの分離
- ホストカーネルを直接使用
- 最小オーバーヘッド
- すべてのLinuxコンテナワークロードに適合

shim: containerd-shim-runc-v2
config.toml:
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
    runtime_type = "io.containerd.runc.v2"

6.3 Kata Containers

Kata Containers:

- 軽量VM内部でコンテナを実行
- QEMU/Cloud-Hypervisor/Firecrackerを使用
- 別のゲストカーネルで強力な分離
- マルチテナント環境に適合
- VMオーバーヘッドあり

shim: containerd-shim-kata-v2
config.toml:
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata]
    runtime_type = "io.containerd.kata.v2"

6.4 gVisor

gVisor(runsc):

- ユーザースペースカーネル(Sentry)
- システムコールをインターセプトして再実装
- ホストカーネルの攻撃面を縮小
- ptraceまたはKVMベースで動作
- 一部システムコール未サポート

shim: containerd-shim-runsc-v1
config.toml:
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
    runtime_type = "io.containerd.runsc.v1"

6.5 WebAssembly(Wasm)

Wasmランタイム:

- WebAssemblyバイナリをコンテナとして実行
- Wasmtime、WasmEdgeなどを使用
- 非常に高速な起動時間(ミリ秒単位)
- 最小メモリ使用
- ポータブルバイナリ
- 制限されたシステムアクセス(WASI)

shim: containerd-shim-wasm
config.toml:
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasm]
    runtime_type = "io.containerd.wasm.v1"

6.6 RuntimeClass選択

Kubernetes RuntimeClass連携:

1. RuntimeClassリソース定義:
   apiVersion: node.k8s.io/v1
   kind: RuntimeClass
   metadata:
     name: kata
   handler: kata

2. PodでRuntimeClassを指定:
   spec:
     runtimeClassName: kata
     containers:
       - name: app
         image: nginx

3. containerdがhandlerにマッチするランタイムを選択:
   handler "kata" -> containerd.runtimes.kata設定
   -> containerd-shim-kata-v2を実行

7. ctrとcrictlでContainerとTaskを直接見る

1章の分離は概念の説明ではなく、コマンド出力にそのまま現れます。二つの一覧が別々に存在し、その行数が食い違い得るという事実こそが分離の証拠です。

最初に引っかかるのが名前空間です。containerdはクライアントごとに隔離された名前空間へメタデータを保存し、kubeletが作るものはすべてk8s.io名前空間に入ります。-n k8s.ioを付けずにコマンドを実行すると既定の名前空間を見ることになるので一覧が空になり、コンテナが消えたという誤った結論に至ります。ノード上で調査するとき、このフラグは任意ではなく必須です。

# コンテナ(メタデータ)一覧 — 列は CONTAINER, IMAGE, RUNTIME
ctr -n k8s.io containers list

# Task(実行中のプロセス)一覧 — 列は TASK, PID, STATUS
ctr -n k8s.io tasks list

# このビルドが実際に持つプラグインとスナップショッター
ctr plugins ls

二つの一覧を並べると分離が見えてきます。containersにはあるがtasksには無い項目が、プロセスを持たないメタデータだけのコンテナです。健全なノードでもこの状態は一時的に現れます。コンテナが作られたがまだ開始されていない場合や、プロセスが終了したがまだ削除されていない場合です。問題はこの状態が長く続くときで、終了後の後始末が詰まっているという合図であり、次に見る場所は9章の切り分け順序です。

コンテナを一つ深く覗くときはinfoを使います。2章で見たOCIランタイムスペックが実際にどんな値で埋まったかがここで分かります。

# コンテナレコード全体(イメージ、スナップショットキー、ランタイム、ラベル)
ctr -n k8s.io containers info CONTAINER_ID

# OCIスペックだけを見たいとき
ctr -n k8s.io containers info CONTAINER_ID --spec

# Task内のプロセス一覧とリソース指標
ctr -n k8s.io tasks ps CONTAINER_ID
ctr -n k8s.io tasks metrics CONTAINER_ID

--specで取り出した出力で確認すべき値は決まっています。linux.resourcesに入ったメモリ上限とCPU quotaがPodスペックに書いた値と合っているか、linux.namespacesにどの名前空間があり、どれがサンドボックスと共有されているか、cgroupsPathが期待どおりのパスかです。Podスペックとこの出力が食い違うなら、変換のどこかが介入したということで、その地点はたいていkubeletか6章のランタイムハンドラ設定です。

同じコンテナをcrictlで見ると視点が変わります。ctrはcontainerdのオブジェクトを見せ、crictlはCRIが定義するモデル、つまりPodとコンテナを見せます。Kubernetesの文脈で何かを確認するならcrictlのほうが適切です。コンテナがどのPodに属するか、何回目の再試行かといった情報はctrには存在しません。

crictl ps            # 実行中のコンテナ
crictl ps -a         # 終了したものも含める
crictl inspect CONTAINER_ID
crictl stats

8. 実践例: containerdを再起動してもコンテナは生きている

4章で説明したshimのデーモン化が実際にどんな結果を生むかは、自分で再現してみるのが早道です。ステージングのノード一台で次の順序をそのまま踏みます。

# 1) 基準線 — Task一覧と、そのうち一つのPIDを控えておく
ctr -n k8s.io tasks list

# 2) shimプロセスがいくつ上がっているか数えておく
pgrep -c containerd-shim-runc-v2

# 3) containerdだけを再起動する(kubeletには触らない)
systemctl restart containerd

# 4) もう一度確認 — Taskはそのままで、PIDも変わらない
ctr -n k8s.io tasks list
pgrep -c containerd-shim-runc-v2

3段階でcontainerdプロセスは実際に死んで新しく立ち上がります。ところが4段階の出力は1段階と同じです。TaskのPIDが変わっていないことが核心です。コンテナプロセスの親はcontainerdではなくshimであり、shimは自らをデーモン化してcontainerdとは別のセッションで動くため、containerdが消えても影響を受けません。再起動したcontainerdは各shimのttrpcソケットアドレスを再び見つけて接続し、その時点から状態照会とコマンド伝達が再開されます。

この実験が教えてくれるのは安心ではなく切り分けの順序です。コンテナが落ちたという報告が来たときにcontainerdを再起動しても、たいていは何も直りません。コンテナを実際に抱えているのはshimなので、containerdの再起動は管理プレーンを立て直すだけです。逆にこの性質ゆえの固有の失敗もあります。再接続に失敗すると、コンテナプロセスは正常に動いているのにcontainerdがそれを管理できない状態になります。症状はctr tasks listには出ないのにpgrep containerd-shim-runc-v2ではプロセスが残っているという不一致で、このときはソケットのパスが残っているかとcontainerdログの再接続エラーを確認します。

9. 失敗パターンと切り分けの順序

TaskがStoppedのまま削除されないのが最もよく報告される症状です。crictl ps -aにExitedとして残り、消えず、同じPodが再起動を繰り返します。3章の状態機械を思い出せば、Stoppedは終点ではなくDeleteを待つ状態です。削除が進まない理由はたいてい後始末の段階が詰まったことで、cgroupが消えないかrootfsのアンマウントが失敗するケースです。確認の順序はTaskの状態、残っているマウント、そしてcontainerdログの後始末エラーです。強制的に片付ける必要があるならctr -n k8s.io tasks delete --force CONTAINER_IDがプロセスを殺して削除を進めます。ただしこれはkubeletの知らないところで状態を変える行為なので、切り分けが終わったあとの最終手段としてのみ使うべきです。

二つ目は孤立したshimです。containerdが正常終了ではなく強制終了された後に残り、症状は8章で述べた不一致と同じです。ノードのshimプロセス数が実際のコンテナ数より目に見えて多ければこの状態を疑います。shim一つが抱える資源は大きくありませんが、積み重なるとcgroupとマウントも一緒に残り、最終的に新しいコンテナの生成が失敗します。手で片付けるよりノードを空けて入れ替えるほうがほぼ常に安全です。

三つ目はcgroupドライバの不一致です。コンテナは起動するのに不安定に再起動し、リソース制限が意図どおりにかからず、ノードを入れ替えても再現します。containerdのドキュメント基準で、runcオプションのSystemdCgroupの既定値はconfig version 2とversion 3のどちらもfalseです。ところが最近のディストリビューションのkubeletはおおむねsystemdドライバを使います。つまり既定値のままにすると食い違う組み合わせになりやすいということです。確認は設定ファイルの当該値とkubeletのcgroupドライバ設定を突き合わせることで、ここでも6.2節の設定パスがconfigのバージョンによって異なる点を先に見る必要があります。

# このビルドの既定設定をそのまま取り出して現在のファイルと比べる
containerd config default > /tmp/containerd-default.toml
grep -n -i "systemdcgroup\|runtime_type\|version" /tmp/containerd-default.toml | head

四つ目は存在しないランタイムハンドラです。6.6節のRuntimeClassはKubernetes側の検証を通りますが、kubeletがそのhandler名をCRIへ渡した瞬間、containerdが対応するruntimes項目を見つけられずサンドボックス生成が失敗します。RuntimeClassのhandlerと設定のruntimes配下のキー名は文字単位で一致していなければならず、その項目のruntime_typeに対応するshimバイナリがPATHになければなりません。kataを入れたのにPodが立たないなら、この三つを順に確認するのが最も速い経路です。

五つ目はPaused状態に取り残されたTaskです。3.3節の状態機械のとおり、TaskはPauseで止まり、Resumeでしか解けません。freezer cgroupでプロセスを凍らせたものなので、プロセスは生きておりPIDもそのままですが、何も進みません。アプリケーションから見ると完全に固まったように見え、CPU使用率はゼロでログも止まります。この状態は人がctr tasks pauseを実行したか、チェックポイントの道具が途中で失敗したときに残ります。確認はctr -n k8s.io tasks listのSTATUS列、復旧はctr -n k8s.io tasks resume CONTAINER_IDです。

# STATUS列からPAUSEDを探す
ctr -n k8s.io tasks list

# 実行状態に戻す
ctr -n k8s.io tasks resume CONTAINER_ID

10. これらの機能を使わないほうがよい場合

5章のcheckpoint/restoreは説明だけ聞くと魅力的ですが、実際に使える場所は狭いのが実情です。CRIUが復元するのはプロセスのメモリとファイルディスクリプタの状態であって、その状態が外の世界と結んでいた関係まで復元してくれるわけではありません。開いていたTCP接続の相手は復元を知らず、ファイルロックとセッショントークンは失効しており、データベースのコネクションプールはすでに切れたソケットを抱えています。したがって実際に値打ちを持つのは外部状態への依存がほとんど無いワークロード、たとえば初期化に時間がかかる計算プロセスをあらかじめ温めておいて復元するといった用途です。Kubernetesレベルのコンテナチェックポイント対応はリリースごとに状況が変わってきたので、正確なフィールドは使用中のバージョンのドキュメントで確認してください。

6章の代替ランタイムも同様に既定の選択肢ではありません。KataやgVisorを導入する理由は性能ではなく隔離であり、その隔離を買うために支払う対価は起動時間とメモリと互換性です。その対価はワークロードごとに違うので、他人のベンチマークではなく自分のワークロードで測る必要があります。測りもせずにクラスタ全体の既定ランタイムを変えるのが最悪の選択で、マルチテナントの区画や信頼できないコードを実行する名前空間だけにRuntimeClassで付けるのが出発点として妥当です。Wasmは隔離よりも起動時間と可搬性を買う選択であり、システムアクセスが制限されるため既存のコンテナイメージをそのまま移せる対象ではありません。

最後に、ctr自体が運用の道具ではないという点を押さえておく必要があります。ctrはcontainerd開発者のためのデバッグクライアントであり、安定したインターフェースであるという約束はありません。状態を読む用途では優秀ですが、コンテナを作ったり消したりする用途に使うとkubeletが知る世界と実際の世界が食い違います。kubeletはその不一致を見つけると自ら復旧を試みますが、その復旧が人の意図と同じである保証はありません。Kubernetesのノードでは、読むのはctrとcrictl、書くのはkubectlという境界を守るほうが安全です。

11. 参考資料

12. まとめ

containerdのコンテナライフサイクル管理はContainer(メタデータ)とTask(実行)の分離、shimによるプロセス分離、多様なランタイムクラスサポートが核心です。shimのデーモン化設計によりcontainerd再起動でもコンテナが維持され、OCIランタイムスペックベースの標準化されたインターフェースでrunc、Kata、gVisor、Wasmなど多様なランタイムを統合します。

コメント

まだコメントはありません。

ログインするとコメントできます