Container Desgin Patterns
κeenです。先日、Kubernetesの開発者が書いたKubernetes: Container Design Patternsというのを教えてもらって、面白かったのでそれを紹介します。 ただ漫然とコンテナを使っているだけでは気付かない使い方があったのでコンテナに興味のある方は是非一読下さい。
序論
オブジェクト指向が出てすぐにオブジェクト指向デザインパターンが産まれたように、分散システムにもデザインパターンが必要となってきました。 分散システムのデザインパターンの萌芽はHadoop/MapReduceに見ることが出来ますが、Javaに限られていました。 ところがここ数年の(Linuxの)コンテナ技術の躍進により欠けていたピースが埋まりました。分散システムパターンへのデプロイの抽象化です。 依存モジュールも一緒にデプロイ出来ますし、デプロイの状態も成功/失敗の二値になります。 それだけでなく、コンテナはオブジェクト指向におけるオブジェクトによく似た役割を果たします。それを見ていきましょう。
単一コンテナのマネジメント パターン
コンテナは、自然にインターフェースの境界となります(オブジェクトと同じように)。
アプリケーション機能のインターフェースに留まらず、コンテナそのものの管理インターフェースも提供するでしょう。
典型的には run()
pause()
stop()
の管理ですが、もっと多様な管理インターフェースが有り得ます。
大抵の今時のプログラミング言語ならHTTP経由でJSONをやりとりする機能くらい簡単に書けるのでそれがコンテナ間で統一さたインターフェースになりえるでしょう。
上位のアプリケーションに対してはコンテナ内の情報(アプリケーションのQPS, プロファイル情報, コンフィグ情報, ヘルスチェック, ログなどなど)を提供するでしょう。
下位のアプリケーションにはマネジメントインターフェース、例えばgraceful shutdownなどを提供することになるでしょう。
Android OSがActivityを管理する時に様々なコールバック(onCreate
, onResume
…)を呼ぶように、分散マネージャがコンテナを管理するための様々なAPIを提供すると綺麗に管理出来ます。
また、コンテナ特有の機能として「レプリケーションする」(スケールアップするためのインターフェース)なんかもあるかもしれません。
単一ノード、複数コンテナのアプリケーションパターン
コンテナマネージャにはKubernetesのPodやNomadのTask Groupsのように複数のコンテナを1つのノードにスケジュールする機能があります。 別にコンテナに分けずに複数のコンテナをアプリケーションと同じの1つのコンテナに入れることも可能です。しかしながら分けた方が様々なメリットが得られます。
- コンテナがリソース管理の単位である。cgroupによる管理がやりやすくなる。
- コンテナがパッケージングの単位である。複数のコンテナでデプロイや管理をするチームを分けるのも簡単になるし、テストも簡単になる。
- コンテナが再利用の単位である。多くのサブ機能コンテナは多種のメインコンテナと一緒に使うことが出来る。
- コンテナが障害を分離する境界になる。例えば、コンテントマネジメントコンテナに障害があったとしても、Webサーバコンテナはサービスを継続出来るようになる。
- コンテナがデプロイの単位となる。新しい機能の追加やロールバックが単独で行える。(ただし、アプリケーションのバージョンの組み合わせが増えるという点では短所にもなる)。
ということでそれらを前提にしたデザインパターンをいくつか。
サイドカーパターン
メインのアプリケーションコンテナにサイドカーのように付属するコンテナを付けて、機能を足すパターンです。 例えばアプリケーションがあって、それのログをローカルストレージから分散ストレージに保存するサイドカーなど。
逆に、ローカルディスクのファイルを提供するWebサーバがあったとして、そのために定期的にgitからstatic fileをpollするサイドカーコンテナなんかもあるかもしれません。
これらのサイドカーは同じノードのコンテナ同士ならディスクボリュームを共有出来ることに依存した作りになっています。
アンバサダーパターン
アンバサダーパターンはメインコンテナと別システムとのコミュニケーションを代理します。 例えば、twemproxy のコンテナをアンバサダーとすれば実際は複数のmemcachedにシャードされたmemcachedクラスタと通信しているも関らず、アプリケーションはローカルホストのmemcachedと通信しているように出来ます。
この利点は1つにはアプリケーションをローカルホストのmemcachedと通信することだけを考えて書けばいいようになる点、もう1つにはアプリケーションのテストをローカルホストのmemcachedとだけすれば良くなる点、さらにもう1つにはtwemproxyのコンテナを(別の言語で書かれた)別のアプリケーションに使い回せる点にあります。
これは同じホストのコンテナ同士はローカルホストのネットワークインターフェースを使って通信出来ることに依存した作りになっています。
アダプターパターン
アダプターパターンはアンバサダーパターンの逆で、アプリケーションを外部から観測した時に統一されたインターフェースを提供するようにするために使います。
アプリケーション毎にメトリクスの採り方は異ります(例えばJavaならJMX、など)。様々にあるメトリクスを統一したインタフェースで提供するとメトリクスアグリゲータが非常にシンプルになります。
具体的な例を出すとPrometheusのためのインターフェース (HTTP /metrics
)を提供する(exporter)コンテナなどです。
アダプターとメインコンテナはストレージやローカルホストネットワークを通じてデータをやりとりするでしょう。
複数ノードアプリケーションパターン
次は複数のノードに跨る協調分散システムでのパターンです。 こちらも同じくPodなどの抽象化を前提とします。
リーダー選出パターン
分散システムではリーダを選出する需要が多々あります。 例えば複数のレプリカを作った時にマスターがコケたら次のマスターを選出しないといけません。
巷にはリーダー選出アルゴリズムを実装したライブライが出回っていますが、往々にして難解であり、また、特定の言語でしか動かないので再利用性がありません。
そこで、ライブラリを使うのではなくてリーダー選出コンテナを実装しましょう。
単一ノードのパターンと同じく、リーダー選出コンテナとアプリケーションコンテナを一緒にスケジューリングします。 そして、リーダー選出コンテナが他のコンテナとネゴシエーションしてリーダーになったら、ローカルホストのHTTP APIにbecomeLeader、renewLeadershipなどのクエリを投げます。 アプリケーションはただそれらのエンドポイントを実装しさえすればいいのです。
このコンテナはアプリケーションにも言語に依存しないので自由に使い回すことが出来ます。
ワークキューパターン
もう1つのよくある分散システムでのタスクは、ワークキューによる分散実行です。 いくつかワークキューの実装はありますが、やはりそれらは特定の言語に依存しています。
そこで仕事の分配のコンテナ、そして、仕事実行のフレームワークとなるコンテナを用意してあげましょう。
フレームワークのコンテナは分配コンテナから仕事(ファイルなど)を受け取って、ユーザの書いた実行コンテナに処理を移譲します。そしてフレームワークコンテナが実行コンテナの出力した結果をまた分配コンテナに戻します。
[Request] +---[フレームワークコンテナ]=[実行コンテナ(ユーザ)]
| |
[分配コンテナ] --+
| |
| +---[フレームワークコンテナ]=[実行コンテナ(ユーザ)]
|
[ワークキューの保存など]
分散協調の部分をフレームワークコンテナがやってくれるのでユーザが書く実行コンテナは非常にシンプルになります。
分配/集約パターン
このパターンは、クライアントが1つの巨大なタスクをルートコンテナに投げ、ルートコンテナが子コンテナ達に分割したタスクを移譲します。そして子コンテナ達の結果を纏めてクライアントに返します。
MapReduceと同じように、タスクを実行するリーフコンテナと、リーフコンテナの結果を纏めるマージコンテナを用意する必要があります。
コンテナは特定のインターフェースさえ実装していればいいのでリファクタが(オブジェクト指向の時と同じように)容易です。
また、子コンテナにルートコンテナと同じようなscatter/gather機能を持ったコンテナを使うことで、任意の深さにまでタスクツリーを作ることが出来ます。 これは例えば処理中のリソース使用量が多いときに部分的にタスクを実行してマージを繰替えすことで必要となる最大リソースを減らす、などに使えるでしょう(他にももっとあるかもしれませんがパッとは思いつきませんでした)。
まとめ
コンテナのデザインパターを紹介しました。フレームワークコンテナなど、興味深いパターンもあって興味深いですね。
まだまだパターンはありえると思うのでこの分野(?)、もう少し広まると良いですね。