Adapter とは、アプリケーションと同じ実行単位に配置し、外から見える出力とインターフェースを統一する補助コンテナです。アプリケーションのコードには手を入れず、外から見た形だけを揃えます。
実行単位(同じマシンにまとめて配置され、まとめて作られ削除されるコンテナの集まり)・実行基盤(コンテナの配置とライフサイクルを管理する基盤)・volume(同じ実行単位の複数のコンテナから同じ内容が見えるファイルの置き場)は、Sidecar と同じ意味で使います。同じ実行単位のコンテナはネットワークを共有する設定にできるため、隣のコンテナに localhost で届きます。
Adapter は「複数のコンテナにまたがって出力とインターフェースを標準化する事」で、外の世界に「単純化され均質にされたアプリケーションの姿」を見せます(論文「Design Patterns for Container-based Distributed Systems」の 4.3 節)。
この名前は、論文より前に Brendan Burns 氏が 2015 年 6 月 29 日に公開したブログ「The Distributed System ToolKit: Patterns for Composite Containers」で紹介され、同ブログには Adapter が「出力を標準化し、正規化する」ものだと書かれています。論文は、このブログのパターンを Brendan Burns 氏と David Oppenheimer 氏が体系化した物です。
論文とブログが挙げる監視の例を以下に示します。
flowchart LR
subgraph U1["実行単位 1"]
A1["アプリ A<br/>JMX"] --> D1["adapter"]
end
subgraph U2["実行単位 2"]
A2["アプリ B<br/>StatsD"] --> D2["adapter"]
end
subgraph U3["実行単位 3"]
A3["アプリ C<br/>独自の形式"] --> D3["adapter"]
end
D1 --> M["監視基盤"]
D2 --> M
D3 --> M
上記の図の監視基盤が知っているのは 1 つの形式だけです。3 つのアプリケーションはそれぞれ別の方法でメトリクスを出しており、その違いを吸収するのが各実行単位の adapter です。JMX(Java Management Extensions)は Java の管理と監視のための仕様で、アプリケーションが登録した情報を外から読み書きする窓口です。
StatsD は、メトリクスを短いテキスト 1 行で送るプロトコルと、それを受けて集約するデーモンを指します。どちらも監視基盤が直接読める形とは限らず、そのままでは監視基盤ごとに読み方を用意する事になります。
ブログでも論文でも、Sidecar・Ambassador・Adapter は並列の 3 つのパターンとして挙げられています。同居する補助コンテナ全般を sidecar と呼ぶ広い用法では Adapter も sidecar に含まれ、この用法と論文の分類の関係は Sidecar のノートで説明しています。
Ambassador との違いは、単純化して見せる相手にあります。論文の 4.3 節では Adapter が Ambassador との対比で定義され、Ambassador がアプリケーションに外の世界を単純化して見せるのに対して、Adapter は外の世界に向けてアプリケーションを揃えます。上記の図で揃えられているのは、監視基盤から見た 3 つのアプリケーションです。
書籍『オブジェクト指向における再利用のためのデザインパターン』にも同じ名前のパターンがあります。書籍の Adapter は、クライアントが期待する形に既存のクラスのインターフェースを変換するパターンで、形を合わせる境界はコンテナではなくクラスです。
なぜ Adapter が必要なのか
出力の形式を最初から揃えておければ、変換は必要ありません。論文もその方法に触れており、Google 社内ではコードの規約によって統一を実現していると書かれています。同じ節は続けて、それはソフトウェアを一から作る場合にしかできないとも書いています。
一から作れない相手は必ず混ざります。オープンソースのミドルウェアや、手を入れられない既存のシステムがその例です。論文は、Adapter パターンによって、レガシーとオープンソースの雑多な世界が、元のアプリケーションを改変せずに統一されたインターフェースを提示できるようになるとしています。
変換をアプリケーションの外に出さず、監視基盤の中に持たせる方法もあります。その場合に何が起きるのかは、以下の通りです。
flowchart LR
subgraph M["監視基盤"]
C1["A 用の変換コード"]
C2["B 用の変換コード"]
C3["C 用の変換コード"]
end
A1["アプリ A<br/>JMX"] --> C1
A2["アプリ B<br/>StatsD"] --> C2
A3["アプリ C<br/>独自の形式"] --> C3
上記の図では、変換のコードが監視基盤の中に集まっています。アプリケーションが 1 つ増えるたびに監視基盤に手を入れる事になり、変換の誤りが監視基盤そのものを壊す位置に置かれます。論文にも、既存の監視ソリューションには複数種類のバックエンドと通信できる物があるものの、監視システム自身の中にアプリケーション固有のコードを持つので、関心の分離がきれいに保たれないと書かれています。
接点と変換の向き
論文の 4.3 節には、メインのコンテナが localhost または共有のローカルボリュームを通じて adapter と通信できると書かれています。どちらを使うかは、アプリケーションが出力を何で出しているかで決まります。監視基盤がメトリクスを取りに来る構成では、adapter がアプリケーションとは別のポートで待ち受け、外部からそのポートに問い合わせを受ける形にできます。
| アプリケーションの出し方 | 接点 | adapter が行う事 |
|---|---|---|
| ポートを開いて問い合わせに答える | localhost | 問い合わせて、統一された形式に変換する |
| ファイルに書き出す | 共有 volume | 読み取って、統一された形式に変換する |
| 一定間隔で外に送る | localhost | 受け取って、統一された形式で送り直す |
上表の監視の例では、変換されるデータはアプリケーションから外に向かいます。1 行目と 2 行目は adapter が取りに行く形、3 行目はアプリケーションが送る形です。起点は違っても、どれもアプリケーション固有の出力を外向けの形式に揃えています。3 行目には注意点があり、送り先を localhost の adapter に向け直す設定だけはアプリケーションに必要です。コードを触らずに済むという性質は、この設定までは免除しません。
論文の 4.3 節には、標準化する対象が「出力とインターフェース」と書かれています。論文が具体例として扱っているのは監視のインターフェースです。ただし、定義がインターフェースまで含んでいるので、外から来る要求の受け口を揃える構成にも当てはまります。
例えば、外部が新しい形式で呼びたいのにアプリケーションが古い形式しか受け付けない場合、adapter が受け口になって変換します。要求を受け取る向きは監視の例と逆になります。それでも、外から見たアプリケーションの形を揃えている点は監視の例と同じで、Adapter と Ambassador を通信の向きではなく見せる相手で分けて考えるのは、このためです。
取得の起点
監視の用途では、監視基盤が一定間隔でメトリクスを取りに来る形が多くなります。Prometheus の exporter をアプリケーションと同じ実行単位に同居させる使い方が、この形です。
Prometheus はメトリクスを取りに来て蓄える監視基盤で、公式ドキュメントには exporter について、サードパーティのシステムから既存のメトリクスを Prometheus のメトリクスとして書き出す助けになるライブラリとサーバだと説明されています。同ページには、exporter は「HAProxy や Linux のシステム統計のように、対象のシステムに直接 Prometheus のメトリクスを組み込む事が現実的でない場合に有用」だと書かれています。
以下が取りに来る形での往復です。
sequenceDiagram
participant M as 監視基盤
participant D as adapter
participant A as アプリケーション
M->>D: 統一された形式で<br/>取得を要求
D->>A: localhost で<br/>固有の窓口に問い合わせ
A-->>D: 固有の形式で応答
D->>D: 統一された形式に変換
D-->>M: 応答
上記の図でアプリケーションのコードは変わっていません。変換が起きるのは adapter の中だけで、取りに来る形であれば、アプリケーションは自分がどの形式で監視されているのかを知りません。監視基盤を別の実装に入れ替える場合も、アプリケーションは変えずに済みます。adapter が OpenMetrics のような標準の形式で出していて、新しい監視基盤も同じ形式を読めるなら、adapter も変えずに済みます。
差が残る場合は、adapter か監視基盤の設定のどちらかで吸収します。Adapter の価値は、監視基盤ごとに adapter を取り替える所ではありません。アプリケーション固有のインターフェースと外向けの標準的なインターフェースの境界が、アプリケーションの外に置ける所にあります。
exporter を必ず同居させるわけではありません。Prometheus の複数対象を 1 つの exporter で見る形の解説には、exporter がメトリクスの取得元のマシンで動く必要はないと書かれています。1 台の exporter が多数の対象を担当する構成もあり、それは変換のコードを 1 箇所に集める形なので、ここで扱う Adapter とは配置が違います。
同居させる場合でも、対象によっては設定が必要です。Java のアプリケーションから JMX を別のコンテナから読むには、リモート接続を有効にする起動オプションが必要です。jmx_exporter の公式ドキュメントは、同じ JVM の中で動く Java Agent としての利用を大半の利用者に勧めており、その理由にリモート JMX の設定を避けられる事を挙げています。変換をプロセスの中に入れるか別のコンテナに出すかは、この手間と、独立した更新や資源の分離とのトレードオフになります。
メトリクス以外の用途
同じ形は、出力と接点を持つ物なら何にでも当てはまります。日時とログレベルの書き方がまちまちなログを 1 つの構造に揃える用途と、生死の判定の仕方が違うアプリケーションに共通の窓口を用意する用途が代表的です。どれも、変換の規則を実行単位の中に閉じ込めて、外から見た形だけを揃えている点で同じです。
利点
- アプリケーションを改変せずに、外から見た形を揃えられる
- 変換のコードが監視基盤から出るため、アプリケーションが増えても監視基盤にアプリケーション固有の変換コードを足さずに済む
- 同じ形式で出力する複数のアプリケーションに、同じ adapter を使い回せる
- 変換の規則が実行単位の中に閉じ、監視基盤の更新と切り離せる
- ソースを触れないオープンソースや既存のシステムも、同じ扱いに乗せられる
1 番目と 5 番目は、論文が Adapter の中心に置いている性質です。手を入れられる相手だけなら、出力を揃える規約を決めれば済みます。Adapter が効くのは、手を入れられない相手が混ざった時です。
欠点
以下は、アプリケーションを改変せずに外向きの形を揃える事を優先した結果として現れる制約です。
- 実行単位ごとに adapter のコピーが動き、消費する資源がその数に比例して増える
- 統一された形式に無い項目は、捨てるか独自の名前で載せるかのどちらかになる
- アプリケーションの出力形式が変わると adapter が壊れ、アプリケーションのテストでは気付けない
- 業務と関係のない変換の対応表が、アプリケーションの数だけ保守の対象として残る
- 変換を挟む分だけ、値が外から見えるまでに追加の遅延が生じる
3 番目は、アプリケーションと adapter が別々に更新できる事の裏返しです。壊れた事に気付く経路を別に用意しないと、監視だけが静かに止まります。
分ける利益が小さいケース
- アプリケーションを自分たちで改変でき、最初から統一された形式で出力させられる構成
- 出力する形式が既に 1 つに揃っている構成
- 変換の規則が複雑で、対応表の保守がアプリケーションの改変より重くなる構成
- 実行単位の数が多く、adapter の常駐メモリの総量が無視できない構成
1 番目は論文が示した条件そのもので、一から作れるならその時点では Adapter は必要ありません。ただし、オープンソースのミドルウェアを 1 つ足した時点で改変できない相手が現れます。監視基盤が期待する形式そのものが変わった場合も、揃え直す手間は同じ所に戻ってきます。