[Note] マイクロサービスパターン【プロセス間通信編】


# これは何？

書籍 [マイクロサービスパターン【実践的システムデザインのためのコード解説】 (impress top gear)](https://www.amazon.co.jp/dp/4295008583/) に沿って、その著者のChris Richardson氏のサイト、[A pattern language for microservices
](https://microservices.io/patterns/) を引用しながらプロセス通信の章の内容をまとめた記事です。

# 同期的なリモートプロシージャ呼び出しパターン

## Remote Procedure Invocation パターン

> パターン: [Remote Procedure Invocation (RPI)](https://microservices.io/patterns/communication-style/rpi.html)
> クライアントがRESTなどの同期的なリモートプロシージャ呼び出しベースのプロトコルを使ってサービスを起動する。

* 技術例
  * REST
  * gRPC
  * Apache Thrift
* メリット
  * シンプルでやりやすい
  * "リクエスト/リプライ"が容易
* デメリット
  * "リクエスト/リプライ"以外の"通知"、"Publish/subscribe"などができない
  * サービス側が生きていることが必須なため可用性が良くない
* Issue
  * クライアントはサービスインスタンスの場所(IPアドレス)を見つけ出す必要がある

## RESTとgRPCの使い方

多くの書籍や記事で取り上げられているので割愛

## Circuit breaker パターン

> パターン: [Circuit breaker](https://microservices.io/patterns/reliability/circuit-breaker.html)
> 連続的な失敗の数が指定されたしきい値を超えたら、一定の期間が過ぎるまで、RPIプロキシは呼び出しをただちにはねつける。

* 方法
  * リクエストの連続的な失敗をカウントし、サーキットブレーカーをONにする。一定時間が経過するまで、サービスが直ちにエラーを返すようにする。一定時間時間が過ぎたらサーキットブレーカーをOFFにして、確認リクエストを通す。エラーならば再度ONにする。

## サービスディスカバリ

【サービスディスカバリの課題の背景】

マイクロサービスでは分散されたサービスインスタンスの数のその場所(IPアドレス)が動的に変化する。

![](https://microservices.io/i/servicediscovery/discovery-problem.jpg)

サービスディスカバリの主な実装方法は2種ある。

* サービスとそのクライアントが直接サービスレジストリを操作する
* デプロイインフラストラクチャがサービスディスカバリを処理する

### アプリケーションレベルのサービスディスカバリのパターン

> パターン: [Self Registration](https://microservices.io/patterns/self-registration.html)
> サービスインスタンスがサービスレジストリに自分のことを登録する

> パターン: [Client-side service discovery](https://microservices.io/patterns/client-side-discovery.html)
> サービスクライアントがサービスレジストリから利用できるサービスインスタンスのリストを入手し、ロードバランシングアルゴリズムにそのなかから1つを選択させる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91562/02b469b0-9801-c6fb-d592-2cdfcc022ed5.png)

* メリット
  * 3rdへの依存がない
  * Server-side Discoveryよりも少ない動的な構成とネットワークで実現できる
* デメリット
  * サービスをシャットダウン、削除する度にサービスインスタンス側がレジストリへ通知をする必要がある
  * クライアントがサービスレジストリと密結合になる
  * クライアント側に各プログラミング言語(フレームワーク)ごとのサービスディスカバリロジックを実装する必要がある

### プラットフォームが提供するサービスディスカバリのパターン

> パターン: [3rd Party Registration](https://microservices.io/patterns/3rd-party-registration.html)
> サードパーティ(K8sなど)がサービスレジストリにサービスインスタンスを自動的に登録する。

> パターン: [Server-side service discovery](https://microservices.io/patterns/server-side-discovery.html)
> クライアントはルーターにリクエストを送り、ルーターがサービスディスカバリを行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91562/ce9c9126-72a7-6530-4b19-0a8ca9ddffef.png)


* メリット
  * プラットフォームのレジスターはヘルスチェックをすることだけで登録・解除の処理が可能
  * 特定のプログラミング言語に依存しないでプラットフォームによって処理される
* デメリット
  * そのプラットフォームを使ってデプロイされたサービスのディスカバリしかサポートできない
  * ルーターの設定を行わないといけない(構成要素がClient Side Discoveryより多い)

# 非同期的メッセージングパターンを使った通信

> パターン: [Messaging](https://microservices.io/patterns/communication-style/messaging.html)
> クライアントが非同期メッセージングを使ってサービスを呼び出す。

### メッセージチャネルについて

* ポイントツーポイント (point-to-point)チャネルは、チャネルを読み取るコンシューマのなかのひとつだけにメッセージを届ける。ポイントツーポイントチャネルは、1対1のインタラクションスタイルで使われる。
* パブリッシュ/サブスクライブ(public/subscribe)チャネルは、接続しているすべてのコンシューマにメッセージを届ける。1対多のインタラクションスタイルで使われる。例えば、イベントメッセージは通常パブリッシュ/サブスクライブチャネルを介して送られる。

## メッセージブローカー

### ブローカーベースのメッセージングの概要

メッセージブローカーは、すべてのメッセージが通過する仲介者。センダーがレシーバのネットワーク位置を知らなくても良い点、レシーバが処理できる状態になるまでブローカーがメッセージをバッファリングする点が大きな利点となる。

メッセージブローカーには選択肢が多くある。人気のあるOSSメッセージブローカーとして次がある。

* [ActiveMQ](http://activemq.apache.org/)
* [RabbitMQ](https://www.rabbitmq.com/)
* [Apache Kafka](https://kafka.apache.org/)

AWS KinesisやAWS SQSといったクラウドベースのメッセージングサービスもある。メッセージブローカーを選択するときは、次のことを含むさまざまなことを考慮する必要がある。

**サポートされているプログラミング言語** さまざまなプログラミング言語をサポートしているか
**サポートされているメッセージング標準** AMQPやSTOMPといった標準をサポートしているか、それともプロプライエタリなものを使っているか
**メッセージの順序** メッセージの順序を維持するか
**配信保証** どのようなタイプの配信保証をしているか
**永続記憶** メッセージはディスクに書き出され、ブローカーがクラッシュしても生き残れるようになっているか
**持続性** コンシューマがメッセージブローカーに再接続したとき、切断中に送られてきたメッセージを受信できるか
**スケーラビリティ** どの程度スケールするか
**レイテンシ** エンドツーエンドのレイテンシはどれくらいか
**競合するコンシューマ** 競合するコンシューマをサポートするか

### メッセージブローカーを使ったメッセージチャネルの実装

|メッセージブローカー|ポイントツーポイント|パブリッシュ/サブスクライブ|
|-------|------|-----|
|JSM|キュー|トピック|
|Apache Kafka|トピック|トピック|
|RabbitMQなどのAMQPベースのブローカー|交換+キュー|ファンアウト交換とコンシューマごとのキュー|
|AWS Kinesis|ストリーム|ストリーム|
|AWS SQS|キュー| - |

## ブローカーベースのメッセージの利点と欠点

* メリット
  * 疎結合
  * メッセージのバッファリング
  * 柔軟性の高い通信: 1対1 & 1対多、同期&非同期 すべてのパターンをサポートする
  * 明示的なIPC(プロセス間通信): 開発者に物理的なネットワークがあることを意識させる(vs RPC)
* デメリット
  * パフォーマンスのボトルネックになる可能性
  * 単一障害点になる危険性
  * 運用の複雑度の上昇: メッセージングシステムは、インストール、設定、運用を必要とする新たなシステムコンポーネントである

## 競合するレシーバとメッセージの順序

シーケンシャルを保証するべきサービスにてメッセージの順序を担保する仕組みが必要。Apache KafkaやAWS Kinesisなどの新しいメッセージブローカーは、シャーディング(パーティション化)された(sharded)チャネルを使ってこの問題に対処する。

以下の図の例では、個々のOrderイベントメッセージは、シャードキーとしてorderIdを持っている。同じ注文に対するイベント(作成、キャンセル、完了など)は、同じシャードにパブリッシュされ、同じコンシューマインスタンスに読み込まれる。その結果、メッセージは正しい順序で処理されることが保証される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91562/22605c5e-28ef-754b-f78d-9b5176ec63d6.png)

## 重複するメッセージの処理

重複メッセージの対処方法は2つある。

* べき等なメッセージハンドラを作る
* メッセージを追跡し、重複するメッセージを捨てる

### べき等なメッセージハンドラ

べき等(idempotent)なメッセージハンドラにすることで、メッセージの再送において**メッセージの順序が維持されていれば**、何度実行しても安全である。

しかし、順序が維持されない場合は下記のメッセージ追跡と重複メッセージの破棄の処理をする必要がある。

### メッセージの追跡とメッセージの破棄

コンシューマがメッセージングシステムからのメッセージIDを登録する専用テーブルを用意し、コンシューマは、メッセージを処理するときに、ビジネスエンティティを作成、更新するトランザクションの一部としてデータベーステーブルにメッセージIDを記録する。メッセージが重複する場合、INSERTは失敗し、コンシューマはそのメッセージを破棄する。

## トランザクショナルメッセージング

サービスは、データベースを更新するトランザクションの一部としてメッセージをパブリッシュにしなければならない場合が多い。データベース更新とメッセージシステムへのパブリッシュの2つの操作をトランザクション管理する仕組み。

### メッセージキューとしてデータベーステーブルを使う方法

> パターン: [Transactional outbox](https://microservices.io/patterns/data/transactional-outbox.html)
> データベーストランザクションの一部としてデータベースのOUTBOXテーブルにイベントやメッセージを保存することによってメッセージをパブリッシュする。

OUTBOXテーブルは一時メッセージキューとして機能する(つまりパブリッシュした終了時に削除する)。メッセージリレーは、OUTBOXテーブルを読み、メッセージブローカーをパブリッシュするコンポーネント。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91562/e2d7645f-14cd-bb02-a06d-25a1625860da.png)

* メリット
  * サービスは"高位"のドメインイベントをパブリッシュする
  * 2Phase-Commit(2PC)無し
* デメリット
  * 開発者が送信の実装忘れを起こす可能性がある

### Transaction log tailing パターンを使ったイベントパブリッシュ

> パターン: [Transaction log tailing](https://microservices.io/patterns/data/transaction-log-tailing.html)
> トランザクションログをテーリングしてデータベースに加えられた変更をパブリッシュする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91562/b9f4dcd7-3206-b2a4-a79b-67971d104322.png)

* メリット
  * 2Phase-Commit(2PC)無し
  * 正確性の保証
* デメリット
  * 一般的になっているが定義的に比較的曖昧なやり方
  * データベースにスペシフィックな技術を要求する
  * 二重送信に対する対応方法がトリッキー
* このパターンが実際に使われている例
  * [Debezium](https://debezium.io/) データベースへの変更をApache KafkaメッセージブローカーにパブリッシュするOSSプロジェクト
  * [LinkedIn Databus](https://github.com/linkedin/databus) Oracleトランザクションログをマイニングして変更点をイベントとしてパブリッシュするOSSプロジェクト。LinkedInは、Databusを使って様々な派生データストアをレコードシステムに同期させている。
  * [DynamoDB streams](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Streams.html) DynamoDB streamsは、過去24時間のDynamoDBテーブルに加えられた変更(追加、更新、削除)の時系列順のシーケンスを格納している。アプリケーションは、DynamoDB streamsからこれらの変更を読み取り、たとえばイベントとしてパブリッシュすることができる。

<details><summary>UMLメモ</summary><div>

```text:サービスディスカバリ_クライアント
@startuml
Service_Instances -> Service_Registry: 自身の登録

Client -> Service_Registry: サービス一覧のクエリ要求
Service_Registry --> Client: 一覧返却

Client -> Service_Instances: リクエスト with LB
@enduml
```

```text:サービスディスカバリ_3RD
@startuml
Platform_Registor -> Service_Registry: サービスを登録する

Client -> Platform_Router: ドメイン名でサービスを呼び出す

Platform_Router -> Service_Registry: DNS問い合わせする
Service_Registry --> Platform_Router: 返答

Platform_Router ->  Service_Instances: リクエスト
@enduml
```

</div></details>


# 参考

* [マイクロサービスパターン【実践的システムデザインのためのコード解説】 (impress top gear)](https://www.amazon.co.jp/dp/4295008583/)
* https://microservices.io/patterns/
* https://www.slideshare.net/KaushalLahankarFRM/kafka-vs-kinesis