Microservice間におけるデータの一貫性について

今回は以前から読みたかった本であるMicroservices Patternsを読みましたので、この本の中から特に印象に残ったMicroservice間において擬似的にトランザクションを貼る方法であるSagaパターンについてお話させて頂きます。

Sagaパターン

Sagaパターンとは、localトランザクションの繋がりであるSagaを実行し、失敗時にはそれぞれの補償トランザクションを使って、前のSagaによって行われた変更を戻しデータの一貫性を保つ方法です。つまり、失敗したときは自力でロールバックするような機構を作っておきましょうってことです。Microservices 下記にこちらの本に載っているSagaの例を載せておきます。

f:id:harada-777:20210328155447p:plain:w500

これはUber的なサービスでオーダーが作成される場合のSagaの例です。 またこのアプリケーションのアーキテクチャは以下の通りです。

f:id:harada-777:20210328155457p:plain:w600

このアプリケーションにおけるトランザクションとそれに対する補償トランザクションは以下のようになります。

f:id:harada-777:20210328155452p:plain:w600

本書ではオーダー、チケットは作成したタイミングでは、オーダーの状態が、PENDINGになっており、承認されるとAPPROVEDになり、拒否されるとREJECTEDとすることで補償トランザクションの仕組みを実装していました。 こうやって自力でロールバックを実装することで擬似的にmicroservice間でトランザクションを貼ることができるようになります。 またこのSagaパターンは構成に2つのパターンがあるので、次はその2つのパターンについて解説していきます。

Choreographyパターン

Choreographyパターンは、自分が実行された後、次の実行へのトリガーを自分で実行するパターンのことを指します。今回の例における流れを以下に示します。

f:id:harada-777:20210328155436p:plain:w600

オーダー作成が成功するパターン
  1. Order ServiceはAPPROVAL_PENDING状態のOrderを作成し、OrderCreated eventを発行する
  2. Consumer ServiceはOrderCreated eventを受け取り、顧客がオーダー可能かを判定し、ConsumerVerified eventを作成する
  3. Kitchen ServiceはOrderCreated eventを受け取り、オーダーのバリデーションを行い、CREATE_PENDING状態のTicketを作成し、TicketCreated eventを作成する
  4. Accounting ServiceはOrderCreated eventを受け取り、PENDING状態のCreditCardAuthorizationを作成する
  5. Accounting ServiceはTicketCreated eventとConsumerVerified eventを受け取り、顧客のくクレジットカードに請求をして、CreditCardAuthorized eventを発行する
  6. Kitchen ServiceはCreditCard Authorized eventを受け取り、TicketをAWAITNG_ACCEPTANCE状態に変更する
  7. ORDER SERVICEはCreditCardAuthorized eventを受け取り、OrderをAPPROVEDに変更して。OrderApproved eventを発行する
    クレジットカードの認証に失敗しオーダー作成が失敗するパターン
  8. Order ServiceはAPPROVAL_PENDING状態のorderを作成し、OrderCreated eventを発行する
  9. Consumer Serviceが、OrderCreated eventを受け取り、顧客がオーダー可能かを判定し、ConsumerVerified eventを作成する
  10. Kitchen ServiceはOrderCreated eventを受け取り、オーダーのバリデーションを行い、CREATE_PENDING状態のTicketを作成し、TicketCreated eventを作成する
  11. Accounting ServiceはOrderCreated eventを受け取り、PENDING状態のCreditCardAuthorizationを作成する
  12. Accounting ServiceはTicketCreated eventとConsumerVerified eventを受け取り、顧客のクレジットカードに請求をしたが失敗しCreditCardAuthorized Failed eventを発行する
  13. Kitchen ServiceはCreditCard Authorized eventを受け取り、TicketをREJECTED状態に変更する
  14. ORDER SERVICEはCreditCardAuthorized Failed eventを受け取り、OrderをREJECTEDに変更する 6と7でオーダーとチケットを拒否しているのが成功パターンと違う点です。

このパターンはわかりやすい反面、以下のような欠点があります。 - Sagaパターンの実装が各サービスに分散する - 相互依存になる - 密結合になるリスクがある

「 Sagaパターンの実装が各サービスに分散する」と言うのは、各サービスがSagaを意識した実装を行わなければならないと言う意味です。OrchestrationパターンはSagaを意識するのがOrchestratorのみで済みます。 「相互依存になる」と言うのは、補償トランザクションを実行している部分を見ると、Order ServiceもAccounting Serviceに依存しているし、Accounting ServiceもOrder Serviceいることがわかります。このようにChoreographyパターンは相互依存を生んでしまいます。 「密結合になるリスクがある」と言うのは、各サービスが自分が関連するサービス全てのイベントをサブスクライブしないといけず、サービス同士が密に結合してしまうと言うことです。 以上のような欠点があることから一般的には次のOrchestrationパターンが良く使われるすです。

Orchestrationパターン

こちらのパターンはSaga orchestratorを用意し、各サービスの調整をOrchestratorが行うパターンです。

f:id:harada-777:20210328155442p:plain:w600

Order Serviceの中にSaga orchestratorがいるような構成になっています。 オーダー作成が成功した場合以下のような流れになります。 1. Saga orchestratorがVerify Consumer commandをConsumer Serviceが送る 2. Consumer Serviceが Consumer Verifiedと返信する 3. Saga OrchestratorはCreate TicketコマンドKitchen Serviceに送る 4. Kitchen ServiceはTicket Createdと返信する 5. Saga OrchestratorはAccounting ServiceにAuthorize Cardメッセージを送る 6. Accounting ServiceはCard Authorizedメッセージを返信する 7. Saga orchestratorはApprove TicketコマンドをKitchen Serviceに送る 8. Saga OrchestratorはApprove OrderコマンドをOrder Serviceに送る

このパターンの場合、途中で作成が失敗した場合は、Saga orchestratorが補償トランザクション実行のためのイベントを発行します。つまりAccounting ServiceがOrder Serviceを叩くといったことがありません。 つまり、常にSaga orchestratorが各サービスを実行し、その逆がないのでこのパターンの特徴です。なので、Saga orchestratorにSagaに関する知識が集約され、また相互依存がなくなります。 以上2つのパターンについて解説しました。次にこのSagaパターンを実装するに当たっての大切な注意点について述べます。

注意

このSagaパターンを実装するに当たって注意として挙げられているのが、それぞれのlocalトランザクションと、次のServiceへのイベントの発行がアトミックに行わなければならないと言う点です。localトランザクションでDBへの変更ができていても、イベントの発行ができていなければ意味がありません。よってアプリケーション上でDBへの保存、次にイベントの発行というコードを書くことは望ましくありません。 上記を実現するための手段して挙げられているのが、Transaction Log Tailingです。これはDBのコミットログを追い、それらをMessage brokerへのイベントに変換して送信する方法です。こうすることでDBへの変更とイベントの発行がアトミックになります。これらをサポートするサービスとしてDebeziumなどがあるそうです。

感想

Sagaパターン正直難しかったです、、笑 Microservice間においてトランザクションてどうすんやろうと思ってたのですが、Sagaパターンにはある意味泥臭く頑張るという印象を受けました。導入の際は気を付けないとシステムの構成が複雑になってしまうのと感じました。なのでそこまで一貫性を保つことが必須でない場合は、リトライをそれぞれの処理に入れるとか、定期的にバッチ処理で同期させるようにするとか他のアイディアも検討した後に導入すべきだなぁと思いました。ただ個人的には面白そうなのでどっかで実装し見たいと思います! またこの本は他にもサービスメッシュや、E2Eテストの話、Api Gateway、BFFやCQRSなどについても載っているのでMicroserviceに興味のある方は是非お読みください!実際のコードもここに上がっています。