この記事(自著)をDeepLで訳した記事です。特に誤字脱字、明らかな誤訳も修正しておらず、箇条書きと段落の体裁だけ整えています。
以下、DeepLの翻訳。
最近、チーム内でマイクロサービスにおけるトランザクション管理を中心に議論をしました。マイクロサービス指向のアーキテクチャではトランザクションを実装するのが難しいことはよく知られている。この記事では、マイクロサービス指向アーキテクチャにおけるトランザクション管理について、私が現在理解していることを書いてみたいと思います。厳密な技術的な議論をしているわけではない。むしろ、分散トランザクション管理を中心としたいくつかの概念をどのように繋げていくか、ということを検討している。
リカプ トランザクション
まずは取引とは何かをおさらいしておきましょう。個人的には、「取引」という言葉から何か神秘的で難しいものを感じていました。しかし、実はとてもシンプルな概念であることに気づきました。
仮想通貨の仕組みを簡単に例に考えてみましょう。顧客が仮想通貨を稼いだり使ったりすると
- 帳面にレコードを書き込んで
- 顧客残高テーブルにレコードを書きます。
ここで重要なことは、2つのテーブルでこれらの2つのレコードが同期している必要があります。あなたが元帳テーブルで顧客のための1つのレコードを見つける場合、私は意味する、あなたはまた、顧客の残高テーブルで対応するレコードを見つける必要があります。これは "トランザクション "です。トランザクションは、データ操作のコレクションです。そして、それらは、1.すべての操作が成功したか、2.いずれも成功しなかったかのどちらかです。このデータ管理の特性には、データ一貫性という別の名前があります。私の経験上、整合性管理とトランザクション管理は、しばしば交換的に使われています。
例の仮想通貨システムの場合、簡単に言えば、以下の2つの状況を回避する必要があります。
- レコードは元帳表には存在するが、顧客残高表には存在しない
- レコードは顧客残高表には存在するが、元帳表には存在しない
今のところ、難しいことはないですよね。
アールディービートランザクション
障害処理を考慮する必要があるとき、それは複雑になり始めます。はい、コンピュータ・システム、特にI/Oは、失敗する可能性があります。だから、元帳や顧客残高表への書き込みのいずれかが失敗した場合、不整合が発生します。これは問題です。従来のリレーショナルデータベース(RDB)では、このような障害はいわゆる「トランザクション」で処理されます。はい、ここで用語が紛らわしいのは理解しています。ここでいう "トランザクション "とは、前項で説明した "トランザクション "という概念ではなく、RDBが提供する機能の名称です。
ご存知の通り、「RDBのトランザクション」はこんな感じで動作します。成功事例の場合。
- トランザクション開始
- 帳尻を付ける
- 残高表にレコードを書き込む
- トランザクション」を終了します。
失敗した場合のために
- トランザクション開始
- 帳尻を付ける
- 顧客残高テーブルにレコードを書くが失敗する
- をロールバックします。step2の元帳テーブルへの書き込みがキャンセルされるということです。
これで終わりです。まだまだシンプルで良いですね。これで、レコードの書き込みに失敗する可能性がある世界でも、台帳テーブルと顧客残高テーブルの矛盾を回避できるようになりました。
マイクロサービスにおけるトランザクション管理 佐賀県
RDBトランザクション」の限界は、複数のデータベース間で素敵な機能が使えないことです。また、マイクロサービスのアーキテクチャでは、各データベースは独立したサービスによって隠されているのが一般的です。そのため、データの不整合を避けるために、古き良き時代の "RDBトランザクション "を利用できる望みはありません。
例えば、1つのデータベースに2つのテーブルの代わりに、元帳サービスと顧客残高サービスという2つの異なるサービス(またはAPI)があるとしましょう。そこで、仮想通貨管理のための元帳レコードと顧客残高レコードを作成するために、ネットワーク経由でこの2つのサービスを呼び出す必要があります。
RDBトランザクション」を使わずに矛盾を回避するためには、障害処理はどうすればいいのでしょうか?
ちょっと待って、「RDBトランザクション」が書き込みに失敗したときに何をしているのかはすでに説明しました。リモートサービスコールでも全く同じことができるのではないでしょうか?つまり、サービスコールの一つが失敗した場合、関連するサービスコールをすべてキャンセルすることができます。このようにします。
- トランザクション "を開始します。マイクロサービスでは何もしていません。
- 帳票サービスのAPIを作成する
- 顧客残高テーブルの作成APIを呼び出すが失敗
- ロールバックが開始されるので、ステップ2で作成したレコードを削除するために元帳サービスのキャンセルAPIを呼び出す
つまり、元帳サービスも顧客残高サービスも、create APIに加えてcancel (またはdelete) APIをサポートする必要があるということです。cancel APIの実装は多少手間がかかるかもしれませんが、それほど複雑なものではないはずです。
RDBの世界に比べれば、ある程度の余計なロジックを実装する必要がありますが、それでも複雑ではありません。APIの障害を検知したら、関連する呼び出しをキャンセルすればいいだけです。以上です。
このcancel on failureパターンはマイクロサービスの文脈では「Saga」という名前がついています。しかし、「RDBトランザクション」におけるロールバック操作の代替案としては、これがストレートなものだと思う。
さらなる考察。佐賀でキャンセルに失敗したらどうなる?
O.K. これで、マイクロサービスでトランザクションを管理する方法ができました。これは良いことです。しかし、実際には次のような疑問に答えなければなりません。
"ロールバック中にキャンセルAPIが何らかの形で失敗したらどうなるのか?"
再帰的な質問なので厄介です。キャンセルAPIが失敗したときに再試行すればいいのですが、その再試行も失敗する可能性があります。
ここでidempotenceの概念が出てきます。idempotenceという言葉を一度は聞いたことがあると思います。APIがidempotentであれば、複数の同一のリクエストを行っても、1つのリクエストを行ったのと同じ効果が得られるということです。私たちの仮想通貨システムの文脈ではどのような意味があるのでしょうか?
先ほどの例に戻りましょう。 失敗した場合のために。
- トランザクション」を開始します。マイクロサービスでは何もしていません。
- 帳票サービスのAPIを作成する
- 顧客残高テーブルの作成APIを呼び出すが失敗!
- で作成したレコードを削除するために元帳サービスのcancel APIを呼び出す。しかし、なぜかキャンセルに失敗して元帳サービスのレコードが残ったままになってしまう。
このようなケースが発生しています。この後、2つのテーブル間でデータの不整合が発生し、元帳には記録があるが顧客の残高には記録がありません。
全体の操作を再試行するとどうなるのか?
- トランザクション」を開始します。マイクロサービスでは何もしていません。
- 元帳サービスのAPIの作成を呼び出します。覚えておいてください、APIは今はidempotentです。つまり、元帳サービスのレコードは全く変わりません。
- 顧客残高サービスのAPI作成を呼び出します。うわー、今度は成功しました。
- トランザクションを終了します。
これはラッキーなケースです。前回の顧客残高サービスでの失敗は一時的な失敗だったので、リトライはうまくいきます。ここで重要なのは、台帳作成APIはアイドエンプティなので何も考えずに安全に何度も呼び出せるということです。
あ、でも、顧客残高テーブルへの書き込みが成功しなかった場合はどうなるのでしょうか?例えば、入力データが無効な場合や、顧客残高サービスのバリデーションが常に失敗している場合などです。
このケースをシミュレーションしてみましょう。これは少し混乱するシナリオです。最初に、顧客残高の作成に失敗し、元帳上のキャンセルにも失敗したことを覚えておいてください。
- トランザクション」を開始します。マイクロサービスでは何もしていません。
- 元帳サービスのcreate APIを呼び出します。今はAPIがidempotenctになっていることを覚えておいてください。つまり、元帳サービスのレコードは全く変わりません。
- 顧客残高サービスのAPI作成を呼び出します。無効なデータのため、再び失敗します。
- ステップ2で作成したレコードを削除するために、元帳サービスのcancel APIを呼び出します。再び失敗するか、成功するかのどちらかになります。
さて、ここからがこの記事の一番面白いところです。はい、その通りです。2回目のリトライでのロールバックは失敗するかもしれません。しかし、有限のリトライ数では成功するはずです。
有限のリトライ数とはどういう意味か?それを説明するために、失敗を2つの異なるクラスに分類する必要があります。
- 再試行可能な失敗。これは通常、サーバのダウンやネットワーク障害によって引き起こされます。そのため、システムが復旧した後、リクエストはリトライで成功するはずです。
- 再試行不可能な失敗。これは通常、データが無効な場合に発生します。データが無効なため、サービスは意図的にリクエストを失敗させます。これを緩和するには、同じリクエストの単純なリトライが絶対に成功しないように、リクエストの内容を変更する必要があります。
もうお分かりいただけたと思います。そう、佐賀パターンでロジックを構築する際に、秘密のヒントがあります。それは
- Cancel API は Unretryable の失敗で失敗してはいけません。
Creation API は Unretryable 失敗で失敗する可能性があります。しかし、Cancelは失敗しません。
さらに考えてみましょう。すべての失敗が再試行可能な場合はどうなるのか?
最後に考えたことは、上記の質問です。言い換えれば、cancel APIと同じように、すべての作成APIが再試行不可能な失敗で失敗することがない場合、Sageではロールバックステップが必要なのでしょうか?
作成APIがidempotentであることを考えると、ロールバックステップは全く必要ありません。すべての作成APIが成功するまで、すべてのステップを再試行すればいいのです。これの良いところは、キャンセルAPIが不要になることです。いいと思いませんか?
このように、マイクロサービスのトランザクション管理は、次のようなことができれば、もっと簡単になります。
- すべての作成APIがidempotentである
- 作成APIからのすべての失敗を再試行可能にする
2つ目の条件を満たすにはどうすればいいのでしょうか?答えは、作成APIの呼び出しに無効なデータがない場合です。つまり、作成APIのクライアントでリクエストデータを完全に検証できれば、無効なデータを含む作成APIの呼び出しを避けることができます。
しかし、これは一種のトートロジーであることはご存じでしょう。どうすれば、作成APIを呼ばずにデータを完全に検証できるのでしょうか?作成APIの中に同じロジックを持っていても、ほぼ同じでしょうか?ロジックが常に変化しているような現実的な状況で、それは可能なのでしょうか?
この問題は、作成APIがリクエストの内容が正常に処理できるかどうかをテストするトライアルAPIを提供していれば解決できます。ドライランAPIやバリデーションAPIなどの名前があるかもしれません。
非常に興味深いです。次に、分散トランザクション管理を実装するための有名なプロトコルである2フェーズコミットの基本的な考え方にぶつかります。2フェーズコミットの最初のステップでは、無効なデータによる失敗が起きないように参加者が確認します。そして、参加者全員が正常にレコードを作成できることを確認できれば、コーディネーターが参加者全員にレコードを作成するように指示します。
このような流れで仮想通貨管理が行われます。成功事例の場合
- トランザクション」を開始します。マイクロサービスでは何もしていません。
- 帳票サービスのトライアルAPIを呼び出す。OK、成功しました。
- 顧客残高サービスのtrail APIを呼び出す。成功しました。
- 帳票サービスの作成APIを呼び出す。少なくとも複数回のリトライで成功するはずです。
- 顧客残高サービスのAPI作成を呼び出す。少なくとも複数回のリトライで成功するはずです。
- ロールバックの必要はない。
失敗はトライアルステップでキャッチされるべきである。
- トランザクションを開始する。マイクロサービスでは何もしていない。
- 帳票サービスのトライアルAPIを呼び出す。O.K.成功しました。
- 顧客残高サービスの試行APIを呼び出す。悲しいかな、失敗。
- まだ何も作成していないので、ロールバックのステップは必要ありません。