マイクロサービスのような分散システム環境においては、複数のサービスに跨ってデータの整合性を保つことが、 モノリシックなアプリケーションの場合と比較して困難である。 単一のDBであればトランザクションが使えるが、マイクロサービスではそうでないためだ。

Transactional Integrityというアプローチでは、分散システム環境におけるデータの整合性を維持するためのテクニックがある。

Eventual Consistency

Eventual Consistencyは、日本語では結果整合性と訳される。 RDBMSにおける悲観的ロックのような、厳密な一貫性を要求する考え方とは異なる。 それは、一時的に不整合が発生しても問題なく、結果的に一貫性が保たれれば良いという考え方に 基づいている。

マイクロサービスにおいて、1つのリクエストが複数のサービスに跨ってかつ、複数のデータを更新するとき、 Eventual Consistencyの考え方を採用することができる。 つまり、各マイクロサービスはリクエストに対して実際のデータ更新を行う前に即座にレスポンスし、 その後非同期的にデータ更新を行う。

データ更新を非同期的にノンブロッキングで行うため、どこかのマイクロサービスが不具合を起こしてもリクエストが止まることがないことがメリットだ。

Correlation ID

マイクロサービスでは、複数のサービスがひとつのリクエストを扱うため、リクエストのログの追跡が難しくなる。 これに対する解決策が、Correlation IDである。

Correlation IDは、複数のマイクロサービスがひとつのリクエストに対して共有するIDだ。 ログは基本的に各マイクロサービスがストアするが、それとは別にすべてのサービスのログを時系列順に、同じ画面で見るための方法があるとよい。

Correlation IDの発行はAPI Gatewayのレイヤーが行う。 また、ライブラリを提供するなど、全てのサービスが同じフォーマットでそれをロギングする仕組みを作る必要がある。

Rollback and Staged Commit

すべてのマイクロサービスがロールバックをサポートする(ロールバックのAPI/RPCを提供する)というパターンもある。 ロールバックをサポートする場合は、トランザクションIDのようなものをマイクロサービスが発行し、そのIDを使ってロールバックのリクエストを受け取れるようにする。 ロールバックの際は、変更を元に戻すだけでなく、通知など、別のアクションも取れるようにすることもできる。 また、Staged Commitをサポートし、ロールバックだけでなくコミットのAPI/RPCを提供することも可能だ。 Staged Commitでは、2 Phase commitのように、各マイクロサービスがcohortsとなって、「コミットせよ」または「ロールバックせよ」というメッセージを受け取るのを待つ。

これらは分散トランザクションを実現するための基本的な機能であるが、ここで重要なのは、ロールバックやコミットを行うのは誰か?という問題である。 それを解決するのが、次に説明するTransaction Managerである。

Transaction Manager

Transaction Managerは実態はマイクロサービスであり、関連のあるマイクロサービス群に対してロールバックやコミットなどのアクションを行う。

まずは、失敗ケース(つまり複数のマイクロサービスについてロールバックしたいケース)を考えよう。 例えば、あるマイクロサービスがビジネスロジック的に失敗したとき(コードのエラーではない)、そのマイクロサービスはTransaction Managerにロールバックのリクエストを送信する。 その際は、 Correlation ID を併せてTransaction Managerに渡す。 Transaction ManagerはCorrelation IDを元にマイクロサービスのリレーションを理解することができ、必要な各マイクロサービスに対してロールバックのメッセージを送信する。

次に成功ケース(コミットを行いたいケース)について考えよう。 マイクロサービスは自身で、コミットを行える直前の状態まで到達した時点で、Transaction Managerに対してCorrealtion IDと共に、「合意」メッセージを送信する。 Transaction Managerはすべてのcohortsから合意メッセージを受け取った時点で、各マイクロサービスにコミットを指示する。

これらを実現することで、マイクロサービスのデータの整合性は結果整合性という形で保たれる。

筆者の感覚的には、Staged Commitは必ずしもマイクロサービスアーキテクチャにおいてサポートする必要はないと考えている。 各マイクロサービスは即座にデータ更新をコミットし、ロールバックを指示された場合にはロールバックする、というアーキテクチャでじゅうぶんにシンプルで、 データの整合性を維持できるためである。ただし言うまでもなく、データが不整合な状態になっている時間は長くなる。

Transaction Managerは、それ自体が応答不能になるなどでメッセージがロストすることはあってはならないため、Cloud Pub/SubやAWS SQSなどを用いるのが良い。

ロールバックを実現するためのEvent sourcing

Staged Commitを行わない場合、データは即コミットされるため、ロールバックをRDBのように単純には行えない。このとき、データ更新を所謂CRUDで実装していては、前の状態に戻すことは不可能である。 CRUDでは単一のデータの状態を書き換えてしまうため、履歴情報は保持していないためである。履歴テーブルを作るなどのテクニックもあるが、このケースではEvent sourcingが相性が良い。

例えば、ユーザのEmailを更新するケースを考える。 CRUDでは、以下のようなフローを踏むだろう。

id name email
1 Bob bob@example.com

id name email
1 Bob bob2@example.com

id name email
1 Bob bob3@example.com

Event sourcingパターンでは、以下のようになる。

Event ID occurred recorded name email
1 2019-04-17 12:30:00 2019-04-17 12:30:00 Bob bob@example.com

Event ID occurred recorded name email
1 2019-04-17 12:30:00 2019-04-17 12:30:00 Bob bob@example.com
2 2019-04-17 12:35:00 2019-04-17 12:35:00 Bob bob1@example.com

Event ID occurred recorded name email
1 2019-04-17 12:30:00 2019-04-17 12:30:00 Bob bob@example.com
2 2019-04-17 12:35:00 2019-04-17 12:35:00 Bob bob1@example.com
3 2019-04-18 18:30:00 2019-04-18 18:30:00 Bob bob2@example.com

Event sourcingパターンは、CQRSを実現するためのものとよく言われるが、マイクロサービスアーキテクチャでは一般的なデータアーキテクチャである。 特に、どのサービスによってデータが更新されたのかを記録できるため、ロールバックとは異なる文脈でも有用である。

MDMでデータの整合性をチェックする

MDMはMaster Data Managementの略で、データの整合性をチェックするためのコンポーネントである。 Hourlyなどでfunctionを走らせ、マイクロサービスを跨いでデータの一貫性をチェックする。 通知を行い人間がデータを修正するか、データの修正までMDMが行うかは選択の余地がある。 MDMについては別途記事を書こうと思う。

参考

A Modern Data Architecture for Microservices
Event sourcing
Perspective on Architectural Fitness of Microservices