概要
Java読書会でせっかく勉強したのにつぎつぎと忘れていくので、印象に残ったところを記録していく
3章 マイクロサービスアーキテクチャで使われるプロセス間通信
- マイクロサービスアーキクテチャでは、複数のサービスを組み合わせて、全体として一つのサービスを形成するので、各サービス間の通信方法は必然的に重要な要素となる
- この章では大きく同期・非同期と分けながら色々な概念を紹介しているが、最重要なのは「3.3.7 トランザクショナルメッセージング」
- 以後の章では、トランザクショナルメッセージングがある前提で話が進められる
同期呼び出し
Remote procedure invocationパターン
- 略してRPI。同期的なリモート呼び出しを指す
- https://microservices.io/patterns/communication-style/rpi.html
- 具体例としてはRESTやgRPCが挙げられている
- SOAP、CORBAやJava RMIなどもこの類に入る
- Java RMIのような言語固有のメカニズムを使うのは、様々な技術が入り乱れるマイクロサービスアーキクテチャには向かないとしている
- この本としては、同期呼び出しは可用性を下げるので、あまりおすすめしてない
Circuit breakerパターン
- https://microservices.io/patterns/reliability/circuit-breaker.html
- 同期呼び出し向けのパターンで、あるサービスへの呼び出しが一定回数、連続で失敗したり応答を返さなかったりした場合、以降の呼び出しは、(おそらくクライアント側のプロキシのところで)即座にエラーを返すようにする
- 一定期間が過ぎたら再びリモート呼び出しを行うようにする
- Java向けにはNetflixのHystrixが紹介されている
サービスディスカバリ
- 同期呼び出しの場合に通信相手をどうやって見つけるか?
- Self registrationパターンと、Client-side discoveryパターンの組み合わせ
- Self registrationは、呼ばれる側が自らサービスレジストリに登録しにいく
- Client-side discoveryは、クライアント側がサービスレジストリから呼び出したい相手を見つけ出す。候補が複数ある場合は、ラウンドロビンなりランダムなりで負荷分散する
- CORBAのNameServiceと同様な考え方
- プラットフォームが提供するサービスディスカバリのパターン
- 3rd party registration
- Kubernetes環境下では立ち上げたサービスをサービスレジストリに自動で登録する
- https://microservices.io/patterns/3rd-party-registration.html
- Server-side discovery
- クライアントがサービスを呼ぶときは、サービスレジストリの手前にあるルーター(この文脈ではロードバランサと呼ぶべきか?)に対して呼び出しを行う。ルーターは上記の登録済みサービスを見つけ出してリクエストを転送する
- https://microservices.io/patterns/server-side-discovery.html
- 3rd party registration
非同期メッセージング
- Messagingパターン
- サービス間の通信は、チャネルを通じて行われる
- 非同期リクエスト・レスポンスの場合は、双方向のチャネルを使う
- 非同期メッセージングには、メッセージブローカーを使う場合と使わない場合がある
- ブローカーなしだと、同期の場合と同様に可用性が下がるのと、サービスディスカバリが必要になるのでこの本ではおすすめしてない
- メッセージブローカーありの場合は、ライブラリとしては、JMS、Apache Kafka、RabbitMQ、AWS Kinesis、AWS SQSなどが挙げられている
- SQSの場合は、パブサブがない
- この本としては、ブローカーありの方が、粗結合で可用性が高いのでおすすめとしている
- 多くのブローカーは、At least Onceしか保証してないため、重複メッセージの排除のために処理済みメッセージのidをDBに保存するなどの工夫が必要
トランザクショナルメッセージング
- サービスが自身のDBに対する処理を行いつつ、他のサービスに対してメッセージを発行する場合の話
- 例えばOrderのレコードを作りつつ、OrderCreatedイベントを発行する場合(イベントでなくても他のサービスへのリクエスト発行でも良い)
- このようなケースではDBの整合性はトランザクションによって保たれるが、DBの処理とメッセージの発行を分けて実施するとイベントの順序が逆転してしまうことがある
- 例えばOrderがCancelされてから、Orderが生成されるというイベント順になったりする
- 解決方法としては、メッセージをキューイングするためのテーブル(Outboxテーブル)を用意して、元々のテーブル更新と、メッセージキューイングのテーブル更新を一つのトランザクションで行う
- Transactional outboxパターン: https://microservices.io/patterns/data/transactional-outbox.html
- Outboxテーブルに貯められたメッセージは以下のいずれかの方法で取り出され、その後ブローカーに渡される
- Polling publisherパターン:https://microservices.io/patterns/data/polling-publisher.html
- 名前の通りOutboxテーブルのポーリング
- Transaction log tailing: https://microservices.io/patterns/data/transaction-log-tailing.html
- Outboxテーブルのトランザクションログを監視する。
- Polling publisherパターン:https://microservices.io/patterns/data/polling-publisher.html
Eventuate Tram
- 前記のようなTransactional Outboxパターンを実現しようと思うと、それなりにライブラリが必要になる
- 著者が調べた範囲では、既存のライブラリでは、低レベルすぎたり、イベント以外のメッセージが発行できなかったりと不便なものしかなかった
- おすすめは著者が自ら作った「Eventuate Tram」
- Transactional outbox対応
- Transaction log tailingとPolling publisherの両対応
- ブローカーはApache Kafka
- 重複メッセージの検出対応
- 一方向メッセージ、イベントパブリッシュ、コマンドリプライなど多様なメッセージングパターンに対応
- 本書では、このあとはほとんどがEventuateベースのコードになります。
- Eventuateの宣伝本だった?
REST APIの実装方法
- サービス間の通信は非同期メッセージング推奨とは言っても、公開APIはRESTだったりする
- 反応の遅い非同期通信では、RESTに求められるパフォーマンスがでない懸念がある
- 解決法1: 例えばOrderのGETの場合は、顧客情報やレストラン情報などのミラーをOrderService内に抱えれば、サービス間の非同期通信自体しなくてすすむ
- これは7章のCQRSでより発展した議論になる
- 解決法2:REST APIでは、不完全状態のOrderをつくって、IDだけ返す。あとはクライアントがOrderの状態をポーリングする
まとめ
- 色々な概念・パターンが登場したけど、重要なのは「トランザクショナルメッセージング」
- 昔、Mutexロックを持っている状態で、キューにメッセージを入れる実装をしたことがあったが、発想は同じ
- トランザクションもキューも処理をシリアライズする仕組みなので、組み合わせて使うと全体としてもシリアルになる
- 本書ではこれ以降の議論は、トランザクショナルメッセージングによって、メッセージやイベントの順序性が保たれていることに立脚しているので、ここの部分は極めて重要
- 著者が言うようにトランザクショナルメッセージングを実現する高水準なライブラリがEventuateしなないのであれば、マイクロサービスパターンを活用したマイクロサービスアーキテクチャを構築するには、事実上Eventuateを使う以外にないということになるのかもしれない。
- 代替品はないのか?