DUICUO

Docker、Containerd、RunC とは何かを簡単に分析します。

[[431465]]

RunCとは何ですか?

前回の記事「コンテナを実際に動作させるためのツール:runcとOCI仕様の徹底理解」では、runcとOCIについて既に説明しました。ここでは、その概念を改めて確認します。

Docker、Google、CoreOS などのベンダーが Open Container Initiative (OCI) を作成しました。OCI には現在、コンテナ ランタイム仕様とコンテナ イメージ仕様という 2 つの主要な標準ドキュメントがあります。

コンテナランタイムのOCI標準は、主にコンテナの実行状態と、ランタイムが提供する必要のあるコマンドを規定しています。次の図は、コンテナの状態遷移を示しています。

  • init state: これは私が独自に追加した状態であり、標準には存在しません。コンテナが存在しない初期状態を表します。
  • 作成中: コンテナは create コマンドを使用して作成されます。このプロセスは作成中と呼ばれます。
  • `created`: コンテナは作成されましたが、まだ実行されていません。これは、イメージと設定が正しく、コンテナが現在のプラットフォームで実行できることを示しています。
  • running: コンテナの実行状態。コンテナ内のプロセスは稼働状態にあり、ユーザー定義のタスクを実行しています。
  • 停止: コンテナは、実行が終了した後、エラーが発生した後、または「stop」コマンドによって停止された後に一時停止状態になります。この状態では、コンテナはプラットフォーム内に多くの情報を保持しており、完全に削除されていません。

ランクの起源

Dockerのlibcontainerから移行されたRunCは、コンテナの起動/停止やリソースの分離といった機能を実装しています。Dockerは、OCIコンテナランタイム標準のリファレンス実装としてRunCをOCIに寄贈しました。Dockerはデフォルトでdocker-runc実装を提供しています。実際、containerdラッパーを使用することで、Dockerデーモンの起動時にRunC実装を指定できます。当初、人々はDockerのOCIへの貢献について混乱していました。Dockerが貢献したのはコンテナを「実行」するための標準的な方法だけで、それ以上のものではありませんでした。イメージフォーマットやレジストリのプッシュ/プルフォーマットは含まれていませんでした。Dockerコンテナを実行する際、Dockerは実際には以下の手順を実行します。

  • 画像をダウンロード
  • イメージ ファイルをバンドル ファイルに解凍するか、ファイル システムを複数のレイヤーに分割します。
  • バンドルファイルからコンテナを実行する

Dockerの標準化は、まだ3番目のステップに過ぎません。それ以前は、コンテナランタイムはDockerがサポートするすべての機能をサポートしていると誰もが考えていました。最終的にDockerは、元のOCI仕様では「実行中のコンテナ」部分のみがランタイムを構成すると明記されていたことを明確にしました。この「概念的な乖離」は今日まで続いており、「コンテナランタイム」というトピックは紛らわしいものとなっています。この記事では、どちらの側も完全に間違っているわけではないことを示せるよう、この用語を広く使用していきます。RunCは、このOCIドキュメントに準拠したコンテナを作成できます。これは標準であるため、KataやgVisorなど、OCI準拠のコンテナランタイムである他のOCI実装も確かに存在します。

runcの使い方

  1.  バンドルを作成する
  2. $ mkdir -p /mycontainer/rootfs
  3.  
  4. # Dockerを悪用しルートファイルシステムバンドルコピーする
  5. $ docker エクスポート $(docker ビジーボックスを作成) | tar -C /mycontainer/rootfs -xvf -
  6.  
  7. #仕様を作成する デフォルトではshがコンテナエントリポイントになります
  8. $ cd /mycontainer
  9. $ runc スペック
  10.  
  11. # コンテナを起動する
  12. $ sudo -i
  13. $ cd /mycontainer
  14. $ runc 実行 mycontainerid
  15.  
  16. # コンテナを一覧表示する
  17. $ 実行リスト
  18.  
  19. # コンテナを停止する
  20. $ runc kill mycontainerid
  21.  
  22. # 掃除
  23. $ runcコンテナIDを削除します

コマンドラインで `runc` を使用すると、必要に応じて任意の数のコンテナを起動できます。しかし、このプロセスを自動化するには、コンテナマネージャが必要です。なぜでしょうか?数十ものコンテナを起動し、その状態を追跡する必要があると想像してみてください。これらのコンテナの一部は障害発生時に再起動する必要があり、終了時にはリソースを解放する必要があり、レジストリからイメージを取得する必要があり、コンテナ間ネットワークを構成する必要もあります。これには低レベルと高レベルの両方のコンテナランタイムが必要であり、 `runc` は低レベル実装の実装です。

低レベルおよび高レベルのコンテナランタイム

コンテナランタイムといえば、runc、lxc、lmctfy、Docker(コンテナ)、rkt、crio-o など、様々な例が思い浮かぶかもしれません。これらはそれぞれ異なる目的で構築され、異なる機能を実装しています。containerd や crio-o のように、コンテナの実行に runc を使用し、イメージ管理と API を高レベルで実装しているものもあります。これらの機能(イメージの転送、管理、展開、API など)は、runc の低レベル実装と比較すると、高レベル機能と言えるでしょう。この点を踏まえると、コンテナランタイムの領域が非常に複雑であることがわかります。各ランタイムは、この低レベルから高レベルまでのスペクトルの異なる部分をカバーしています。これは非常に主観的な図です。

したがって、実際には、実行中のコンテナのみに焦点を当てたランタイムは通常「低レベルコンテナランタイム」と呼ばれ、より高度な機能(イメージ管理やgRPC/Web APIなど)をサポートするランタイムは通常「高レベルコンテナランタイム」と呼ばれます。ここでは、これらを「高レベルコンテナランタイム」、または単に「コンテナランタイム」と呼びます。低レベルコンテナランタイムと高レベルコンテナランタイムは、根本的に異なる問題に対処することに注意することが重要です。

  • 低レベルコンテナランタイム:コンテナは、Linuxの名前空間とcgroupを用いて実装されます。名前空間は、ファイルシステムやネットワークなどの仮想システムリソースを各コンテナに提供することを可能にし、cgroupはメモリやCPU使用量など、各コンテナが使用できるリソースを制限する手段を提供します。最下層ランタイムでは、コンテナランタイムはコンテナの名前空間とcgroupを作成し、それらの中でコマンドを実行する役割を担います。低レベルコンテナランタイムは、コンテナ内でこれらのオペレーティングシステム機能を使用することをサポートします。現在、低レベルコンテナランタイムには以下のものがあります。runc:Dockerに代表される、最も広く利用されているコンテナランタイムです。runv:runVはハイパーバイザーベースのランタイムです。ゲストカーネルを仮想化することでコンテナをホストから分離し、境界を明確にすることで、ホストとコンテナの両方のセキュリティを容易に強化できます。代表的な実装としては、KataとFirecrackerがあります。runsc:runsc = runc + safetyで、代表的な実装としてはGoogleのgvisorがあります。gvisorは、アプリケーションのすべてのシステムコールをインターセプトすることで、軽量かつ安全に分離されたコンテナランタイムサンドボックスを提供します。現時点では、本番環境でのユースケースはまだ見つかっていないようです。Wasm:Wasmのサンドボックス機構は、Dockerよりも優れた分離性とセキュリティを提供します。ただし、Wasmコンテナはまだドラフト段階であり、本番環境への導入には程遠い状況です。
  • 高レベルコンテナランタイム:コンテナを実行したい開発者は、通常、低レベルコンテナランタイムが提供する機能だけでは不十分です。イメージフォーマット、イメージ管理、イメージ共有に関連するAPIや機能も必要になりますが、これらは一般的に高レベルコンテナランタイムによって提供されます。日常的な使用においては、低レベルコンテナランタイムが提供する機能ではニーズを満たせない場合があります。そのため、低レベルコンテナランタイムを実際に使用するのは、高レベルコンテナランタイムとコンテナツールを実装する開発者だけです。低レベルコンテナランタイムを実装する開発者は、containerdやcri-oのような高レベルコンテナランタイムは、コンテナ実行の実装をruncにアウトソーシングしているため、真のコンテナランタイムとは異なると言うかもしれません。しかし、ユーザーの観点から見ると、これらはコンテナ機能を提供する単なる個別のコンポーネントであり、別の実装に置き換えることができるため、この観点からはランタイムと呼ぶことには依然として意味があります。containerdとcri-oはどちらもruncを使用していますが、サポートされる機能が大きく異なる、全く異なるプロジェクトです。 dockershim、containerd、cri-o はすべて CRI 準拠のコンテナ ランタイムであり、高レベル ランタイムと呼ばれます。

Kubernetes は、containerd などの高レベルコンテナランタイムをサポートするだけで済みます。containerd は、OCI 仕様に従って、汎用 runc、セキュリティ強化された gvisor、より独立した runv など、さまざまな低レベルコンテナランタイムとインターフェースします。

コンテナ

RunCと同様に、Dockerのオープンソース製品であるcontainerdも、かつてはオープンソースのDockerプロジェクトの一部であったことがわかります。containerdもまた、自己完結型のソフトウェアです。

  • コンテナランタイムを自称していますが、ランタイム__RunC_とは異なります。_containerd_と_runc_は役割が異なるだけでなく、組織構造も異なります。_runc_は単なるコマンドラインツールであるのに対し、_containerd_は長時間稼働するデーモンであることは明らかです。_runc_のインスタンスは、基盤となるコンテナプロセスを超えることはできません。通常、_runc_はcreate呼び出し時にライフサイクルを開始し、コンテナのルートファイルシステム内の指定されたファイルに対して実行されます。
  • 一方、`_containerd_` は数千個の `_runc_` コンテナを管理できます。これはサーバーのように機能し、コンテナの起動、停止、ステータス報告などのリクエストをリッスンします。`_containerd_` は内部的には RunC を使用しています。しかし、`_containerd_` は単なるコンテナライフサイクルマネージャーではありません。イメージ管理(レジストリからのイメージのプルとプッシュ、ローカルへのイメージの保存など)、コンテナ間のネットワーク管理、その他多くの機能も処理します。

containerd は、シンプルさ、堅牢性、移植性を重視した業界標準のコンテナ ランタイムです。containerd は次のタスクを処理できます。

  • コンテナのライフサイクルの管理(コンテナの作成から破棄まで)
  • コンテナイメージのプル/プッシュ
  • ストレージ管理(イメージとコンテナデータのストレージの管理)
  • runc を呼び出してコンテナを実行します (runc などのコンテナ ランタイムと対話します)。
  • コンテナのネットワークインターフェースとネットワークの管理

上の図は、Containerdの全体的なアーキテクチャを示しています。下から上に向かって、ContainerdはLinux、Windows、ARMなどのプラットフォームなどのオペレーティングシステムとアーキテクチャをサポートしています。これらの基盤となるオペレーティングシステム上で実行されるのは、前述のruncやgVisorなどの基盤となるコンテナランタイムです。基盤となるコンテナランタイムの上には、Containerdランタイム、コア、API、バックエンド、ストア、メタデータなどのContainerd関連コンポーネントがあります。これらのContainerdコンポーネント上に構築され、それらと対話するのがContainerdクライアントです。KubernetesがCRIを介してContainerdと対話する場合、KubernetesはContainerdクライアントとしても機能します。Containerd自体はctrと呼ばれるCRIを提供していますが、このコマンドラインツールはあまりユーザーフレンドリーではありません。

これらのコンポーネントの上には、Google Cloud、Docker、IBM、Alibaba Cloud、Microsoft Cloud、RANCHER などの実際のプラットフォームがあります。これらのプラットフォームはすでに containerd をサポートしており、一部のプラットフォームではすでに containerd をデフォルトのコンテナ ランタイムにしています。

Kubernetes の観点から見ると、ランタイム コンポーネントとして containerd を選択すると、呼び出しチェーンが短くなり、コンポーネントが少なくなり、安定性が向上し、ノード リソースの消費量が削減されます。

ドッカー

2013年にリリースされたDockerは、開発者がコンテナをエンドツーエンドで実行する際に直面する多くの問題を解決しました。Dockerに含まれる機能は以下のとおりです。

  • コンテナイメージ形式
  • コンテナイメージを構築する方法 (Dockerfile/docker build)。
  • コンテナイメージ(Docker イメージ、Docker rm など)を管理する方法。
  • コンテナインスタンスを管理する方法 (docker ps、docker rm など)。
  • コンテナイメージを共有する方法 (docker push/pull)
  • コンテナを実行する 1 つの方法 (docker run)。

当時のDockerはモノリシックなシステムでした。しかし、その機能はどれも真に相互依存していませんでした。それぞれの機能は、より小規模で集中化されたツールに実装され、連携して利用できました。各ツールは、コンテナ標準という共通フォーマットを用いて連携することができました。Docker 1.11以降、DockerデーモンはOCI標準に準拠するために複数のモジュールに分割されました。この分割後、構造は以下の部分で構成されました。

このうち、containerd はコンテナのランタイムとライフサイクル (作成、起動、停止、中止、シグナル処理、削除など) を独立して担当し、イメージの構築、ボリューム管理、ログ記録などの他のタスクは Docker デーモンの他のモジュールによって処理されます。

Docker のモジュールはオープン スタンダードを取り入れており、OCI の標準化を通じてコン​​テナー テクノロジが急速に発展することを期待しています。

Dockerコンテナを作成する際、Dockerデーモンはコンテナを直接作成しなくなり、代わりにcontainerdにコンテナの作成を要求します。containerdは要求を受信すると、コンテナを直接操作するのではなく、containerd-shimというプロセスを作成します。このプロセスがコンテナを操作します。コンテナプロセスには、状態の収集、標準入力の維持、ファイル記述子(fds)のオープンを処理する親プロセスが必要であることを指定します。この親プロセスがcontainerdである場合、containerdがクラッシュすると、ホストマシン上のすべてのコンテナが終了しなければなりません。shimとしてcontainerd-shimを導入することで、ライブリストア機能が提供され、この問題を回避できます。systemdのMountFlagsがslaveに設定されていることに注意してください。

コンテナを作成するには、名前空間と cgroup を構成し、ルート ファイル システムをマウントする必要があります。その後、`runc` はこの OCI ドキュメントに従って準拠したコンテナを作成できます。

コンテナは実際にはcontainerd-shimがruncを呼び出すことで起動されます。runcはコンテナを起動した後、すぐに終了します。containerd-shimはコンテナプロセスの親プロセスとなり、コンテナプロセスのステータスを収集してcontainerdに報告し、コンテナ内のpid 1のプロセスが終了した後にコンテナ内の子プロセスを引き継いでクリーンアップを行い、ゾンビプロセスが発生しないようにします。containerd、containerd-shim、そしてコンテナプロセス(つまりコンテナのメインプロセス)の3つのプロセスは相互に依存しています。ライブリストア機能がどのように保証されているかについては、「containerd、containerd-shim、runc間の依存関係」[1]を参照してください。

参照

https://www.ianlewis.org/en/container-runtimes-part-1-introduction-container-r

https://iximiuz.com/en/posts/コンテナ化からオーケストレーションへの旅/#コンテナ管理

https://github.com/moby/moby/issues/35873#issuecomment-386467562

[1]https://fankangbest.github.io/2017/11/24/containerd-containerd-shim%E5%92%8Crunc%E7%9A%84%E4%BE%9D%E5%AD%98%E5%85%B3%E7%B3%BB/

この記事はWeChat公式アカウント「運営と発展の物語」から転載したものです。