以前の記事で『Microservice Patterns』について要約したが、その中の一つの Saga パターンについて、もう少し詳しく掘り下げてみる。
どういう文脈で Saga パターンを使うか?
各サービスがそれぞれの Bounded Context (整合性の境界)で自前のデータストア(Database per Service)を持っているマイクロサービスアーキテクチャで、複数サービスにまたがるワークフローのデータ整合性を維持したい。
どういう制約のもとで Saga パターンを使うか?
以下のような事情で、分散トランザクションは使いたくない。
- モダンでメジャーな NoSQL やメッセージブローカではサポートされていないものが多い。
- CAP 定理の認知度が高まって、Consistency を絶対視する風潮が見直され、Availability をより重視するシステムも増えている。
- 分散トランザクションは大きな同期プロセス間通信システムとも見ることもでき、可用性を損ねる要因になりうる。1
- トランザクションが提供するような「幻想2」に頼らなくても、実際、現実世界は結果整合性で回っている。3
Saga とはどんなパターンか?
『Microservice Patterns』では第4章全体が Saga パターンの記述で文量が多い。ここではポイントを絞ってみる。
Saga
複数のマイクロサービスのローカルトランザクション(後述)を非同期メッセージングでつなげて、サーガと呼ばれるワークフローを構成するパターン。上の「制約」に書いたとおり分散トランザクションは使わず、Compensation4(補償)と countermeasures で、結果整合性(Eventual consistency)を実現する。
Choreography 型と Orchestration 型
Saga パターンの型としては、Choreography 型と Orchestration 型の二つがある。前者は、各サービスが互いの実行結果に応じて協調して処理を実行し、最終的にワークフローを完遂するもので、単純で小規模なマイクロサービスシステムに向いている。
後者は、中央集権的なオーケストレータがサーガの進行を管理するもので、Choreography 型に比べてより複雑で大規模なフローにも向いている。以降、Orchestration 型での構成をメインに記述する。
ローカルトランザクション
オーケストレータからのメッセージを契機に各マイクロサービスにより実行され、ローカルな ACIDトランザクションとしてその都度コミットまたはロールバックされるサーガ内のステップ。必要に応じてオーケストレータにリプライを返すことになるが、データの更新とメッセージングがアトミックになるように、Transactional Messaging5 のパターン群などを併用する必要がある。
ローカルトランザクションの種類
ローカルトランザクションには以下のような種類がある。
Compensatable Transaction
補償 (compensate)可能な、つまり状況に応じて後で変更を打ち消すことができるローカルトランザクション。実際に補償を実行する Compensating Transaction(後述)と対になる。
Pivot Transaction
サーガの構成として、前半に Compensatable なトランザクションの一群を置き、後半に Retriable(次に記述)な一群を置く。ここで Compensatble と Retriable の境界に位置するローカルトランザクションを Pivot Transaction と呼ぶ。Compensatable でも Retriable でもないか、あるいは「最後の Compensatable」かつ「最初の Retriable」であるかの場合がある。
Retriable Transaction
Pivot 以降にまとめて置かれるトランザクションで、ロールバック不要で成功が保証されているもの。たとえば、永続化された Aggregate の状態(後述)を「承認待ち」から「承認済み」に変更するだけのようなトランザクション。
Compensating Transaction
Compensatable Transaction に付随するもので、サーガが中止される場合には、対応する実行済み Compensatable Transaction と逆順で実行される。ACID トランザクションのロールバックに相当するが、変更を中止するというより、打ち消すトランザクションを実行するという点で、Git の revert コミットに近いイメージ。
状態
サーガの進行状況を表す状態と、各サービスがもつドメインのデータの状態の二つがある。前者はサーガの進行状況をステートマシンとして表したもので、ステップごとに永続化される。テスタビリティを高める効果もある。
後者は各サービスが担当する Aggregate の状態フィールドで、例えば、「承認待ち」、「承認済み」、「却下」といったものになる。後述の semantic lock としても機能する。
Countermeasure
「制約」に書いたとおり分散トランザクションを使わないため、ミドルウェア的な排他制御が行われず ACID の I(Isolation)が欠如して ACD となる。このため複数のサーガが並行して実行される際、そのままでは Lost Update6 や Dirty Read 7 といった「異常(anomaly)」が生じうる。これを防ぐ技法が countermeasure で、Semantick Lock、Commutative Updates8、Pessimistic View9、Reread Value10、Version File11、By Value12 といったものがある。Saga パターンでは Semantic Lock を中心に、必要に応じて他の countermeasure を併用する。
Semantic Lock
従来の DBMS によるロックではなく、アプリケーションレベルで実装するロック。各サービスが担当する Aggregate ごとに状態フィールドを管理する。
排他制御の振る舞いは要件による。例えばロック検出時の処理は以下のような選択肢がある。
- 単に失敗する。サービス側の実装が楽な反面、クライアント側でリトライする負担が生じうる。
- 旧来の ACID のようにブロックする。クライアントの負担が減る一方で、デッドロック検知の仕組みを自前で実装するなどの負担も生じうる。
参考文献
- Microservice Patterns
- Reactive Microsystems
- Semantic ACID Properties in Multidatabases Using Remote Procedure Calls and Update Propagations.
- https://microservices.io/patterns/data/saga.html
-
Two-phase commit is the anti-availability protocol. (Pat Helland) ↩
-
"... illusion that the world consists of a single globally strongly consistent present"(『Reactive Microsystems』) または "... the illusion that each transaction has exclusive access to the data" (『Microservice Patterns』) ↩
-
"Don't ask permission--Guess, Apologize, and Compensate" (Reactive Microsystems)、"It is often easier to ask for forgiveness than to ask for permission"(Grace Hopper) ↩
-
Transactional outbox,Transaction log tailing, Polling publisher などのようなマイクロサービスパターン ↩
-
あるサーガによる変更を、他のサーガが変更前のデータを使って上書きしてしまうこと。 ↩
-
進行途中の別サーガによる変更を読み取ってしまうこと。その別サーガがその変更をキャンセルすると整合性が壊れる。 ↩
-
更新操作が可換になるように、つまり実行順序によらず整合性が維持されるよう工夫する技法。Lost Updates 防止に効果がある ↩
-
不整合を発生させ得るステップをサーガの終盤に寄せるなど、ビジネスリスクを最小化するよう構成する技法 。Dirty Read 防止に効果。 ↩
-
Dirty Write 防止のために、更新前に念のためもう一度読み込んでみる技法. Lost Updates 防止に効果。参考:Optimistic Offline Lock pattern. ↩
-
非可換な操作群を記録しておいて、必要に応じて組み替えて可換にする技法。 ↩
-
リクエストの内容によって動的にロジックを切り替える. 場合によっては分散トランザクションも使う。 ↩