加盟店リソース作成時の整合性担保の検討と実装

こんにちは。メルペイでバックエンドエンジニアとして従事している a-r-g-v です。私は加盟店さまの初期審査やサポートを行うための社内向け管理画面(以下、社内Tool)や加盟店さま向けの管理画面を開発しているチームに所属しています。この記事はメルペイ加盟店リソースを作成するシステムとそこにある課題、それらに対する改善の取り組みついて紹介させていただきます。

また、本記事は、Merpay Tech Openness Month 2021 の16日目の記事です。

背景

メルペイではマイクロサービスアーキテクチャを採用しています。我々のプロダクトでは、精算サービス、銀行サービス、決済サービスなどの複数マイクロサービスを組み合わせて加盟店さまの体験を作り上げており、メルペイ内でもトップクラスに他のマイクロサービスと接続しています。

メルペイを導入するためにはまず加盟店申込みフォームから申込みをしていただきます。申込みがあった後、オペレーターは社内Toolから申込み情報を確認し初期審査を実施します。オペレーター審査を通過した後に、メルペイ加盟店を作成しメルペイで決済できる状態を作ります。決済可能な加盟店を作成するためには、複数のマイクロサービスにリソース作成をリクエストする必要があります。代表的なリソースを抜粋すると、以下のようになります。

  • 加盟店台帳マイクロサービスに加盟店情報を登録
  • 銀行サービスに振込先口座を登録
  • 残高サービスに売上金口座を登録
  • 精算サービスに精算サイクル情報を登録
  • コード決済サービスに外部事業者採番の加盟店IDを登録

加盟店さまの申込情報によって作成の必要があるリソースは異なります。最も多いケースでは、約10個のマイクロサービスに対して合計20個弱のリソースを作成する必要があります。そのため、 『複数マイクロサービスに跨ったリソース作成の整合性をどのように担保するか』を検討する必要があります。

加盟店リソース作成時の依存マイクロサービス
図: 加盟店リソース作成時の依存マイクロサービス

課題

今までは、これらのマイクロサービスへのリソース作成のリクエストはオペレーターの加盟店初期審査を経て、審査が可決した場合に登録のリクエストと同期的に行っていました。そのため、これらの複数マイクロサービスへのリクエストのうち途中で失敗すると、加盟店リソースの作成が中途半端な状態で止まってしまうケースが考えられます。このようなケースではオペレーターが審査結果登録のリクエストをリトライ送信することで対応していました。

オペレーターリトライによるエラーの回復の例
図: オペレーターリトライによるエラーの回復の例

しかしながら、オペレーターの再度のリクエストリトライが行われない場合もあります。そのような場合では、作成に失敗したリソースが再作成されないため、中途半端にリソースが残った状態となります。このような状態を放置すると意図しない不整合が発生し、加盟店さまのUXの毀損や、エンジニアの調査対応につながっていました。また、オペレーターの工数が余分に掛かってしまうことも問題でした。そのため、結果整合性を保証する必要があります。

解決策の検討

この問題を解決するためにはオペレーターのリクエスト同期でリソース作成を行うのをやめる必要があります。ここでは、以下の2つの観点があると考えました。

  • リクエスト非同期での加盟店リソース作成タスク実行をどう実現するか?
  • 加盟店リソース作成処理の整合性の担保をどのように実現するか?

それぞれの観点毎に評価を行いました。

リクエスト非同期でのタスク実行の実現

オペレーターのリクエスト同期に代わって、加盟店リソース作成処理を起動する契機を検討する必要があります。ここでは、以下の 2つの方法があると考えました。

  • Cloud Tasks を使用する
  • データベーステーブルキューと定期実行バッチを使用する

Cloud Tasks

タスクキューの実装先としてCloud Tasksを使用する選択肢があります。Cloud Tasks とは Google Cloud Platform が提供するプロダクトの1つで、HTTP リクエストのタスクキューです。リクエスト先として 加盟店リソース作成用のAPIを指定することで、オペレーターの審査結果登録リクエストとは非同期でリソース作成処理を実現できます。

しかしながら、社内システムの制約により HTTP Target タスクのHTTPタイムアウト秒数が短いこと(デプロイ時間短縮の観点)や、内部データベースと整合性を担保した形で Tasks 作成を行うには Transactional Outbox パターンが必要であることから、採用を見送りました。

★テーブルキューと定期実行バッチ

データベースのテーブルをキューとして照会し、未処理データがあればタスクを実行する定期実行バッチを作成するという選択肢も考えられます。キューテーブルへの挿入がデータベーストランザクションのACID特性で保護されるため、内部データベースとの整合性を担保するのが容易です。オペレーターの審査結果登録リクエスト同期で発行するトランザクション中でキューにデータを追加することで、リクエスト非同期でのリソース作成処理を実現することができます。

デメリットとしては、他の方法に比べてスループットが限定されることがあります。しかしながら、今回のユースケースではスループットへの要求が高くないため、こちらの方法を採用することにしました。

リソース作成処理の整合性の担保

リソース作成処理の整合性をどのように担保するかという方法を検討する必要もあります。ここでは、以下の 2つの方法があると考えました。

  • Saga パターン, TCC パターンの採用
  • リトライ可能エラーの自動リトライ

Saga パターン, TCC パターンの採用

リソース作成処理の整合性を担保する有名な方法として、Saga パターンや TCC パターンが存在します。

Sagaパターン

非同期メッセージングでコーディネートされた一連のローカルトランザクションを使って複数のサービスにまたがるデータ整合性を維持する手法です。より詳細な手法は Microservice Architecture Pattern: Saga が詳しいです。

TCC(Try-Confirm-Cancel) パターン

依存先マイクロサービスがリソースの作成を「Try: 仮確保, Confirm: 確定, Cancel: 確保取消し」の3APIに分けて提供し、API呼び出し元が これらの 3API を使って結果整合性を保証する手法です。具体的には、全依存リソースの Try リクエストが成功した後に Confirm リクエストを実行し、1つでも失敗した段階で 全リソースに Cancel リクエストを送信するという方法です。より詳細な手法は、foghost さんが書かれたマイクロサービスにおける決済トランザクション管理が詳しいです。

これら手法の導入にはコール先のAPIの改修が必要となります。今回のユースケースでは、加盟店リソースを作成するために多くのマイクロサービスやAPIをコールする必要があります。そのため、依存先APIの改修が必要な方法では改修範囲が大きくなってしまいます。よって、コール先のマイクロサービスのAPI 実装を修正する方法は現実的ではないと判断しました。また、APIをコールする側で整合性管理する方式が望ましいということもわかりました。

また、今回のユースケースではリソースをリアルタイムでロールバックする必要がありませんでした。なぜなら、リソース作成処理は冪等性が担保されているため安全にリトライできるからです。リトライ毎に同じ冪等キーを利用しているため、既にリソース作成されている処理はスキップすることができます。更に、今回の作成処理においては、意識するべきリソースの状態数が少なく、かつリソース間の依存関係が単純という特徴がありました。そのため、リソース状態を巻き戻さなくてもエラーが発生せずリトライ可能でした。

今回、シーケンス中に意識するべき依存リソースの状態は「作成済み」か「未作成」の2状態のみであり、状態遷移は「未作成」を「作成済み」に更新するパターンしか存在しません。既に作成済み状態に遷移している場合は、リソース作成処理をスキップすれば良いですし、未作成であればリソースを作成するだけで十分であるからです。 リソース間の依存関係が単純であるため、今回のシーケンス中に発生する可能性のあるエラーは以下の2つです。

  • ネットワークエラー等再度リトライを行うだけで解決できるエラー。
  • 入力情報の誤り等リトライをし続けても解決することができないエラー。

後者のエラーについては、ロールバック後にリトライを行ったとしてもエラーとなります。これらの理由により、ロールバックは不要と判断しました。

★リトライ可能エラーの自動リトライ

加盟店リソース作成処理の途中でリトライ可能エラーが発生した場合、再度処理を先頭からやり直す手法です。この方法を採用するためには加盟店リソース作成処理が冪等になっている必要がありますが、メルペイでは API を冪等に作るポリシーがあるため、依存マイクロサービスのAPIのほとんどが冪等になっており、加盟店リソース作成処理全体が冪等になっていました。

デメリットとしては、途中でリトライ不可能エラーが発生した場合にリソース作成処理が不完全な状態で放置されてしまうことがあります。そのため、リトライ不可能エラーが発生した場合の対処方法を検討する必要があります。しかしながら、今回のユースケースでリトライ不可能エラーが発生するほとんどのパターンは、審査情報の入力誤りによるリソース作成エラーです。このケースでは、新規に加盟店決済を行えないように設定することで対応できるため、こちらの方法を採用しました。

解決策

ここでは、定期実行バッチとリトライ可能エラーの自動リトライの手法を採用しました。

具体的には、オペレーターの審査結果登録契機でテーブルキューにデータを挿入することにしました。また、定期実行されるバッチを作成しました。このバッチは定期的にテーブルキューを参照し、未処理のデータがあれば加盟店リソース作成処理を実行するようにします。処理中でリトライ可能なエラーについては次回バッチ実行時にリトライを行い、リトライ不可能エラーについては処理ステータスをエラーに変更した後、Sentry/Slack 通知を行うようにしました。

図: 定期実行バッチを使った結果整合性の保証

まとめと展望

今回は複数マイクロサービスのリソース作成の整合性担保を定期実行バッチとリトライ可能エラーの自動リトライという方法を採用することで、シンプルに解決することができました。しかしながら、社内Toolでは上記以外でも 複数のマイクロサービスにリクエストを行っているユースケースが多数あるため、整合性を管理する汎用的な仕組みが必要です。現時点では、一部のユースケースをバッチ等によって担保しているため、類似実装が重複してしまうことや、バッチ間の関連がわかりにくいという課題があります。これを解決する方法として、再利用可能な整合性管理システムの導入等があると考えています。また、今回は検討できませんでしたが、Cloud Workflows 等の採用なども選択肢として考えていきたいと思っております。

メルペイではバックエンドエンジニアを募集しています。私達のチームでも仲間を募集していますので、興味のある方はぜひご応募ください! https://mercari.wd3.myworkdayjobs.com/ja-JP/mercari_external/job/Roppongi/Software-Engineer–Backend–merpay-_JR-000000015

参考文献

Chris Richardson, 長尾高弘, 橂澤広亨, マイクロサービスパターン 実践的システムデザインのためのコード解説, 株式会社インプレス, 2020