この記事では、様々なPodやコンテナのデザインモードについて詳しく説明します。
著者:Alibaba Cloud Container Platformのシニアテクニカルエキスパート、CNCFアンバサダー、Zhang Lei氏
1) Podが必要な理由
コンテナを支えるコンセプト
スケジューリングの原子単位であるPodは、Kubernetesにおいて非常に重要な概念です。しかし、この概念はDockerコンテナを使用する際には適用できません。Podを理解するためには、まずコンテナを理解することが重要です。
コンテナとは、隔離された表示と制限されたリソースを持つプロセスのことです。
コンテナ内のPID(Process ID)1のプロセスは、アプリケーションそのものです。このような場合、仮想マシンの管理はインフラの管理に相当します。一方、コンテナの管理はアプリケーションの管理に相当します。これはimmutable infrastructureのベストプラクティスの一つです。
先の例を踏まえて、Kubernetesとは一体何か、考えてみましょう。多くの人が、Kubernetesはクラウド時代のOSだと言っています。これが事実であれば、コンテナイメージはOSのソフトウェアインストールパッケージであると考えられます。
実際のOSの例
HelloWorld というアプリケーションは、プロセス群から構成されています。ここで、プロセスはLinuxのスレッドに相当することに注意してください。
Linux のスレッドは軽量プロセスです。Linux の HelloWorld の pstree を見ると、HelloWorld が api、main、log、compute の 4 つのスレッドで構成されていることがわかります。これらのスレッドは互いに協力してHelloWorldのリソースを共有しています。
以上が、OSにおけるプロセスグループまたはスレッドグループの実例です。
実際のOSでは、プログラムはプロセスグループによって管理されます。Kubernetesは、LinuxなどのOSのようなものです。コンテナは、Linuxのスレッドのようなプロセスに例えられます。さらにPodは、Linuxでいうところのプロセスグループやスレッドグループにあたります。
プロセスグループの概念
プロセスグループの概念を理解する前に、プロセスの概念を理解しておきましょう。
先ほどの例では、HelloWorld は 4 つのプロセスで構成されており、それらはいくつかのリソースやファイルを共有しています。では、HelloWorld をコンテナ内で実行するにはどうすればよいのでしょうか。
最も一般的な方法は、4つのプロセスを実行するDockerコンテナを起動することです。この場合、コンテナ内のどのプロセスがPID1であるかを知ることが重要です。例えば、メインプロセスのPIDが1の場合、他の3つのプロセスの管理はどのプロセスが担当するのでしょうか?
コンテナはシングルプロセスモデルとして設計されています。しかし、これはコンテナ内で1つのプロセスしか起動しないという意味ではありません。コンテナ内のアプリケーションはプロセスであるため、PID1のプロセスのみが管理され、他の起動したプロセスはホストされます。したがって、サービスアプリケーションのプロセスは、当然、プロセス管理機能を持っています。
例えば、HelloWorldはシステム能力を持っているか、コンテナ内のPID1のプロセスを直接systemdに変更しています。そうしないと、アプリケーションやコンテナは複数のプロセスを管理できません。万が一、ユーザーがPID1のプロセスを殺してしまったり、実行中にクラッシュしてしまった場合、他の3つのプロセスのリソースを取り戻す責任は誰にもなく、深刻な問題となります。
逆に、アプリケーションをsystemdに変更したり、コンテナ内でsystemdを動作させる場合は、コンテナの管理がアプリケーションの管理ではなく、systemdの管理になります。例えば、コンテナ内でsystemdを動かした場合、アプリケーションは終了するのでしょうか?例外や障害は発生しないのでしょうか?実は、コンテナがsystemdを管理しているため、その答えを知ることはできません。これが、複雑なプログラムをコンテナで動かすのが難しい理由のひとつです。
結論から言うと、コンテナは実際にはシングルプロセスモデルです。そのため、コンテナ内で複数のプロセスを起動しても、PIDが1のプロセスだけが存在します。このプロセスが失敗したり終了したりすると、他のプロセスはオーファンプロセスとなり、そのリソースを取り戻すことができません。
Linuxコンテナのシングルプロセスモデルでは、コンテナのライフサイクルは、PID1のプロセス、つまりコンテナアプリケーションプロセスと同じです。これは、コンテナ内に複数のプロセスを作成できないという意味ではありません。一般的に、コンテナアプリケーションプロセスはプロセスを管理することができません。そのため,exec や SSH を用いてコンテナ内に作成された他のプロセスは,SSH 終了などの予期せぬ終了時にオーファンプロセスとなる可能性があります。
別の方法として,コンテナ内で systemd を実行して他のプロセスを管理することもできます。この場合、アプリケーションはsystemdに引き継がれるため、直接管理することはできません。そのため、アプリケーションのライフサイクルとコンテナのライフサイクルは異なります。管理モデルはかなり複雑です。
プロセスグループとしてのPod
PodはKubernetesで抽象化されたもので、プロセスグループに似ています。
前述の通り、HelloWorldアプリケーションは4つのプロセスで構成されています。このアプリケーションは、Kubernetesでは4つのコンテナを持つPodとして定義されています。この概念は非常に重要です。
今回のケースでは、互いに協調して異なる機能を実装する4つのプロセスは、コンテナで実行する必要があります。Kubernetesでは、先の2つの問題を防ぐために、それらを異なるコンテナに配置しています。では、これらのプロセスはKubernetes上でどのように実行されるのでしょうか。4つのプロセスは、4つの独立したコンテナで別々に起動されるが、同じPodに定義されています。
KubernetesがHelloWorldを起動すると、Podのリソースの一部を共有する4つのコンテナが存在します。したがって、Kubernetesでは、Podはあくまでも論理的な単位であり、物理的な実体には対応していません。その代わり、物理的に存在するのは4つのコンテナです。この4つのコンテナ、あるいは複数のコンテナを組み合わせたものを「Pod」と呼びます。Pod内のコンテナはいくつかのリソースを共有する必要があるため、PodはKubernetesにおけるリソース割り当ての単位であると理解してください。したがって、PodはKubernetesにおけるスケジューリングの原子単位でもあります。
前述のPodデザインは、Kubernetesから派生したものではなく、GoogleのエンジニアがBorgを開発しているときに発見されたものだといいます。これについては、Borgの論文に詳しく書かれています。要するに、Googleのエンジニアは、Borgでアプリケーションをデプロイする際に、多くのシナリオでプロセスとプロセスグループの関係に似た関係を発見しました。具体的には、これらのアプリケーションは密接に連携しているため、同じサーバーにデプロイして特定の情報を共有しなければなりません。
スケジューリングの原子単位としてPodを使う理由
Podがプロセスグループであることを理解した上で、次のような疑問を持つ人もいるでしょう。
- なぜPodが概念として抽象化されているのか?
- Podを使わずにスケジューリングすることで問題を解決することは可能か?
- なぜKubernetesではPodがスケジューリングの原子単位でなければならないのか?
これらの疑問に答えるために、次のような例を考えてみましょう。
2つのコンテナは互いに密接に連携しています。そのため、同じPodにデプロイする必要があります。具体的には、1つ目のコンテナはAppという名前で、ビジネスコンテナであり、ログファイルを生成します。もう 1 つのコンテナは LogCollector という名前で、App コンテナが生成したログファイルをバックエンドの ElasticSearch に転送します。
Appコンテナには1GBのメモリ、LogCollectorコンテナには0.5GBのメモリが必要です。現在のクラスタ環境では、Node_Aで1.25GB、Node_Bで2GBのメモリが利用可能です。
この場合、Pod定義が利用できなかったらどうなるでしょうか?答えは、両方のコンテナが同じサーバー上で密接に連携することになります。スケジューラが最初に App コンテナを Node_A にスケジュールした場合、リソースが不足しているため LogCollector コンテナを Node_A にスケジュールできないことに注意してください。この場合、アプリケーション全体に不具合が生じ、スケジューリングは失敗します。
前述の例は、タスクコスケジューリングの失敗の典型的なケースです。この問題はさまざまな方法で解決できます。
例えば、Mesosはリソースホーディングを実装しています。これは、親和性制約を持つすべてのタスクが揃ってから統一スケジューリングを開始することを意味します。これは、この問題に対する典型的な解決策です。
したがって、MesosのAppコンテナとLogCollectorコンテナは、両方のコンテナが投入されたときにのみ、一元的にスケジューリングされます。しかし、これには新たな問題も発生します。まず、待ち時間が発生するため、スケジューリング効率が低下します。さらに、お互いの待ち時間が原因でデッドロックが発生してしまいます。これらの問題をMesosで解決するのは非常に複雑です。
もう一つのソリューションは、Googleの楽観的スケジューリングで、Borgの次世代システムであるOmegaシステムでは、非常に複雑で便利なソリューションです。例えば、スケジューリングはコンフリクトに関係なく実行され、そのコンフリクトをロールバックによって解決するために、よく設計されたロールバックメカニズムが用意されています。この方法は、より優雅で効率的ですが、その実装メカニズムは非常に複雑です。悲観的なロックの構成は楽観的なロックの構成よりも単純であることはよく知られています。
タスクの共同スケジューリング問題は、KubernetesのPodを使うことで直接解決されます。Kubernetesでは、AppコンテナとLogCollectorコンテナは同じPodに属し、Pod内でスケジューリングされるため、タスクの共同スケジューリング問題は発生しません。
Podとは?
このセクションでは、Podについてさらに理解を深めることができます。Pod内のコンテナは、お互いに超親和性を持ちます。通常、親和性の問題はスケジューリングによって解決されます。
例えば、2つのPodが同じホスト上で動作する必要があります。この場合、両方のPodには親和性があり、スケジューラは親和性問題を解決します。しかし、親和性の問題を解決するのはPodだけです。親和性がないと、すべてのPodやアプリケーション全体を起動することができません。
さて、親和性とは何でしょうか。それは、以下のように分けられます。
- プロセス間でファイルをやりとりします。先の例では、一方のプロセスがログを書き、もう一方のプロセスがログを読んでいます。
- どちらのプロセスも、localhostまたはローカルソケットを介して、お互いに通信する必要があります。このローカルな通信は、親和性と呼ばれています。
- コンテナもマイクロサービスも、頻繁にRPCを呼び出す必要があります。この場合、パフォーマンスを向上させるために、両者の間に親和性が必要となります。
- 2つのコンテナまたはアプリケーションは、いくつかのLinux名前空間を共有する必要があります。たとえば、あるコンテナを別のコンテナのネットワーク名前空間に追加すると、後者のコンテナのネットワークデバイスとネットワーク情報を表示できます。
前述の関係はすべて、Kubernetesでpodを使用することで解決します。
なぜpodが必要なのかを結論づけるためには、podが以下のことに役立つと理解することが重要です。
- 親和性について説明する
- 親和性を持つコンテナやビジネスを集中的にスケジューリングする
2)Pod導入の仕組み
Podが解決する課題
Podは論理的な概念である。ここでは、サーバーにPodを実装する方法を説明します。
このケースの核心は、Pod内の複数のコンテナ間で特定のリソースやデータを効率的に共有することです。
コンテナは、Linux の名前空間とコントロールグループ(cgroups)によって分けられています。したがって、この分離を取り除き、コンテナ間でリソースやデータを共有することが、Pod設計の大きな関心事となっています。
具体的なソリューションとしては、ネットワークとストレージの2つの部分があります。
(1)ネットワークの共有
まず、Pod内の複数のコンテナがネットワークを共有する方法を考えます。
例えば、PodにはコンテナAとコンテナBが入っていて、ネットワーク・ネームスペースを共有する必要があるとします。Kubernetesでは、Podのネットワーク名前空間を共有するために、各Podにインフラストラクチャコンテナを作成します。
インフラストラクチャコンテナは、100~200KBのサイズの小さなイメージです。これは、アセンブリ言語で書かれた恒久的に休止するコンテナです。他のすべてのコンテナは、インフラストラクチャコンテナのネットワーク・ネームスペースに参加します。
したがって、ネットワークビューはPod内のすべてのコンテナと同じです。つまり、ネットワークデバイス、IPアドレス、MACアドレスなどのネットワーク関連の情報は、Pod内のすべてのコンテナで同一です。ここでは、ネットワーク関連の情報は、Podに初めて作成されたインフラストラクチャ・コンテナのものです。これがPodでのネットワーク共有の実装方法です。
Podは、PodのネットワークネームスペースのIPアドレスであり、インフラストラクチャコンテナのIPアドレスでもある1つのIPアドレスしか持ちません。一方、その他のネットワークリソースはすべて同一で、各Podのすべてのコンテナで共有されます。このように、Podでは共有が実装されています。
この場合、中間コンテナが必要になります。そのため、Pod内のインフラストラクチャ・コンテナを最初に起動する必要があります。また、Podのライフサイクルはインフラストラクチャコンテナと同じですが、コンテナAとBには無関係です。このため、KubernetesではPod内の1つのイメージを更新することができます。
(2) ストレージの共有
コンテナがPod内のストレージを共有する方法は何でしょう?これは非常に簡単です。
例えば、PodにはNginxコンテナとCommonコンテナが入っているとします。Nginx から Nginx コンテナのファイルにアクセスするためには、ディレクトリの共有が必須です。Podでのファイルやディレクトリの共有は簡単で、実際にはボリュームをPodレベルにシフトすることで行われます。Pod内のすべてのコンテナは、すべてのボリュームを共有します。
前述の図に示すように、ボリュームはshared-dataと名付けられ、Podレベルに配置されています。shared-dataボリュームは、コンテナにマウントされた後、コンテナによって共有されます。これが、KubernetesがPod内のコンテナのストレージを共有する仕組みです。
前述の例では、Appコンテナがボリュームにログを書き込んでいます。このボリュームは、コンテナにマウントされた後、LogCollectorコンテナにすぐに表示されます。これがPodによるストレージ共有の実装です。
3)コンテナのデザインパターン
では、なぜPodが必要なのか、Podがどのように実装されているのかを理解した上で、Kubernetesが提唱するコンテナデザインモードについて深掘りしてみよう。
例
例えば、Javaで書かれたアプリケーションを公開するためには、Tomcatのweb appsディレクトリにWARパッケージを保存し、アプリケーションを起動する必要があります。これを実現するにはいくつかの方法があります。
方法1:WARパッケージとTomcatをイメージにパックします。しかし、イメージには実際には2つのオブジェクトが含まれています。この場合、WARパッケージを更新するにしても、Tomcatを更新するにしても、イメージの再作成が必須となります。
方法2:Tomcatのみをイメージにパックします。イメージにはTomcatだけが含まれていますが、ホストからのWARパッケージをTomcatコンテナ内のwebappsディレクトリにマウントするために、hostPathなどのデータボリュームを使用する必要があります。このようにして、コンテナは起動後に使用されます。
ただし、コンテナは状態が変化するマイグレーション可能なものであるため、この方法では分散ストレージシステムを整備します。コンテナはマイグレーションが可能であり,ステータスが変化するため,1回目はホストAで起動し,2回目はホストBで起動することがあります。そのため、コンテナがホストAであろうとBであろうと、WARパッケージを見つけられるような分散ストレージシステムが必要となります。
ボリュームのような分散ストレージシステムがあっても、WARパッケージはボリュームの中に維持します。例えば、Kubernetesのボリュームプラグインを開発し、Podが起動する前にアプリケーションの起動に必要なWARパッケージをボリュームにダウンロードしておきます。そうすれば、WARパッケージをマウントして利用することができます。
この操作は複雑で、コンテナはボリューム内のWARパッケージのコンテンツを管理するために、永続ストレージ・プラグインに依存する必要があります。
Init Container
ローカルのKubernetesインスタンスに分散型ストレージがない場合でも、アプリケーションを利用したりリリースしたりするためには、より一般的な方法が必要となります。
Kubernetesでは、Init Containerがまさにその方法です。
例えば、前回のYAMLファイルでは、WARパッケージをイメージからボリュームにコピーするためだけのInit Containerが定義されています。操作の後、Init Containerは終了します。そのため,Init Containerはコンテナよりも先に起動し,厳密な順序で実行されます。
WARパッケージのコピー先であるappディレクトリは、実際にはボリュームです。前述の通り、Podには複数のコンテナが含まれており、ボリュームを共有しています。そのため、TomcatコンテナにはTomcatのイメージだけがパッケージされています。ただし、起動前にコンテナはappディレクトリを自分のボリュームとして宣言し、パッケージをwebappsディレクトリにマウントする必要があります。
このとき、データをコピーするためにInit Containerが実行されているので、ボリュームにはアプリケーションのsample.war
パッケージが含まれている必要があります。Tomcatコンテナを起動してボリュームをマウントしながら、コンテナ内のsample.war
パッケージを探します。
そのため、Podは自己完結型であり、世界中のどのKubernetesインスタンスでも起動することができます。また、分散型ストレージシステムが存在するかどうか、ボリュームが永続的かどうかに関わらず、Podは解放されます。
この典型的な例では、異なる役割を果たす2つのコンテナを組み合わせて、Pod内のInit Containerに従ってアプリケーションをパッケージ化しています。これは、Kubernetesにおける典型的なコンテナ設計モードで、Sidecarと名付けられています。
Sidecar
Sidecarは、メインのビジネスコンテナのために特定の補助的な作業を行うために、Pod内にいくつかの特別なコンテナを定義することを意味します。前述の例では、Init Containerは、Tomcatコンテナ用の共有ディレクトリにイメージからWARパッケージをコピーするだけのSidecarです。
このSidecarは他にもいくつかの処理を行います。
- スクリプトの作成や条件の事前設定など、コンテナ内のSSHで実行する作業の一部は、Init ContainerやSidecarで完結します。
- また、典型的な例として、ログ収集があります。ログ収集は、プロセスであると同時にコンテナでもあり、Podにパッケージ化して収集します。
- もう一つの重要なアプリケーションは、Debugアプリケーションです。このアプリケーションは、podの名前空間を実行するために、pod内に小さなコンテナを定義します。
- また、Sidecar は他のコンテナの動作状態をチェックします。SSH でコンテナにログオンする必要はありません。その代わり、監視コンポーネントを小さなコンテナにインストールし、そのコンテナをSidecarとして起動することで、メインのビジネスコンテナと連携します。この場合、ビジネスモニタリングはSidecarによって実装されます。
この方法の明らかな利点は、実際にビジネスコンテナから補助機能を切り離し、Sidecarコンテナを独立してリリースすることができることです。さらに重要なのは、この機能を再利用できることです。つまり、モニタリング用のSidecarやログ用のSidecarを社内で共有することができるのです。これがデザインモードの強みです。
Sidecar - アプリケーションとログ収集
このセクションでは、Sidecar の他のアプリケーションシナリオを紹介します。
例えば、アプリケーションのログ収集の場合、ビジネスコンテナはPod内で共有されているボリュームにログを書き込みます。これにより、ログコンテナ(Sidecarコンテナ)はボリュームを共有し、ログファイルを読み込んだ後、リモートストレージに格納したり、別のケースに転送したりすることができます。業界で一般的に使われているFluentdのログプロセスやログコンポーネントについても、基本的には同じように動作します。
Sidecar - Proxy Container
Sidecarは、Proxy Containerとしても機能します。Proxy Containerとは?
Podが外部のシステムや外部のサービスにアクセスする必要があるが、外部のシステムはクラスタであるとします。この場合、1つのIPアドレスですべてのクラスターにアクセスするにはどうすればよいでしょうか?解決策としては、これらのクラスターのアドレスを記録するコードを修正します。あるいは、デカップリング手法であるSidecarプロキシコンテナを使用します。
この方法では、外部のサービス・クラスターに接続するための小さなプロキシを開発し、プロキシのIPアドレスのみを公開します。この場合、ビジネス・コンテナがプロキシに接続し、プロキシがサービス・クラスターに接続します。Pod内のコンテナは、同じネットワークネームスペースとネットワークビューを共有しているため、パフォーマンスが低下することなく、localhostを介して直接通信します。
そのため、Proxy Containerはデカップリングの際にパフォーマンスを損なうことはありません。さらに重要なのは、このProxy Containerナのコードが全社的に再利用されていることです。
Sidecar - Adapter Container
Adapter Containerは、Sidecar の 3 番目のデザインモードです。
例えば、公開されているビジネス API はフォーマット A であるが、API のフォーマット B しか認識しない外部システムがビジネスコンテナにアクセスする必要があるとします。この場合、ビジネスコンテナのコードを変更する必要があります。実際には、アダプタを使用して変換を完了させます。
例えば、ビジネス・コンテナの公開されている監視APIは/metrics
であり、コンテナの/metrics
URLにアクセスすることでAPIにアクセスすることができます。しかし、監視システムがアップグレードされ、そのアクセスURLは/healthz
となっています。この場合、/metrics
URLではなく/healthz
URLのみが公開されるため、モニタリングは利用できません。この問題に対処するには、コードを修正するか、/healthz
宛てのリクエストを /metrics
に転送するアダプタを開発します。そうすれば、アダプタが/healthz
のURLを外部システムに公開するので、ビジネスコンテナは引き続き動作します。
ここでポイントとなるのは、Pod内のコンテナ同士が、パフォーマンスを落とすことなく、localhostを介して直接通信することです。また、このアダプターコンテナは、社内で再利用できるかもしれません。これらはすべて、デザインモードの利点です。
概要
以下は、この記事を明快にまとめたものです。
- Podは、Kubernetesのコンテナデザインモードを実装するための中核となるメカニズムです。
- コンテナデザインモードは、Google Borgで大規模なコンテナクラスタを管理するためのベストプラクティスの一つであり、Kubernetesの複雑なアプリケーションオーケストレーションの基本的な依存関係の一つでもあります。
- すべてのデザインモードにおいて、本質はデカップリングと再利用です。
- バックアッププランを見る
Podとコンテナのデザインモードは、Kubernetesシステムの基本です。企業やチームでPodの利用状況を確認すると、いわゆる「リッチコンテナ」と呼ばれる設計が使われていることがあります。しかし、この設計は多くの悪いO&M習慣を生み出すため、中間的な移行のためのものです。リッチコンテナを切り離し、それぞれを小さなPodに分割するコンテナデザインモードを徐々に採用することを強くお勧めします。これは、Alibabaのクラウドへの完全移行のための重要な要素でもあります。
本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。
アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ