マイクロサービス間のトランザクションのガイド
1. 前書き
この記事では、マイクロサービス間でトランザクションを実装するためのオプションについて説明します。
また、分散型マイクロサービスシナリオでのトランザクションの代替案についても確認します。
2. マイクロサービス間でのトランザクションの回避
分散トランザクションは、失敗する可能性のある多くの可動部分を持つ非常に複雑なプロセスです。 また、これらのパーツが異なるマシンまたは異なるデータセンターで実行される場合、トランザクションをコミットするプロセスは非常に長くなり、信頼性が低下する可能性があります。
これは、ユーザーエクスペリエンスとシステム全体の帯域幅に深刻な影響を与える可能性があります。 したがって、one of the best ways to solve the problem of distributed transactions is to avoid them completely.
2.1. トランザクションを必要とするアーキテクチャの例
通常、マイクロサービスは、独立しており、単独で役立つように設計されています。 アトミックなビジネスタスクを解決できるはずです。
このようなマイクロサービスでシステムを分割できれば、それらの間でトランザクションを実装する必要がまったくない可能性が高くなります。
たとえば、ユーザー間のブロードキャストメッセージングのシステムについて考えてみましょう。
userマイクロサービスは、次の基になるドメインクラスを持つユーザープロファイル(新しいユーザーの作成、プロファイルデータの編集など)に関係します。
@Entity
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Basic
private String name;
@Basic
private String surname;
@Basic
private Instant lastMessageTime;
}
messageマイクロサービスはブロードキャストに関係します。 エンティティMessageとその周辺のすべてをカプセル化します。
@Entity
public class Message implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Basic
private long userId;
@Basic
private String contents;
@Basic
private Instant messageTimestamp;
}
各マイクロサービスには独自のデータベースがあります。 messageマイクロサービスからユーザークラスにアクセスできないため、エンティティMessageからエンティティUserを参照しないことに注意してください。 idのみでユーザーを参照します。
これで、UserエンティティにlastMessageTimeフィールドが含まれます。これは、彼女のプロファイルに最後のユーザーアクティビティ時間に関する情報を表示するためです。
ただし、ユーザーに新しいメッセージを追加してlastMessageTimeを更新するには、マイクロサービス間でトランザクションを実装する必要があります。
2.2. トランザクションなしの代替アプローチ
マイクロサービスアーキテクチャを変更し、フィールドlastMessageTimeをUserエンティティから削除できます。
次に、メッセージマイクロサービスに個別のリクエストを発行し、このユーザーのすべてのメッセージの最大messageTimestamp値を見つけることで、この時間をユーザープロファイルに表示できます。
おそらく、messageマイクロサービスの負荷が高いか、さらにはダウンしている場合、ユーザーの最後のメッセージの時刻をプロファイルに表示することはできません。
ただし、ユーザーのマイクロサービスが時間内に応答しなかったという理由だけで、メッセージを保存するための分散トランザクションのコミットに失敗するよりも、それは受け入れられる可能性があります。
もちろん、複数のマイクロサービスにまたがるビジネスプロセスを実装する必要があり、それらのマイクロサービス間の不整合を許容したくない場合は、より複雑なシナリオがあります。
3. 2フェーズコミットプロトコル
Two-phase commit protocol(または2PC)は、さまざまなソフトウェアコンポーネント(複数のデータベース、メッセージキューなど)間でトランザクションを実装するためのメカニズムです。
3.1. 2PCのアーキテクチャ
分散トランザクションの重要な参加者の1人は、トランザクションコーディネーターです。 分散トランザクションは、2つのステップで構成されます。
-
準備フェーズ-このフェーズでは、トランザクションのすべての参加者がコミットの準備を行い、トランザクションを完了する準備ができていることをコーディネーターに通知します
-
コミットまたはロールバックフェーズ-このフェーズでは、トランザクションコーディネーターによってコミットまたはロールバックコマンドがすべての参加者に発行されます。
2PCの問題は、単一のマイクロサービスの操作に比べて非常に遅いことです。
Coordinating the transaction between microservices, even if they are on the same network, can really slow the system downであるため、このアプローチは通常、高負荷のシナリオでは使用されません。
3.2. XA標準
XA standardは、サポートリソース全体で2PC分散トランザクションを実行するための仕様です。 JTA準拠のアプリケーションサーバー(JBoss、GlassFishなど)は、そのまま使用できます。
分散トランザクションに参加するリソースは、たとえば、2つの異なるマイクロサービスの2つのデータベースにすることができます。
ただし、このメカニズムを利用するには、リソースを単一のJTAプラットフォームにデプロイする必要があります。 これは、マイクロサービスアーキテクチャで常に実行可能であるとは限りません。
3.3. REST-AT標準ドラフト
もう1つの提案された標準は、RedHatによって開発されたが、ドラフト段階から抜け出せなかったREST-ATです。 ただし、WildFlyアプリケーションサーバーですぐにサポートされます。
この標準では、アプリケーションサーバーを、分散トランザクションを作成および参加するための特定のREST APIを備えたトランザクションコーディネーターとして使用できます。
2フェーズトランザクションに参加するRESTful Webサービスも、特定のREST APIをサポートする必要があります。
残念ながら、分散トランザクションをマイクロサービスのローカルリソースにブリッジするには、これらのリソースを単一のJTAプラットフォームにデプロイするか、このブリッジを自分で作成するという重要なタスクを解決する必要があります。
4. 結果整合性と補償
これまでのところ、マイクロサービス全体で一貫性を処理するための最も実現可能なモデルの1つは、eventual consistencyです。
このモデルは、マイクロサービス全体に分散ACIDトランザクションを適用しません。 代わりに、将来のある時点でシステムが最終的に一貫性を保つことを保証するいくつかのメカニズムを使用することを提案します。
4.1. 結果整合性の事例
たとえば、次のタスクを解決する必要があるとします。
-
ユーザープロファイルを登録する
-
ユーザーが実際にシステムにアクセスできることを自動化されたバックグラウンドで確認します
2番目のタスクは、たとえば、このユーザーが何らかの理由でサーバーから禁止されていないことを確認することです。
ただし、時間がかかる可能性があるため、別のマイクロサービスに抽出したいと思います。 登録に成功したことを知るためだけに、ユーザーを長時間待たせるのは合理的ではありません。
One way to solve it would be with a message-driven approach including compensation.次のアーキテクチャについて考えてみましょう。
-
ユーザープロファイルの登録を担当するuserマイクロサービス
-
バックグラウンドチェックの実行を担当するvalidationマイクロサービス
-
永続キューをサポートするメッセージングプラットフォーム
メッセージングプラットフォームは、マイクロサービスによって送信されたメッセージが確実に永続化されるようにします。 その後、受信者が現在利用できない場合は、後で配信されます
4.2. 幸せなシナリオ
このアーキテクチャでの幸せなシナリオは次のとおりです。
-
userマイクロサービスはユーザーを登録し、そのユーザーに関する情報をローカルデータベースに保存します
-
userマイクロサービスは、このユーザーにフラグを付けます。 このユーザーはまだ検証されておらず、システムの全機能にアクセスできないことを示している可能性があります
-
システムのすべての機能にすぐにアクセスできるわけではないという警告とともに、登録の確認がユーザーに送信されます。
-
userマイクロサービスはvalidationマイクロサービスにメッセージを送信して、ユーザーの身元調査を行います
-
validationマイクロサービスはバックグラウンドチェックを実行し、チェックの結果を含むメッセージをuserマイクロサービスに送信します
-
結果が正の場合、userマイクロサービスはユーザーのブロックを解除します
-
結果が否定的な場合、userマイクロサービスはユーザーアカウントを削除します
-
これらすべての手順を実行すると、システムは一貫した状態になります。 ただし、しばらくの間、ユーザーエンティティは不完全な状態に見えました。
The last step, when the user microservice removes the invalid account, is a compensation phase。
4.3. 障害シナリオ
次に、いくつかの障害シナリオについて考えてみましょう。
-
validationマイクロサービスにアクセスできない場合、永続キュー機能を備えたメッセージングプラットフォームにより、validationマイクロサービスが後でこのメッセージを受信することが保証されます。
-
メッセージングプラットフォームに障害が発生した場合、userマイクロサービスは、たとえば、まだ検証されていないすべてのユーザーのスケジュールされたバッチ処理によって、後でメッセージを再送信しようとします。
-
validationマイクロサービスがメッセージを受信し、ユーザーを検証したが、メッセージングプラットフォームの障害のために応答を返送できない場合、validationマイクロサービスも後でメッセージの送信を再試行します
-
メッセージの1つが失われた場合、またはその他の障害が発生した場合、userマイクロサービスは、スケジュールされたバッチ処理によって検証されていないすべてのユーザーを検出し、検証の要求を再度送信します
一部のメッセージが複数回発行された場合でも、これはマイクロサービスのデータベース内のデータの一貫性に影響を与えません。
考えられるすべての障害シナリオを注意深く検討することにより、システムが結果整合性の条件を満たすことを保証できます。 同時に、コストのかかる分散トランザクションを処理する必要はありません。
しかし、最終的な一貫性を確保することは複雑なタスクであることに注意する必要があります。 すべての場合に単一のソリューションがあるわけではありません。
5. 結論
この記事では、マイクロサービス間でトランザクションを実装するためのメカニズムのいくつかについて説明しました。
また、そもそもこのスタイルのトランザクションを実行する代わりの方法もいくつか検討しました。