マイクロサービスアーキテクチャにおけるデータ整合性について

<!--
タイトル：マイクロサービスアーキテクチャにおけるデータ整合性について
 -->

（株）日立製作所 研究開発グループ データマネジメント研究部の大越です。本研究部では，データ管理技術を中心とした研究開発を推進しています。本稿では，近年注目を集めるマイクロサービスについて，データ管理の観点から，サービス間のデータ整合性の問題について詳しく見ていきたいと思います。

# 1. マイクロサービスにおけるデータ整合性とSagaパターン

近年，マイクロサービスアーキテクチャ（以下，MSA）と呼ばれる設計手法，もしくはシステムアーキテクチャが注目を集めています。 MSAにおいては，システムを独立してデプロイ可能な小さなサービス（マイクロサービス）の集合体として構築することで，スケーラビリティやアジリティの向上を実現しています。その一方で，MSAではサービスごとにデータベースを保持するアーキテクチャ（[DB per Service](https://microservices.io/patterns/data/database-per-service.html)）が一般的であるため，複数のマイクロサービスに跨って処理を行う場合，サービス間のデータの整合性について考慮する必要があります。

サービス間のデータの整合性を維持するデザインパターンの一つとして[Sagaパターン](https://microservices.io/patterns/data/saga.html)が良く知られています。Sagaパターンでのデータ整合性維持を下図を用いて詳しく説明します。

![saga.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2479419/26940055-69bf-7cab-7619-69c7f6ad21e0.jpeg)

Sagaパターンは各サービスにおけるトランザクション（ローカルトランザクション）を連結させ，ある種のワークフローを構築することでサービス間のデータの整合性を維持するデザインパターンです。図のSaga（正常時）の様にマイクロサービス内で完結するローカルトランザクションと，サービス間でのメッセージ伝搬（伝搬方法のバリエーションについてはSagaの調整方法として後述します）によりローカルトランザクションを順に実行することによりサービス間でのデータ整合性を維持します。何らかの異常（典型的にはサービス障害など）で継続が難しくなった場合は，図のSaga（異常時）の様にローカルトランザクションの打ち消し処理に相当する補償トランザクションをローカルトランザクションが実行済みのサービスでそれぞれ実行することにより，トランザクション処理でのロールバックに近い挙動をさせることでデータの整合性を維持します。

![coordinate.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2479419/c353a645-a078-1600-70d5-1ea303c01d11.jpeg)

Sagaの調整方法としてオーケストレーション（Orchestration，図左）やコレオグラフィ（Choreography，図右）が存在します。オーケストレーションは，オーケストレータと呼ばれる特別なマイクロサービスが全体の調停者となり，各マイクロサービスに対しローカルトランザクションを要求することで，Sagaフローの実行を管理します。一方，コレオグラフィは，各マイクロサービスが調停者なしにローカルトランザクションの実行と，別サービスのローカルトランザクションのトリガの発行を行います。それぞれPros/Consがありますが，一般には，複雑なSagaフローの場合はオーケストレーションが，単純なSagaフローの場合はコレオグラフィが好適とされています（参考：[Saga distributed transactions pattern](https://docs.microsoft.com/en-us/azure/architecture/reference-architectures/saga/saga)）。

# 2. Sagaパターンにおけるデータ整合性維持の課題

Sagaパターンにおいては，実施したローカルトランザクションを打ち消すトランザクション（補償トランザクション）を備えることにより，多くのデータ不整合を防止することが可能となります。一方，補償トランザクションのみでは防ぐことのできないデータ不整合も存在します。この補償トランザクションのみでは防ぐことのできないデータ不整合について，典型的なSagaパターンを採用したシステムの例（資金移動サービス）を用いて掘り下げてみたいと思います。

![flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2479419/fe9f3c6e-5402-7ace-ab11-204152ab731d.png)

図は資金移動サービスのシーケンス図です。なお，Sagaの調整方法はオーケストレーションを想定します。基本的な処理の流れは次の通りです。
 
1. ユーザは，資金移動サービス（オーケストレータ）に対し資金移動依頼を出す。
1. 資金移動サービス（オーケストレータ）は，口座A（マイクロサービスA）に対し，出金依頼を出し，出金処理を行う。
1. 資金移動サービス（オーケストレータ）は，口座B（マイクロサービスB）に対し，入金依頼を出し，入金処理を行う。
1. ユーザは，資金移動サービス（オーケストレータ）から資金移動結果を受け取る。

![flow_compensate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2479419/782fca22-6d26-21e9-29f4-3c73cbcd3180.png)

加えて，Sagaにおける補償トランザクションによるデータ整合維持について見てみます。図は口座Bに対する入金依頼において処理が失敗した例です。この場合，すでに出金済みの口座Aに対し，出金依頼のローカルトランザクションと対になる打ち消しのローカルトランザクション（補償トランザクション）として出金取消を行います。これにより，口座A，Bとも資金移動依頼前と同様の状態となり，データ整合性が保たれます。

続いて，もう少し状態を複雑にしてみます。資金移動サービスは自身のサービス障害に備え，適宜，状態の永続化を実施することにしました。このときシーケンス図は次の様になります。

![flow_persist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2479419/f6c1aafc-0630-80fa-ad84-827e4dd8b62b.png)

少々細かいですが，資金移動サービス（オーケストレータ）になんらかの状態遷移が生じるたびに永続化をするものとします。これにより，図の通り出金結果の受信と入金依頼の送信の間で障害が発生し再起動した場合も，（永続化したデータが無事であれば）処理を継続することが可能となります。

ここからは資金移動サービス（永続化あり）のケースで，データ不整合が生じ得るいくつかの典型的な障害パターンについて見ていきたいと思います。

# 2.1 不整合発生ケース1

![flow_failure1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2479419/7967796d-71de-d786-dc35-a03eee28c395.png)

図は資金移動サービスの出金依頼を行い，その永続化を行う間で障害が発生したケースです。オーケストレータは出金依頼を行った直後に障害となり，永続化や出金結果を受信することなく，再起動します。最後に状態を永続化したのは資金移動依頼を受け取った直後のため，この状態より処理を再開しますが，図の通り，出金依頼を実施した状態が永続化されていないため，出金依頼が2重に実施されてしまっています。

# 2.2 不整合発生ケース2

![flow_failure2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2479419/250fac0e-9845-36bc-b874-304c0e7f9201.png)

図は入金依頼の送信がネットワーク障害などにより口座Bに送信されなかった場合を示しています。この場合，資金移動サービスは入金結果の応答待ちとなりますが，口座Bは入金依頼を受け取っていないため，処理が継続できなくなります。入金依頼を再送することも考えられますが，口座Bの受信状況が不明の場合，2重依頼の可能性から送信の判断が難しい場合が考えられます。

# 3. データ整合性維持に関わるデザインパターンの必要性

 前述の例の様に，比較的シンプルなシステムであっても，永続化や障害を考慮すると多くのデータ不整合の発生を考慮したシステム設計が必要となります。ここでは，それらを防止する代表的な処方箋をデザインパターンとして紹介していきます。

# 3.1 Transactional messaging（不整合発生ケース1への処方箋）

 まず，不整合発生ケース1への処方箋である，Transactional messagingについて見ていきます。Transactional messagingは，主にメッセージの送信と内部状態の変更をトランザクションの様に実施するデザインパターンの集合です。具体的には，Transactional outbox，Transactional log tailing，Polling publisherなどのデザインパターンが含まれています（詳細は，[Microservices.io](https://microservices.io/patterns/index.html)の各ページをご参照下さい）。不整合発生ケース1の原因は，出金依頼とその永続化の処理が分割されており，永続化の直前で障害が発生したため処理状態が不定に陥ることにあります。Transactinal messagingは，この様なメッセージの送信と内部状態の更新を一つのトランザクションの様に実現するデザインパターンとなります。これらを用いた典型的なシステムを下図に示します。

![transactionalMessaging.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2479419/190d1ea6-3f7f-76e8-8abc-9d7a4fd6ef8e.jpeg)

Transactional messagingは，状態永続化とメッセージ書き込みをローカルトランザクションとしてデータベースに書き込むTransactional outboxパターンと，Outboxテーブル（メッセージを書き込む特別なテーブル）に書き込まれたメッセージを取得し，他のサービスやメッセージバスなどに送信するTransactional log tailing / Polling publisherパターンを備えています。これらのデザインパターンを組み合わせることにより，状態永続化とメッセージ送信をトランザクションの様に扱うことが可能となります。

実装としては，RDB/Debezium/Kafkaを用いた構成（[Kafkaを用いたマイクロサービスSagaパターンの検証](https://qiita.com/Kiminori-Kurihara/items/24dc08adbb8eeb69ac10)）がその典型です。

# 3.2 Communication style＋α（不整合発生ケース2への処方箋）

 続いて，不整合発生ケース2の処方箋である，Communication styleについて見ていきます。+αとしたのは，ケース2の不整合に対し，Communication styleで定義されているデザインパターン（詳細は，[Microservices.io](https://microservices.io/patterns/index.html)の各ページをご参照下さい）のみでは，不整合発生ケース2に対するデザインパターンとして不足があるためです。 Communication styleは，同期・非同期通信など主に通信に関係するデザインパターンの集合で，具体的には，Messaging，Remote procedure invocation，Domain specific protocolなどが含まれています。これらは，主としてマイクロサービス間の通信をいかに実現するかを定義したもので，データ不整合に対し直接作用するデザインパターンではありません。不整合発生ケース2では，+αとした，Retry，Idempotent Publisher/Consumerなど前述のデザインパターンに付随させて用いるデザインパターン群が主に必要となります（詳細は後述）。

 ケース2を改めて見てみると，入金依頼のメッセージが途中で消失し，資金移動サービスは処理を継続することができなくなっています。ここでメッセージの再送が必要となり，この再送処理を担保するのがRetryパターンとなります。一方，愚直にRetryを行う場合，口座Bの状態を考慮すると，
 
 （1）入金依頼を受け取っており，処理が正常に行われた

 （2）入金依頼を受け受け取ったが，処理に失敗した
 
 （3）入金依頼を受け取っていない

 などの状態が考えられます。このため，シンプルかつ汎用的な解として，成功するまでRetryし，成功した時点で処理を打ち切る，というアプローチが考えられます。しかし，単純なRetryでは同様の要求処理（ここでは入金依頼）が複数回送られてしまうため，「処理要求が複数回送られても同じ状態に収束すること」（＝冪等性）の担保が必要となります。これを実現するデザインパターンがIdempotent Publisher/Consumerとなります。ここでは，構成例として冪等IDを用いたIdempotent Publisher/Consumerについて説明します。

![idempotent.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2479419/48889d0e-8c92-09f3-3af5-770b1f8b622a.jpeg)

 この構成例では，メッセージの送信に対し，受信を行えなかった場合に再送するRetryパターン，送信メッセージに対し冪等IDを付与するIdempotent Publisherパターン，同じ冪等IDを持つメッセージの読み捨てを行うIdempotent Consumerパターンが適用されています。具体の実装はアプリケーションに依存しますが，典型的には下記のような実装が考えられます。
 
 - Retry：試行回数をパラメータとして指定し，エラー時に規定回数の試行（Retry）を実施する。
 - Idempotent Publisher：メッセージ送信時に冪等IDをメッセージに付与する。なお，Retry時は同じIDを再利用する。
 - Idempotent Consumer：冪等IDと処理状態を保存したデータベースを用いて冪等性を実現する。具体的には，未処理の冪等IDを含むメッセージを受信した際は，当該冪等IDをデータベースに追加し，処理状態を更新した後，応答メッセージを生成しデータベースに保存するとともに応答メッセージを送信する。なお，処理済みの場合は内部処理をスキップし，過去の応答メッセージを再送する。
 
 これらの適用により，送信失敗時にはRetryが行われ，仮に多重にメッセージの送受信が行われた場合でもIdempotent Publisher/Consumerにより同じ状態に収束することが担保されます。

 実装としては，[Eventuate](https://eventuate.io/)でメッセージIDを用いた冪等性の実装例がある様です。

# 4. まとめ
 
 本稿では，マイクロサービスでデータ整合性維持に用いられるデザインパターンであるSagaパターンと，その他のデータ整合性維持に関連するデザインパターンについて紹介しました。もちろん，これらのデザインパターンのみでは不十分であり，さらなる拡充とその体系化が必要です。 一方，Sagaの実行で残ってしまうデータの不整合や並列実行で生じ得るデータ不整合などについては，さらに考えを深めていく必要があります。

# 補足

本稿記載の会社名，製品名，もしくは固有名詞は，それぞれの会社の商号，商標，もしくは登録商標です。