メインコンテンツまでスキップ

分散トランザクションと Seata

· 約8分

エンタープライズアプリケーション開発では、分散フレームワークの発展に伴い、本番環境には多くのデータベースインスタンスが存在するようになりました。特にマイクロサービスの分野では、通常、各ビジネス Service モジュールごとに自身のビジネス専用の DB ストレージノードを対応させ、さらにそのストレージノードを高可用構成でデプロイします。

トランザクション問題

Service のビジネスロジックを書くとき、1 つのビジネス操作はしばしば他のビジネスモジュールをリモート呼び出しし、生成されたデータは異なるストレージノードに永続化されます。複数のデータベースインスタンス間でトランザクションの ACID 特性を保証しようとすると、分散トランザクションの問題に直面します。

分散トランザクションが実現すべきことは、次の 2 点に要約できます。

1)ビジネスの呼び出しチェーン全体がすべて成功した場合、その呼び出しチェーンに対応するデータベースすべてでトランザクションをコミットする。

2)呼び出しチェーンの中で例外がスローされた場合、その呼び出しチェーンに対応するデータベースすべてでロールバックを行う。

Seata の役割分担

Seata の分散トランザクションソリューションには、一般に次のような役割があります。

  • TC (Transaction Coordinator): トランザクションコーディネーター。
  • TM (Transaction Manager): トランザクションマネージャー。AP 側に組み込まれます。
  • RM (Resource Manager): リソースマネージャー。ブランチトランザクション処理のリソースを管理し、TC と通信してブランチトランザクションの登録と状態報告を行い、ブランチトランザクションのコミットまたはロールバックを駆動します。
  • AP (Application Program): RM にアクセスするアプリケーション。

図中の TC トランザクションコーディネーターコンポーネントは単独でデプロイし、マイクロサービスクラスタ全体のレジストリに参加させる必要があります。現在一般的なレジストリはすべてサポートされており、Nacos、Zookeeper、Etcd、Eureka、Consul などが使えます。同時に AP 側で対応する接続設定を行う必要があります。TC モジュールは高可用性を必ず保証しなければなりません。このモジュールがダウンすれば、分散トランザクションは成り立たなくなります。

トランザクショングループと高可用性

剛性トランザクションと柔軟トランザクション

剛性トランザクション: 通常ビジネス側の改修は不要で、強一貫性を持ち、ロールバックと分離性をネイティブにサポートします。並行性は低く、短いトランザクションに適しています。対応する方式: XA プロトコル(2PC、JTA、JTS)、3PC。

柔軟トランザクション: ビジネス側の改修が必要で、結果整合性となり、補償インターフェースとリソースロックインターフェースの実装が必要です。高い並行性に対応でき、長いトランザクションに適しています。対応する方式: TCC/FMT、Saga(ステートマシンモード、Aop モード)、ローカルトランザクションメッセージ、メッセージトランザクション(ハーフメッセージ)。

では、長いトランザクションとは何で、短いトランザクションとは何でしょうか。

長いトランザクション: あるリソース(X ロック、ギャップロック、テーブルロック、ページロック)を長時間占有し、更新するデータが多い、または実行速度が遅く、ビジネスフローが長いものです。他のトランザクションがそのリソースにアクセスするとブロックされて待たされ、デッドロックを引き起こしやすくなります。

短いトランザクション: リソースの占有がごく短時間で、更新するデータが少ない、または実行がすぐ完了し、ビジネスフローが短く、トランザクションがすぐに終わるため、デッドロックが発生する確率は小さくなります。

4 つのトランザクションモード

現在 Seata は 4 種類の分散トランザクション管理モードをサポートしています。

AT モード

AT モードは Seata の中で最も特色のあるモードと言えます。要するに最適化された 2PC であり、二相コミットの一種です。このモードを使うには、ローカルデータベースノードに undolog テーブルを作成する必要があります。プロキシの DataSourceProxy がアプリケーションの実行する DML SQL 文をインターセプトし、SQL のセマンティクスを解析してクエリ SQL に変換し、実行前の対応するデータのミラーを undolog テーブルに保存して、第二段階のロールバック用とします。以上のステップはすべてプロキシパターンで完結するため、ビジネスロジックコードへの侵入はゼロで実現されています。

しかしこの方式は完璧ではなく、パフォーマンスの損失がかなり大きくなります。1 回の DML SQL につき、セマンティクス変換とクエリ SQL の実行が 1 回、そしてデータミラーの undolog テーブルへの追加が発生します。第二段階のロールバックが発生した場合はさらにローカルロックを取得し、元のデータミラーへ巻き戻す必要があります。加えて、もう一つ深刻な問題があります。ロールバックの際に、他のローカルトランザクションが Seata のグローバルトランザクション管理を通さずグローバルロックを取得しないまま、ローカルトランザクションで undolog 内のあるデータミラーに対応するデータを直接更新してしまうと、Seata はロールバック時にロールバックミラーと現在のローカルデータが一致しないことを検出し、データ喪失の問題が発生します。これは CAS における ABA 問題に少し似ています。

TCC モード

TCC モードは比較的シンプルで、実際にはこれも 2PC の二相コミットです。AT モードとの違いは、第一段階の prepare(準備)、第二段階の commit、そして rollback のロジックを自分で実装する必要がある点で、Seata のトランザクションマネージャーがこれらの実装メソッドを順に呼び出します。

Saga モード

Saga は一連のローカルトランザクションで構成されます。各ローカルトランザクションはデータベースを更新した後、メッセージまたはイベントを発行し、Saga 内の次のローカルトランザクションの実行をトリガーします。あるローカルトランザクションがビジネスルールを満たせず失敗した場合、Saga はその失敗したトランザクションより前にすでにコミット済みのすべてのトランザクションに対して補償操作を実行します。

そのため Saga は実装が最も手間のかかるモードと言えますが、Saga ベースのステートマシンによって、分散トランザクションの各ステップを非常に柔軟に制御できます。長いトランザクションのシーンでは、なおさら自分できめ細かく各ケースを制御する必要があります。分散トランザクション全体の状態に注目するのではなく、各イベントの処理と、自身のメソッドの冪等性さえきちんと担保すればよいのです。ただし Saga のトランザクションには段階的コミットという概念がなく、すべてローカルで直接コミットされます。ロールバックの途中で他のトランザクションがそれらのデータを読み取り、更新してしまうと、ダーティリードやダーティライトが発生します。この状況については、自前で txid の仕組みを実装して回避する必要があるだろうと考えています。

XA モード

XA モードはまずデータベースが XA プロトコルをサポートしていることを前提とします。その実装には 2PC、3PC があり、完全にデータベース自身が提供するため、アプリケーション側は何の処理も必要ありません。本質的には AT モードとよく似ていますが、違いは、データベースが XA トランザクションを実行する過程で、強一貫性のためにデータリソース(X ロック、ギャップロック、テーブルロック、ページロック。データ範囲によって異なります)を占有し続ける点です。長いトランザクションの場合、高並行のシーンでは非常にデッドロックが発生しやすく、データベースのパフォーマンスもかなり大きく低下します。すべて短いトランザクションであれば、このモードを使うことができます。

まとめ

分散トランザクションの本質は、DB を跨ぐ呼び出しチェーンを、一緒にコミットするか、一緒にロールバックするかのどちらかにすることです。Seata は TC、TM、RM の役割分担によってこれをプロダクトとして仕上げ、AT、TCC、Saga、XA の 4 つのモードを提供しています。AT はデータミラーによってゼロ侵入を実現し、ほとんどの一般的なシーンに適しています。TCC と Saga は補償ロジックをビジネス側に委ねることで、より高い並行性と柔軟性を手に入れます。XA はデータベースのネイティブサポートに依存し、短いトランザクションにのみ適しています。選定の際は、まずビジネスが長いトランザクションか短いトランザクションか、強一貫性を求めるのか結果整合性で許容できるのかを見極めたうえで、当てはまるものを選びましょう。

COMMENTS