Red Hatソフトウェアエンジニアの佐藤匡剛です。
赤帽エンジニアAdvent Calendar 2018の2日目の記事です。当赤帽エンジニアブログは、Red Hatのエンジニアが何の制約もなく書きたいことを好きに書いていくブログですので、Advent Calendarも同じように各自がテーマもバラバラに(笑)好きなことを書いていきます。
今回は、Red Hat UKのコンサルタントであるBilgin Ibryamが書いた書籍『Camel Design Patterns』を紹介します。100ページ程度の短い良書なのですが、自費出版系のLeanpubから出しているので翻訳されそうもないので、せめてブログで紹介したいと思います。(どなたか日本語訳を出してくださる出版社はいないでしょうか?)
Camelデザインパターンとは
要するにApache Camelを使ってシステム間統合のアプリケーションを作る際に有効なデザインパターンですが、Camelを使ったことのある方ならCamel自体がエンタープライズ統合パターン(EIP)の実装系であることはご存知でしょう。EIPはメッセージングをベースとしたシステム間統合で必須のデザインパターンですが、どちらかというとメッセージングやESBの基本的なコンセプトを再利用可能な形にまとめることが主眼となっています。
インテグレーションエンジニアとして実践的にCamelやEIPを使いこなしていると、EIPの基本ブロックを超えて、もう少し抽象レベルの高い設計指針を与えてくれるデザインパターンはないかな、と思うことがあります。しかし、なかなかそうしたニッチなニーズに応えてくれるパターン本は(私の知る限り)存在しませんでした。
(ちなみに、EIPの著者の1人であるGregor Hohpe氏がEIP 2という位置づけでConversation Patternsというパターンをずっと執筆しています。こちらも1メッセージの送受信を超えて、システム間の「会話(conversation)」(=一連のメッセージのやり取り)というより抽象レベルの高い振る舞いをパターン化するという試みで、いつ完成するのか分かりませんが出版される日が非常に楽しみです。)
話を戻すと、『Camel Design Patterns』はそうしたインテグレーションエンジニアの要望に応えてくれる内容になっています。BilginがCamelのコンサルタントとして現場で何度も使ってきた、実践的なパターンをまとめたEIP++的なパターン集がこの本です。一見地味なものもありますが、あくまで現場で何度も使われてきたパターンのみを採用していて、あまり使う場面はないけどカッコイイから入れておこう、というようなミーハーなパターンは1つもないそうです。
パターンの一覧
それではどんなデザインパターンがあるのか見てみましょう。パターンは、「正常系(Fundamental)」「例外処理(Error Handling)」「分散配備(Deployment)」の3カテゴリに分けられています。
正常系のパターン
- VETROパターン
- Canonical Data Model(正規データモデル)パターン
- Edge Component(端末コンポーネント)パターン
- Command Query Responsibility Segregation(コマンドクエリ責務分離)パターン
- Reusable Route(再利用可能ルート)パターン
- Runtime Reconfiguration(実行時設定変更)パターン
- External Configuration(外部設定化)パターン
例外処理のパターン
- Data Integrity(データ整合性)パターン
- Saga(サーガ)パターン
- Idempotent Filter(冪等フィルタ)パターン
- Retry(再試行)パターン
- Throttling(スロットル調整)パターン
- Circuit Breaker(回路ブレーカー)パターン
- Error Channel(エラーチャネル)パターン
分散配備のパターン
- Service Instance(サービスインスタンス)パターン
- Singleton Service(シングルトンサービス)パターン
- Load Levelling(負荷平準化)パターン
- Parallel Pipeline(並行パイプライン)パターン
- Bulkhead(遮断壁)パターン
- Service Consolidation(サービス統合整理)パターン
『Camel Design Patterns』は100ページちょっとしかないので、ある程度洋書を読み慣れた方なら比較的簡単に読めてしまいます。洋書をあまり読んだことのない方でも、分量的にチャレンジするのにちょうど良い本です。この本のパターンの切り口に興味を持たれた方は、実際に読んでみることをお勧めします。
また、Camelデザインパターンと言っていますが、Camelによる実装の詳細以外の、パターンとしてのコンテキスト/問題/解決方法のセットはあらゆるシステム間統合のプロジェクトに適用可能なはずです。CamelでなくAkka/Alpakka、Spring Integration、Muleなどを普段使っている方も本書を手にとってみるといいかもしれません。
それでは、各カテゴリごとにパターン1つ1つを簡単に紹介していきます。
正常系のパターン
まずは、例外などの異常系の処理を考慮しない、正常パスにおいてどのようにインテグレーションのルーティングを構成するのがいいのか、ということに関する7つのパターンです。
VETROパターン
VETROというのは、Validate(データ検証)、Enrich(データ付加)、Transform(データ変換)、Route(ルーティング)、Operate(処理実行)の略です。インテグレーションのフロー(Camelルート)をいくつも開発していくと、たいていはこの5ステップから構成されている、という知見から得られたパターンです。たくさんのインテグレーションを開発しなければいけない場合、このVETROに構造を標準化していくとシステムの見通しがよくなりメンテナンス性が向上します。
VETROパターンについては、私の前職からの先輩でESB伝道師であるフェンさんがとても良い記事をすでに書いているので、詳細はそちらを読んでみてください。
Canonical Data Model(正規データモデル)パターン
こちらはEIPに既に登場しているパターンですが、実プロジェクトでも有効なパターンであるということで再掲されています。
複数のシステムを連携させるときに、個々のシステムの独自のデータモデルをそのまま使っているとインテグレーションの中で多対多のデータ変換が必要になります。連携するシステムの数が増えてくると制御不能になるので、インテグレーションのレイヤーでは正規データモデル(CDM)を標準にして、CDMと個々のデータモデルとの一対多のデータ変換に集約させる、というのがこのパターンのアイディアです。
小さなアプリケーションではCDMの導入はむしろコストが高いですが、アプリケーションが大きくなっていくとCDM導入の検討が必要になっていきます。
Edge Component(端末コンポーネント)パターン
Camelルートを開発していると、ルートの中にチャネル固有の技術的な部分(ファイル転送、SOAP、JMS)と純粋にインテグレーションのビジネスロジックの部分とが混在していきます。「関心事の分離」という観点からは、技術的な部分とビジネスロジックの部分を分けることが望ましいため、チャネル固有の技術的な部分を「エッジ(端末)コンポーネント」として別のCamelルートに切り出します。
Command Query Responsibility Segregation(コマンドクエリ責務分離)パターン
ドメイン駆動設計(DDD)の文脈でもよく取り上げられている、いわゆるCQRSのパターンです。コマンド(=データの更新系、CRUDの"CUD")とクエリ(=データの参照系、CRUDの"R")とでは必然的に必要とされるデータモデルも異なってくるので、システムを別々にした方がよいという考え方です。
Camelによるインテグレーションのサービス開発でも、コマンドとクエリを別々のサービスに実装することが推奨されます。
Reusable Route(再利用可能ルート)パターン
システム開発において、コードの再利用は必須のプラクティスです。Camelでも、本格的な実プロジェクトではインテグレーションフローの単位である「Camelルート」の再利用化を追究する必要があります。
このパターンでは、「開発時(静的)」と「実行時(動的)」の2つの観点から、どうやってCamelルートの再利用化を促進できるかが議論されます。
Runtime Reconfiguration(実行時設定変更)パターン
インテグレーションの実プロジェクトでは、システム間連携のフローが実行時に完全に固定されてしまうのではなく、パラメータ変更等によって実行時にある程度柔軟に変更できる必要があることがあります。このパターンでは、Camelアプリケーションの設定をどうやって実行時に変更できるようにするか、が議論されます。
方法としては、設定ファイルやOSGi、JMXによるもの、メッセージングによるもの、外部データベースによるもの、ルールエンジンやテンプレートエンジンなどをCamelコンポーネントを通して活用するもの、などがあります。
External Configuration(外部設定化)パターン
1つ前のパターンとも関連しますが、(Camelに限らずですが)アプリケーションのパラメータを外部化することも必要です。とくにインテグレーションのアプリケーションでは様々なシステムと様々なプロトコルで連携する必要があるため、多数のパラメータが必要になります。
設定パラメータには、大きく環境に関するプロパティとチューニングに関するプロパティの2種類があります。このパターンでは、Camelでそうしたプロパティを扱うための方法が議論されます。
例外処理のパターン
インテグレーションのアプリケーションでとくに複雑な部分は、何らかの障害が発生した場合の例外処理の方法です。ここでは例外処理に関する7つのパターンが紹介されます。インテグレーションのプロジェクトで技術的に一番面白いのは、この例外処理の部分かもしれません。
Data Integrity(データ整合性)パターン
これはいわば古典的なやり方で、インテグレーションのアプリケーションでデータの整合性を保つためにACID特性のあるトランザクションを使うというパターンです。
すべての転送プロトコルでトランザクションがサポートされる訳ではなく、1)トランザクションなし、2)トランザクションあり、3)結果整合性あり、の3つに分類されます。このパターンでは、そのうち最初の2つを扱います。
ファイル転送のようにトランザクションのない転送プロトコルでは、トランザクションではない何らかの別の仕組みを使ってACIDな処理を実現させます。
RDBやJMSのようにトランザクションをサポートするデータソースに対しては、ローカルトランザクションとグローバルトランザクションの2つの方法が使えます。しかし、そのうちグローバルトランザクションについては導入コストが高いため、冪等フィルタを使って代替する方法も示されます。
Saga(サーガ)パターン
サーガとは、ワークフローや状態マシンの仕組みを使って分散トランザクションの挙動をエミュレートし、トランザクションを使わずに結果整合性を実現しようというパターンです。複数のシステムにまたがった複雑なフローが壮大な叙事詩(=サーガ)のようであるため、この名前が付いたのでしょう。
Idempotent Filter(冪等フィルタ)パターン
EIPにもIdempotent Receiverというパターンが登場しますが、冪等性(idempotency)というのはインテグレーションの世界で重要な概念です。同じ操作を何度実行しても結果が変わらないことを、冪等といいます。
冪等性を導入することで、リトライの際に重複したメッセージが何度も実行されてしまうエラーを排除できたり、結果整合性を保証するためのデータ復元の処理が容易になったりします。
Retry(再試行)パターン
インテグレーションにおいて一時的な通信エラーはつきものですが、そうしたエラーに対してシステムの耐障害性を高める古典的な方法がリトライです。
リトライはインテグレーションにおける例外処理の基本ですが、一点注意が必要です。リトライは一時的な通信エラーには使えるが、長時間にわたるネットワーク障害(数時間から数日)やリカバリに人手が必要な障害には使えないという点です。そうした障害には、後続のCircuit Breakerパターンを使います。
Throttling(スロットル調整)パターン
システムが、一日の特定の時間帯だけリクエストが増大するなど、一時的な負荷の増大に対してシステムのスループットを維持しなければいけない場合、一時的にリクエストの処理量を調整して対応する、というのがこのパターンです。リアクティブの世界では、バックプレッシャー(back-pressure)とも呼ばれるテクニックです。
スロットル調整の方法は、大きく1)メッセージの受取拒否、2)スレッドのブロック、3)キューによる遅延処理、4)別パス実行による縮退運転、の4つがあります。
Circuit Breaker(回路ブレーカー)パターン
Netflix HystrixやResilience4jなどのフレームワークで有名なパターンです。このパターンのミソは、先ほどのRetryパターンで対処できない比較的永続的な通信障害に対して、システムの耐障害性を高められることです。
基本的なアイディアは、外部サービスへの通信に障害が発生した場合、一時的にそのサービスに依存しない予備経路にシステムを移行した上で、一定間隔でその外部サービスへの通信をチェックしてリカバリを試みる、というものです。
Error Channel(エラーチャネル)パターン
いわゆるDLQなどのメッセージングに固有の、キューを使った例外処理に関する考慮点を議論したパターンです。このパターンでは、エラーチャネルを1)Invalid Message Channel(不整合メッセージチャネル)、2)Back Out Channel(差戻しメッセージチャネル)、3)Dead Letter Channel(配信不能チャネル)の3種類に分類しています。前者2つがアプリケーション層のエラーチャネル、最後がメッセージングシステムのエラーチャネルです。
Invalid Message Channelは、VETROパターンにおけるバリデーションに引っかかるなど、アプリケーション層で見つかったデータ不整合のあるメッセージをエラーログとして送るチャネルです。Back Out Channelは、アプリケーション層から外部のサードパーティシステムに送ったメッセージがサードパーティ側で処理失敗して差し戻されたメッセージを記録するチャネルです。最後のDead Letter Channelは、メッセージングシステムが設定ミス等によりメッセージの宛先を解決できず、配信不能になったメッセージを記録するチャネルです。
分散配備のパターン
最後のカテゴリは、分散されたインテグレーションのシステムをどのような構成でデプロイするか、に関するパターンです。アーキテクチャレベルでの、パフォーマンスや高可用性を実現するために考慮すべきパターンになります。
Service Instance(サービスインスタンス)パターン
このパターンはロードバランシングと基本的に同義で、サービスをインスタンスの単位でスケールアウトできるようにしよう、ということです。それだけだと簡単ですが、パターンとしてはロードバランシングの際に以下の考慮点を議論しています。
- サービスの状態管理 ― ロードバランシングではサービスはステートレスにするのが理想ですが、どうしてもステートフルなサービスが必要な場合はリソースの共有化やセッションアフィニティなどの仕組みの導入が必要になります。
- リクエストのディスパッチ方法 ― 何を使って、どうやってロードバランシングするかを検討する必要があります。高可用性(HA)の観点からは、ロードバランサ自体が単一障害点になるのを避ける必要もあります。
- メッセージの順序保証 ― サービスを並列実行する場合、メッセージの処理順序は重要か、重要ならどうやって順序保証をするかの検討が必要です。
- リソースの競合/排他制御 ― サービスを並列実行する場合、呼び出す先の外部リソースが並列処理に対応しているか、リソースの競合が問題にならないかを調べる必要があります。
- リソース間の結合 ― 呼び出し合うサービスやリソースの間には依存関係があるので、サービスの並列実行化によって問題が起こらないかを検討する必要があります。
- シングルトンサービス ― シングルトンで実行する必要があるサービスの場合、アクティブ/スタンバイの構成にする必要があります。次のSingleton Serviceパターンで詳しく議論されています。
Singleton Service(シングルトンサービス)パターン
システム内で動作中のインスタンスがただ1つでなければならないサービスがあります。バッチ処理などがそうです。この場合、Service Instanceパターンによるスケールアウトはできませんが、アクティブ/スタンバイの構成を取ることで高可用性を向上することはできます。
シングルトンサービスを実現する方法は色々とあります。JBoss EAP / WildFlyに昔からあるHA Singletonは、まさにそれを実現するための機能です。また、最近ではKubernetesを使えば簡単にシングルトンなPodを実行できるようになりました。
Load Levelling(負荷平準化)パターン
上流のサービスから下流のサービスへメッセージが流れているときに、上流ではリクエスト処理の負荷が一様でなく、不定期に乱高下するような場合、それでも下流のメッセージ処理を一定水準に平準化したいことがあります。その場合、上流と下流の間にキューを挟んで上流・下流の時間的結合を切る(temporal decoupling)ことによって、負荷平準化を実現します。
メッセージングを普段から使っている当たり前のように感じますが、アーキテクチャレベルでの負荷平準化の観点から、通信の要所要所にキューを差し挟む、ということがこのパターンのポイントなのでしょう。
Parallel Pipeline(並行パイプライン)パターン
システム全体のパフォーマンスを向上させるために、直列的に行われている処理のフローを一部並列化する、というのは有効なテクニックです。関数型プログラミングでも、副作用のない関数の呼出チェーンの一部を並列実行して処理を最適化するというテクニックを使いますが、それと同様です。このパターンのポイントは、アーキテクチャレベルから見た全体的なインテグレーションフローのパイプラインの中で、どこを並列化できるかを検討することにあります。
Bulkhead(遮断壁)パターン
Bulkheadパターンの考え方は、システムをいくつかのパーティションに区切ってその境界を遮断壁によって守ることで、1つのパーティションで大きな障害が発生してもそれがシステムの残りの部分に波及しないようにする、ということです。
インテグレーションシステムへの遮断壁の導入の仕方は、いくつかのレイヤーで考える必要があります。
- ハードウェアの冗長化
- ホスト(プラットフォーム/OS)の冗長化
- プロセスの隔離
- スレッドプールの隔離
Service Consolidation(サービス統合整理)パターン
最後は、サービスのグループ化に関するガイドラインのパターンです。以下3つの選択肢について、それぞれの利点と欠点が議論されます。
- Single Service per Host(ホストごとに1サービス)
- Multiple Services per Host(ホストごとに複数サービス)
- Application Container(s) per Host(ホストごとに1つまたは複数のアプリケーションコンテナ)
どのサービスをどのグループにまとめるかを決定する際は、以下の観点を考慮に入れます。
- 非機能要件
- スケーラビリティとパフォーマンス
- 可用性と耐障害性
- セキュリティ
- ライフサイクル
- アプリケーション/プロセスの生存期間
- リリース間隔
- 計算リソース(CPU、メモリ、ネットワーク帯域幅)特性
- 計算リソースの使用率
- 計算リソースの競合
- 実行時の依存関係
- ホスト実行環境への依存度
- サービス間の依存関係
- メンテナンス性
- 監視/管理、メンテナンスのコスト
- システムの複雑度
おわりに
明日のAdvent Calendarは、先ほども登場したフェンさんの担当です。引き続きCamelの話題が読めると思います :-)