マイクロサービス アーキテクチャでの通信
ヒント
このコンテンツは eBook の「コンテナー化された .NET アプリケーションの .NET マイクロサービス アーキテクチャ」からの抜粋です。.NET Docs で閲覧できるほか、PDF として無料ダウンロードすると、オンラインで閲覧できます。
1 つのプロセスで実行されているモノリシック アプリケーションでは、コンポーネントは言語レベルのメソッドや関数呼び出しを使用して相互に呼び出します。 コード (new ClassName()
など) でオブジェクトを作成する場合はこれらを厳密に結合することができます。あるいは、具象オブジェクト インスタンスではなく抽象化を参照して依存関係の挿入を使用する場合、分離された方法で呼び出すことができます。 どちらの方法でも、オブジェクトは同じプロセス内で実行されます。 モノリシック アプリケーションからマイクロサービス ベースのアプリケーションに変更する際の最大の課題は、通信メカニズムの変更にあります。 サービスのインプロセス メソッド呼び出しから RPC 呼び出しへの直接変換により、やり取りが多くなり、効率的な通信が行われず、分散環境でのパフォーマンスが低下します。 分散システムを適切に設計する際の課題はよく知られており、分散コンピューティングの落とし穴としてもまとめられています。これは、開発者がモノリシックから分散設計への移行時に想定してしまいがちな誤った前提を集めたものです。
ソリューションは 1 つではなく、いくつかあります。 1 つのソリューションは、できるだけ多くのビジネス マイクロサービスを分離することです。 その後、内部マイクロサービス間の非同期通信を使用して、オブジェクトのプロセス間通信では一般的な粒度の細かい通信を粒度の粗い通信に置き換えます。 そのためには、呼び出しをグループ化し、複数の内部呼び出しの結果を集約するデータをクライアントに返します。
マイクロサービス ベースのアプリケーションは、複数のプロセスまたはサービスで (通常は複数のサーバーまたはホスト間でも) 実行される分散システムです。 通常、各サービス インスタンスはプロセスです。 そのため、サービスは、各サービスの性質に応じて、HTTP、AMQP などのプロセス間通信プロトコル、または TCP などのバイナリ プロトコルを使用して対話する必要があります。
マイクロサービス コミュニティは、"スマート エンドポイントとダム パイプ" という考えを推進しています。 このスローガンは、マイクロサービス間をできるだけ分離し、1 つのマイクロサービス内ではできるだけまとまりのある設計を促進するものです。 前述のとおり、各マイクロサービスは独自のデータとドメイン ロジックを所有しています。 しかし、エンドツーエンド アプリケーションを構成するマイクロサービスは、一元化されたビジネス プロセス オーケストレーターの代わりの WS-* および柔軟なイベント ドリブン通信などの複雑なプロトコルではなく、通常は REST 通信を使用して単に振り付けされます。
一般的に使用される 2 つのプロトコルは、リソース API を使用する HTTP 要求/応答 (ほとんどすべてのクエリを実行する場合) と、複数のマイクロサービス間で更新を伝達する場合の軽量な非同期メッセージングです。 これらについては、次のセクションで詳しく説明します。
通信の種類
クライアントとサービスはさまざまな種類の通信を介して通信することができ、ターゲットとなるシナリオと目標はそれぞれ異なります。 最初に、これらの種類の通信を 2 つの軸に分類することができます。
最初の軸では、プロトコルを同期とするか非同期とするかを定義します。
同期プロトコル。 HTTP は同期プロトコルです。 クライアントは要求を送信し、サービスからの応答を待機します。 これは、同期 (スレッドがブロックされている) または非同期 (スレッドがブロックされておらず、応答が最終的にコールバックに到達する) となる可能性のあるクライアント コードの実行とは無関係です。 ここで重要な点は、プロトコル (HTTP/HTTPS) が同期であり、クライアント コードは HTTP サーバー応答の受信時にのみ、そのタスクを続行できることです。
非同期プロトコル。 AMQP (多くのオペレーティング システムとクラウド環境でサポートされているプロトコル) などの他のプロトコルでは、非同期メッセージを使用します。 クライアント コードまたはメッセージの送信者は、通常、応答を待機しません。 RabbitMQ キューまたはその他のメッセージ ブローカーにメッセージを送信するときにメッセージを送信するだけです。
2 つ目の軸では、通信の受信者が単一であるか複数であるかを定義します。
単一の受信者。 各要求は、単一の受信者またはサービスのみに処理される必要があります。 この通信の例はコマンド パターンです。
複数の受信者。 ゼロから複数の受信者が各要求を処理できます。 この種の通信は非同期である必要があります。 例として、イベント ドリブン アーキテクチャなどのパターンで使用されるパブリッシュ/サブスクライブ メカニズムがあります。 これは、イベントを介して複数のマイクロサービス間でデータ更新を伝搬する場合のイベント バス インターフェイスまたはメッセージ ブローカーに基づきます。通常は、サービス バスまたはトピックとサブスクリプションを使用する Azure Service Bus などの同様の成果物を介して実装されます。
マイクロサービス ベースのアプリケーションでは、多くの場合、これらの通信スタイルを組み合わせて使用します。 最も一般的な種類は、通常の Web API HTTP サービスを呼び出すときに HTTP/HTTPS などの同期プロトコルを使用する単一受信者通信です。 マイクロサービスでは、通常、マイクロサービス間の非同期通信でメッセージング プロトコルも使用します。
これらの軸は、使用可能な通信メカニズムは明確であるが、マイクロサービスのビルド時の重要事項ではないことを認識するのに役立ちます。 クライアント スレッド実行の非同期の性質と選択したプロトコルの非同期の性質のいずれも、マイクロサービスの統合時の重要な点ではありません。 重要なこと は、マイクロサービスの独立性を維持しながら、マイクロサービスを非同期的に統合できるということです。これについては、以下のセクションで説明します。
マイクロサービスの非同期統合によるマイクロサービスの自律性の強制
前述のとおり、マイクロサービス ベースのアプリケーションをビルドする際の重要な点は、マイクロサービスを統合する方法です。 内部マイクロサービス間の通信の最小化を試みることが理想的です。 マイクロサービス間の通信が少ないほど良いです。 しかし、多くの場合、何らかの形でマイクロサービスを統合する必要があります。 その場合に重要なルールは、マイクロサービス間の通信が非同期である必要があるということです。 これは、特定のプロトコル (たとえば、非同期メッセージングと同期 HTTP) を使用する必要があるという意味ではありません。 ただ、マイクロサービス間の通信はデータを非同期的に伝搬してのみ行う必要があるが、初期サービスの HTTP 要求/応答操作の一部として他の内部マイクロサービスに依存しないようにするという意味です。
可能であれば、複数のマイクロサービス間の同期通信 (要求/応答) に依存しないようにしてください (クエリの場合でも)。 各マイクロサービスの目標は、エンド ツー エンド アプリケーションの一部である他のサービスが停止しているか異常な状態である場合でも、自立しており、クライアント コンシューマーが使用できるようにすることです。 クライアント アプリケーションへの応答を提供できるように、マイクロサービス間で呼び出しを行う必要があると思われる場合 (データ クエリのために HTTP 要求を実行するなど)、いくつかのマイクロサービスが失敗したときに回復力のないアーキテクチャが存在します。
さらに、HTTP 要求チェーンがある長い要求/応答サイクルを作成するときなど、マイクロサービス間に HTTP 依存関係がある場合、図 4-15 の最初の部分に示すように、マイクロサービスが自律しなくなるだけでなく、そのチェーン内のサービスのいずれかが適切に実行されなくなるとすぐにパフォーマンスに影響します。
クエリ要求など、マイクロサービス間の同期依存関係を追加すればするほど、クライアント アプリの全体的な応答時間が悪化します。
図 4-15. マイクロサービス間の通信におけるアンチ パターンとパターン
上の図に示すように、同期通信では、クライアント要求への対応中に要求の "チェーン" がマイクロサービス間に作成されます。 これは、パターンに当てはまりません。 非同期通信では、マイクロサービスは他のマイクロサービスとの通信に非同期メッセージまたは HTTP ポーリングを使用しますが、クライアント要求はすぐに対処されます。
あるマイクロサービスが別のマイクロサービスで追加アクションを発生させる必要がある場合、可能であれば、そのアクションを元のマイクロサービスの要求と応答操作の一部として同期的に実行しないでください。 代わりに、(非同期メッセージングまたは統合イベント、キューなどを使用して) 非同期的に実行してください。 ただし、できるだけ、元の同期要求と応答操作の一部として同期的にアクションを呼び出さないでください。
最後 (そしてこの時点でマイクロサービスのビルド時のほとんどの問題が発生します) に、初期のマイクロサービスで他のマイクロサービスが最初に所有していたデータが必要になった場合、そのデータの同期要求に依存しないでください。 代わりに、最終的な整合性を使用 (通常は、以降のセクションの説明のとおり、統合イベントを使用) して、そのデータ (必要な属性のみ) を初期サービスのデータベースにレプリケートまたは伝搬します。
「マイクロサービスごとにドメイン モデルの境界を識別する」セクションの説明のとおり、複数のマイクロサービスでのデータ複製は不適切な設計ではありません。むしろ、その場合、データを追加ドメインまたは境界コンテキストの特定の言語または用語に変換できます。 たとえば、eShopOnContainers アプリケーションには、User
という名前のエンティティを持つユーザーのほとんどのデータを担当する identity-api
という名前のマイクロサービスがあります。 ただし、Ordering
マイクロサービス内でユーザーに関するデータを格納する必要がある場合は、Buyer
という名前の別のエンティティとして格納します。 Buyer
エンティティは元の User
エンティティと同じ ID を共有しますが、ユーザー プロファイル全体ではなく、Ordering
ドメインで必要ないくつかの属性のみが存在する可能性があります。
最終的な整合性を確保するために、任意のプロトコルを使用して、マイクロサービス間でのデータ通信と伝搬を非同期に行うことができます。 前述のとおり、イベント バスまたはメッセージ ブローカーを使用する統合イベントを使用することも、他のサービスを代わりにポーリングして HTTP を使用することもできます。 これは問題ありません。 ここで重要なのは、マイクロサービス間の同期依存関係を作成しないことです。
次のセクションでは、マイクロサービス ベースのアプリケーションの使用を検討できる複数の通信スタイルについて説明します。
通信スタイル
使用する通信の種類に応じて、通信で使用できる多くのプロトコルと選択肢があります。 同期要求/応答ベースの通信メカニズムを使用している場合 (特に、Docker ホストやマイクロサービス クラスターの外部でサービスを公開している場合)、HTTP や REST 方法などのプロトコルが最も一般的です。 内部的に (Docker ホストやマイクロサービス クラスター内) サービス間で通信を行っている場合、バイナリ形式の通信メカニズム (TCP およびバイナリ形式を使用する WCF など) を使用することもできます。 また、AMQP など、非同期のメッセージ ベースの通信メカニズムを使用することもできます。
JSON や XML などの複数のメッセージ形式 (バイナリ形式でも) がより効率的な場合もあります。 選択したバイナリ形式が標準的なものでない場合、その形式を使用してサービスを一般公開することはお勧めできません。 マイクロサービス間の内部通信では標準以外の形式を使用できます。 Docker ホストまたはマイクロサービス クラスター (たとえば、Docker オーケストレーター) 内のマイクロサービス間で通信を行う場合や、マイクロサービスと通信する専用クライアント アプリケーションの場合にこの操作を行うことがあります。
HTTP および REST による要求/応答通信
クライアントが要求/応答通信を使用する場合、要求がサービスに送信されてから、サービスが要求を処理して応答を返します。 要求/応答通信は、クライアント アプリからのリアルタイム UI (ライブ ユーザー インターフェイス) のデータのクエリの場合に特に適しています。 したがって、マイクロサービス アーキテクチャでは、図 4-16 のように、ほとんどのクエリでこの通信メカニズムを使用する可能性があります。
図 4-16. HTTP 要求/応答通信 (同期または非同期) の使用
クライアントが要求/応答通信を使用する場合、応答は短時間 (通常は 1 秒未満、または最大でも数秒) で到達することを前提とします。 応答が遅延する場合は、メッセージング パターンとメッセージング テクノロジ (次のセクションで説明しますが、これらは異なる方法です) に基づいて非同期通信を実装する必要があります。
要求/応答通信で一般的に使用されるアーキテクチャ スタイルは REST です。 これは、GET、POST、PUT などの HTTP 動詞を採用する、HTTP プロトコルに基づいており、また、密接に関連している方法です。 REST は、サービスの作成時に最もよく使用されるアーキテクチャ通信方法です。 ASP.NET Core Web API サービスの開発時に REST サービスを実装できます。
インターフェイス定義言語として HTTP REST サービスを使用する場合には特に便利です。 たとえば、Swagger メタデータを使用してサービス API を記述する場合、サービスを直接検出して使用できるクライアント スタブを生成するツールを使用できます。
その他の技術情報
Martin Fowler。 Richardson 成熟度モデル REST モデルの説明。
https://martinfowler.com/articles/richardsonMaturityModel.htmlSwagger 公式サイト。
https://swagger.io/
HTTP に基づくプッシュおよびリアルタイム通信
ASP.NET SignalR などの上位レベルのフレームワークと WebSockets などのプロトコルを使用するリアルタイムの一対多通信も考えられます (通常は REST とは異なる目的で使用)。
図 4-17 に示すとおり、リアルタイムの HTTP 通信は、クライアントが新しいデータを要求するまでサービスが待機するのではなく、データが使用可能になったときに接続されたクライアントにコンテンツをプッシュするサーバー コードを使用できることを意味します。
図 4-17. 一対多のリアルタイム非同期メッセージ通信
SignalR は、バックエンド サーバーからクライアントにコンテンツをプッシュするためのリアルタイム通信を実現する優れた方法です。 通信がリアルタイムであるため、クライアント アプリはほぼ瞬時に変更内容を表示します。 これは、通常、多くの WebSocket 接続 (クライアントごとに 1 つ) を使用して、WebSocket などのプロトコルで処理されます。 典型的な例は、サービスが多くのクライアント Web アプリにスポーツ ゲームのスコアの変更を同時に伝達する場合です。
.NET