二つのリージョンに二台のアプリケーションサーバー、VPN 経由で書き込む一人のクライアント、そして誰も書き留めなかった問い:書き込みが起きたと決めるのはどのコンポーネントか。ルーティングはリクエストをプロセスへ届けます。トランザクション境界は、変更がいつ耐久性を持ち可視になるかを決めます。これは別のシステムが担う別の仕事であり、その隙間こそが多くのマルチリージョン書き込みのバグが住む場所です。

地図ではなくビジネスルールから始める

地理ではなくビジネスルールについての一文から始めてください:どの読み取りと書き込みが一緒に成功しなければならないか、その間にどの同時変更が不可能でなければならないか。在庫予約なら、在庫チェックと予約の両方が有効になるかどちらも有効にならないかであり、その間に別の注文が同じ単位を消費してはなりません。

RequirementWhat satisfies itWhat does not
All-or-nothing applicationA transaction inside one owning systemA VPN path between regions
No double spendIsolation strong enough to exclude the conflictAtomicity alone
Survive a region lossReplication the database itself documentsTwo independent databases behind one client

チームが飛ばすのはまさにこの真ん中の行です。十分な分離のない原子性は、二つのトランザクションがそれぞれ正しくコミットしながら、ビジネスが不可能と考える状態を生み出すことを許し、後からいくら再試行ロジックを積んでも直りません。

権威ある境界一つは地域サーバー二台に勝る

地域のアプリケーションサーバーはそのままで構いませんが、操作を単一のサポートされたトランザクション境界の内側で実行するサービスへ渡す必要があります。アプリケーション層は意図のルーターになり、データ層が真実を所有します。

その境界は短く、触れるデータの近くに保ってください。人間がプロンプトに答える間、あるいは呼び出しが二つのリージョンをまたぐ間、開いたままのトランザクションはロックを保持する時間を費やし、後ろで何かがタイムアウトする可能性を高めます。その障害はデータベースの問題に見えますが、実際にはそう装った設計の問題です。判断をデータ層へ移すと、たいてい完全に消えます。

BEGIN;
UPDATE inventory SET reserved = reserved + 1
 WHERE sku = 'A-100' AND available >= 1;
INSERT INTO reservation (sku, order_id) VALUES ('A-100', 'ord-9f');
COMMIT;

そのスケッチは性能の推奨ではなく境界として読んでください。要点は、チェックと挿入が一つのコミットを共有するため、同時注文は在庫が消費されたのを見るか、まったく見ないかのどちらかになるということです。

レプリケートされたデータベースが与えるものと与えないもの

レプリケートされたデータベースは、それ自身のトランザクションモデルが対応する場合、より広い境界を提供できます。Spanner は単一データベース内のテーブルをまたぐアトミックな読み書きトランザクションを、定義された分離と再試行の挙動とともに文書化しています。これは読み、試し、意図的に頼る保証です。

それは製品から来る保証であり、経路から来るものではありません。リージョン間の VPN はパケットの旅し方と障害の現れ方を変えます。トンネルがどれほど健全に見えても、トランザクション境界を一行たりとも広げません。この二つを混同すると、すべてのコンポーネントが自らのチェックを通るのにデータは一致しないアーキテクチャが生まれます。

二つのデータベースは一つのシステムにはならない

独立して管理される二つのデータベースは、クライアントが両方に到達できるからといって共有のトランザクションマネージャーを得るわけではありません。ビジネスが本当にその組をまたぐアトミックなコミットメントを必要とするなら、調整の仕組み、参加者、復旧手順はすべて前提にせず選び、適格性を確認する必要があります。

作業が準備された後に参加者やリンクが消える瞬間を考えてください。サポートされた分散コミットは、復旧手順と人間の判断を要する不確定状態を残すことがあり、どの分断を通じてもすべてのリージョンが書き込み可能であり続けるという約束はありません。

代わりに非同期の調整が許容できるなら、そう明言し、中間のビジネス状態と整合のルールを書き留めてください。補償を伴うローカルにコミットされた手順の連鎖は正当な設計であり、別名のアトミックなトランザクションではないだけです。

最初の障害の前に再試行を設計する

クライアントは操作がコミットされた後に応答を失うことがあるため、呼び出し側はタイムアウトを失敗として扱えません。耐久性のあるリクエスト識別子を保ち、変更を再試行する前にサービスの結果か冪等性の仕組みを使わないと、再試行は二度目の書き込みになります。

自動トランザクションコールバックは、再試行可能な中断の後に複数回実行されることがあり、不可逆な外部効果の置き場所としては不適切です。ワークフローが明示的な安全プロトコルを定めていない限り、それらはコールバックの外に置いてください。お金がかかるものやメッセージを送るものにも同じ注意が当てはまります。二重請求は二重のログ行よりずっと難しい会話です。

受け入れでは四つのシナリオを再現すべきです:同じ行を争う二人のクライアント、サービスが再試行可能と言う中断、応答が決して届かないコミット、そしてリージョンまたはその間のリンクの喪失です。コミットされたビジネス結果と未解決の状態をルーティング設計の隣に記録し、ネットワークを保証を提供するコンポーネントと取り違えないようにしてください。

関連記事