概要
- Java読書会でせっかく勉強したのにつぎつぎと忘れていくので、印象に残ったところを記録していく
4章 サーガによるトランザクションの管理
- マイクロサービスでトランザクションをどう扱うかの話
- マイクロサービスアーキテクチャでは、多数のサービスがあり、それぞれが自分用のデータベースをもっている。
- ではデータの整合性はどうやって維持するのか?
分散トランザクション
- 従来、分散アーキテクチャでは、分散トランザクションが唯一の選択肢かのごとく言われてきた
- 利点
- 単一データベースとプログラミングモデルが変わらない
- ACIDについても、単一データベースと同様に期待できる
- 欠点
- 可用性が低い
- すべてのサービスが生きてないといけないので
- 同期的
- そもそもそういうプログラミングモデルなので
- サポートがない場合もある
- RDBMS以外が入ると、サポートされてない方が普通
- 総じて密結合
- 可用性が低い
マイクロサービスアーキテクチャの趣旨は、非同期的・粗結合で、全体としては高可用性を実現するものなので、分散トランザクションの欠点の方と相容れないです。
「Sagaパターン」
- 処理全体をトランザクションに収めることができないならということで、Sagaパターンです
- Sagaパターンでは、サービスをまたがる処理を行うときは、各サービス内ではトランザクションを使い、全体としてはトランザクションのシーケンスとします
- 例:createOrderSaga: 以下は一つ一つがトランザクション
- オーダーサービス:オーダ作成
- 顧客サービス:顧客承認
- キッチンサービス:チケットの作成
- 会計サービス:カードの承認
- キッチンサービス:チケット承認
- オーダーサービス:オーダー承認
- ロールバック
- トランザクションがぶつ切りになっているので、一発ロールバックは無理
- 途中で失敗した場合は、まずサービス内でRDBMSのロールバックを使い、それよりも前のコミット済みのトランザクションに対しては、それを打ち消すようなトランザクションをアプリロジックで実現するしかない
- 用語
- 補償可能トランザクション:ロールバックが必要な処理。上記の例の最初の3つ。
- 再試行可能トランザクション:失敗しない処理。上記の例では最後の2つ。
- ピボットトランザクション:補償可能と再試行可能の間。上記の例では、カード承認
ところで再試行可能トランザクションは、ビジネスロジック上失敗しないというだけで、IOなど他の要因で失敗したらどうするのだろう?
サーガのコーディネート
-
「サーガ = トランザクションのシーケンス」なわけだが、それをどう構築するか?
-
コレオグラフィベースのサーガ
- 感想:コレオグラフィベースは実用性に乏しいので忘れてよいと思う
- オーダー作成⇒顧客承認の流れは以下のようにつなげる
- オーダー作成
- ⇒ OrderServiceがOrderCreatedEventを発行
- ⇒ 顧客サービスがそのイベントを受信
- ⇒ 顧客サービスが顧客承認をする
- もちろんリスナー登録は事前に済ませておく
- 全体を制御するようなものが存在しないのが特徴。一見、粗結合に見えるが、実現したいシーケンスが成り立つようにイベント発行をし、さらにリスナー側が処理をできるように必要な情報を盛りだくさんにしないといけないので、実は密結合になる
- オーダーサービスが顧客サービスの結果を受け取るには、やはりリスナ登録が必要になるので、サービス間が双方向参照になってしまう。
- さらに全体を制御するコードがないということは、シーケンスに変更が必要になった場合は、全体の構成を見直さないといけない
- 正直、欠点だらけで、よほど簡単なものしか実現できないと思う
-
オーケンストレーションベースのサーガ
- 全体を制御するCreateOrderSagaのようなクラスを作って、ここから下位のサービスに対して指示を出す
- 普通に考えてこれになる
- 非同期処理の連続になるので、必然的に状態マシンとしてモデリングされる
- 注意事項としては、Sagaクラスは全体のコーディネートだけするようにして、それよりも詳細なビジネスロジックはなるべく持たないようにした方がよい
分離性の欠如への対処方法
-
「サーガ = トランザクションのシーケンス」なので、分離性はまったくない
-
更新の上書きであったり、ダーティーリードなどは当然発生しうる
-
例えば、CreateOrderSagaをやっている途中に、CancelOrderSagaが追い抜いて、全体の整合性が壊れてしまうことがありえる
-
カウンターメジャー(対処方法)
- いくつか紹介されているが、現実的に採用するのは、ほぼSemantic Lockになりそうな気がする
- Semantic Lockでは、処理中のエンティティに「XXX中」というような状態を設けることで、他のSagaがアクセスしよとしたときに、処理を失敗させるなり、ブロックさせるなりできるようにする。
- 名前の通りアプリケーションレベルで「意味論的なロック」を実現する
- DBMSの変わりに自分でロックしていると言えばそれまでか。
- 他の方法に比べて、分かりやすく、汎用性がある
オーダーサービスとCreate Order Sagaの設計
- 著者の作ったEventuage Tram Sagaフレームワークをつかった実装例についての話
- オーケストレータベースのCreateOrderSagaを作る場合、実装上はCreateOrderSagaクラスと、CreateOrderSagaStateクラスに分かれる
- CreateOrderSagaクラス
- オーケストレーションロジックの定義のみ
- 上の方で書いた「例:createOrderSaga」の箇条書きと対応するロールバックを並べているだけの、ほぼ静的な情報のみ
- CreateOrderSagaStateクラス:以下のようなものが定義されている
- Sagaとしての状態
- 下位のサービスへのメッセージの作成
- 下位のサービスから返事を受け取ったときの処理
- Eventuate Tram Sagaは、CreateOrderSagaStateの永続化もしてくれるので、この点は楽だと思う
まとめ
- 誰もが思う「マイクロサービスアーキテクチャってトランザクションどうするの?」への回答
- 全体トランザクションは無理。各サービス内でDBMSのトランザクション使ってください
- 全体ロールバックは、打ち消し処理を、自分で書いてください
- 分離性がないので、各レコードにロック状態のフィールド使って、自前で分離してください
- 簡単に言えば、「自力で全部がんばれ!」
- トランザクションのような難しい処理は、車輪の再発明をするのではなく、信頼できるライブラリを適切に使うのがこれまでの常識だったが、それを破ってでもマイクロサービスはやるべきものなのか?
- 答えは常に「要件次第」か?!