1 回限りの処理と最大 1 回の確認応答

Spanner などの分散クラウド データベースにわたって信頼性の高いメッセージ キューとマイクロサービスを構築するには、独自の課題があります。このドキュメントでは、Spanner キューで 1 回限りの処理と最大 1 回の確認応答を実行するために使用されるコアコンセプトと設計パターンについて説明します。

基本コンセプトとべき等性のジレンマ

「べき等性」とは、オペレーションが実行される回数に関係なく、結果が常に同じであることを意味します。メッセージ キューまたは作業配信システムを構築する際に、べき等性を実現する主な配信方法は次の 3 つです。

  • At-least-once: メッセージの配信と処理が保証されます。ネットワーク障害が発生すると、メッセージが複数回配信され、処理されることがあります。
  • At-most-once: メッセージは最大で 1 回配信されます。重複処理は防止されますが、障害が発生するとメッセージが失われたり、ドロップされたりする可能性があります。
  • 1 回限り: すべてのメッセージが 1 回だけ処理されます。失われたり重複したりすることはありません。

Spanner キューは、at-least-once 配信at-most-once 確認応答を自動的に提供します。ただし、以降のセクションで説明する設計パターンと例を使用して、重複処理と再配信をプログラムで削減できます。

不明なコミット ステータスの問題

単一マシンのデータベースでは、トランザクションは成功するか失敗するかのいずれかです。分散クラウド データベースでは、データが複数の物理データセンターに複製されるため、トランザクションが失われる可能性があります。

アプリケーションがメッセージを処理し、DELETE FROM MessageQueue WHERE message_id = '123' ASSERT_ROWS_MODIFIED 1 などの確認応答を commit すると、リクエストは複数のネットワーク ホップを通過します。

[Your App] <--(gRPC)--> [Spanner API] <--(Google network)--> [Spanner Storage]

Spanner ストレージ リーダーが commit を受信すると、ストレージ レプリカ間で同期されます。コンセンサスに達してデータがディスクに書き込まれると、トランザクションはデータベースで正常に commit されます。

ただし、データがディスクに commit された後、成功確認がアプリケーションに届く前に、一時的なネットワーク障害(プロキシの再起動やネットワークの切断など)が発生する可能性があります。この場合、アプリはネットワーク タイムアウト エラー(DEADLINE_EXCEEDED または UNAVAILABLE)を受け取ります。

接続がアウトオブバンドで切断されたため、アプリケーションは commit が成功する前に接続が切断されたのか、成功した後に切断されたのかを認識できません。これは「不明なコミット ステータス」の問題と呼ばれます。

自動再試行が危険な理由

アプリケーションがネットワーク タイムアウト エラーをインターセプトし、重複除去なしでまったく同じトランザクションを再試行すると、ビジネス ロジックが 2 回実行される可能性があります。たとえば、取引でクレジット カードへの請求や商品の発送が行われた場合、成功した(が未確認の)取引を再試行すると、請求や発送が重複してしまいます。

ユーザーを保護するため、公式の Google Cloud クライアント ライブラリは、不明な commit エラーで失敗したトランザクションを自動的に再試行することはありません。トランザクションの結果が不明であることを示す明示的なエラーがスローされ、アプリケーション コードでべき等パターンを使用して再試行を安全に処理する必要があります。

1 回限りの処理の現実的な戦略

トランザクションを安全に再試行し、重複する処理とメッセージの再配信を減らすには、次のいずれかの主要な設計パターンを実装します。

変更された行をアサートする

すべてのビジネス ロジック(他のテーブルの更新など)とキュー メッセージの確認応答が単一の Spanner トランザクションで発生する場合は、DELETE ステートメントで ASSERT_ROWS_MODIFIED を使用できます。これは、重複排除テーブルを別途必要としないため、1 回限りの処理を行うための最も簡単な戦略です。

ステートメントに ASSERT_ROWS_MODIFIED 1 を追加すると、削除されたメッセージが 1 つだけの場合にのみ、確認応答が成功します。ネットワーク障害が発生し、アプリケーションがトランザクションを再試行すると、メッセージはキューに存在しなくなります。再試行では 0 行が削除され、ステートメントは OUT_OF_RANGE で失敗します。

OUT_OF_RANGE エラーは永続的です。行はすでに削除されているため、ステートメントを再実行しても常に 0 行が変更され、再び失敗します。また、ステートメント レベルでもあります。DELETE のみが失敗します。トランザクションは開いたままになり、すでに書き込んだ内容はバッファリングされたままになります。続行すると、これらの内容は commit されます。Spanner は、トランザクションを中止またはロールバックしません。意図したとおりの exactly-once 保護を実現するには、トランザクションがロールバックされるようにする必要があります。

  • エラーを伝播させる: ランナーベースの API(Go の ReadWriteTransaction など)を使用する場合は、OUT_OF_RANGE エラーをトランザクション関数から伝播させます。クライアント ライブラリはトランザクションを破棄してロールバックし、ビジネス ロジックの書き込みがコミットされないようにします。
  • キャッチして続行しない: トランザクション関数内でアサーション エラーをキャッチして続行しないでください。この場合、Spanner は他のステートメントを commit するため、このパターンで防止しようとしている重複更新が発生します。
  • 手動ロールバック: トランザクションを手動で管理する場合(Go の ReadWriteStmtBasedTransaction や REST/gRPC API など)、アサーションが失敗したときに、対応するロールバック メソッドを明示的に呼び出す必要があります。

トランザクション アウトボックス

ビジネス ロジックが単一の Spanner トランザクション内で発生できない場合(複数ステップのワークフローや複数のシステムにまたがる更新など)、またはメッセージ処理で外部サービスの呼び出しが必要な場合(SMS の送信や支払いの処理など)、ビジネス ロジックとキューの確認応答をアトミックに結合することはできません。

このようなシナリオでは、Spanner トランザクション ブロック内で外部 API を呼び出したり、非トランザクション オペレーションを直接実行したりしないでください。Spanner がトランザクションを再試行または中止すると、アプリケーションがそのビジネス ロジックを複数回実行する可能性があります。

アプリケーションで独自のべき等戦略を実装する必要がありますが、次のパターンを使用して重複作業を減らすことができます。

  • 高速オペレーション(デフォルトの 10 秒のリース内で完了): メッセージを受信したときに、ビジネス ロジックまたは外部 API 呼び出しを実行します。オペレーションが成功した場合にのみ、メッセージの確認応答を行います。オペレーションが失敗した場合、または確認応答前にワーカーがクラッシュした場合は、メッセージの確認応答を行いません。リースが期限切れになり、Spanner が再試行のためにメッセージを自動的に配信します。

  • 長時間実行オペレーション(10 秒以上かかる場合): メッセージを受信したら確認応答を送信し、同じトランザクション内で、将来の配信用にスケジュールされた新しいメッセージを再送信します(配信遅延は推定実行時間よりも長くします)。または、RENEWLEASE_QUEUE_NAME() TVF を使用してメッセージ リースを定期的に延長します。ビジネス ロジックまたは外部呼び出しが成功したら、新しくキューに登録されたメッセージまたは延長されたメッセージを確認します。

多重化されたセッションのべき等性

Spanner セッションは多重化できます。多重化されたセッションは、共有セッション間でメモリ内のトランザクション状態を追跡します。これにより、クライアント ライブラリが再接続して不明なコミット ステータスをより確実に解決できるようになり、クライアントサイドのネットワーク障害から保護されます。

ただし、フロントエンド サーバーがクラッシュした場合、多重化セッションでは不明な commit 結果を排除できません。セッションのトランザクション テーブルをホストする特定のサーバーが commit 直後に再起動すると、インメモリ状態が失われ、再接続時に不明なトランザクション結果エラーが返されます。このリスクに対処するには、このページのべき等パターンを実装します。

次のステップ