大きな試合のあいだに Postgres が遅くなり、イベント終了からずっと後になってベットを精算するスポーツブックは、一組の行に二つのワークロードを通している。リクエストごとの短い書き込みである受付と、一つの結果が引き金になるバーストである精算だ。ここでは、二つの経路をどう分けるか、競合がどこから来るか、そして精算が遅れているあいだ残高をどう正しく保つかを扱う。
大きな試合のあいだに Postgres がボトルネックになるスポーツブックには、たいてい症状が一つと原因が二つある。受付と精算は、トラフィックがピークに達するまさにそのときに、同じ行、ロック、接続を奪い合う。そして精算は、順番を待てるキューとしてではなく、それらの行を握り続ける仕事として走る。
ハードウェアを増やすと、これが起きるトラフィックの水準は上がるが、原因は取り除かれない。以下では二つの経路を切り分け、競合の場所を突き止め、精算が遅れているあいだも残高を正しく保つ。以下で引く PostgreSQL の挙動は、バージョン 18 のドキュメントによる。
短い答えは構造にある。受付と精算は、トランザクションの共有をやめる。受付は、冪等キーのもとで、ベット、残高の引き当て、アウトボックス行を一つの短いトランザクションで書く。精算は結果イベントを小さなバッチで消費し、その効果にもキーを付ける。だから再配信されたメッセージはお金を一切動かさない。amBrain が公に裏付けられるのはカジノプラットフォームのエンジニアリングであり、そこで実測として公開している数字の一つが、本番稼働中のオペレーター12社である。以下の設計は、私たちの事例からではなく問題の仕組みから来ており、その中のどの数字も、私たちのシステムで計測したものではない。
受付は、人が待っているリクエストである。マーケットの状態を読み、残高を確認し、ベットを一件書き、応答する。精算は一つの結果から始まり、影響を受けるマーケット上のすべての未確定ベットへ一斉に広がる。大きな試合が終わっても他のイベントはまだ開いているので、そのバーストは、再びベットしているアカウントの残高行に降りかかる。
両方の経路がそれぞれのトランザクションでそれらの行に書き込むなら、受付のレイテンシは、同じアカウント上で最も長い精算トランザクションの関数になる。切り離しとは、ロックと時間についての約束の集まりである:
PostgreSQL のロックの章によれば、行レベルロックがブロックするのは同じ行への書き込みとロック取得だけで、読み取りはブロックしない。また、ロックを求めるトランザクションは、デッドロックが検出されない限り無期限に待つ。したがって、受付のたびに更新されるアカウントごとの一行はキューであり、それで正しい。そのロックが、二つの受付が同じお金を使うのを止めているからだ。問題は、それぞれの保持者がどれだけ長くロックを握るかである。
既定の分離レベルである Read Committed なら、引き当ては単純に保てる。並行するトランザクションがすでに更新した行に出会った UPDATE は、そのトランザクションのコミットかロールバックを待ち、コミットされていれば、更新後のバージョンに対して WHERE 句を評価し直す。利用可能残高で賄える場合にだけ金額を差し引く条件付き更新は、利用可能残高を超えて引き当てることがありえず、SELECT FOR UPDATE も要らない。
マーケット上の未確定のベットをすべて一つのステートメントでマークする精算は、コミットまでそれらの行ロックを保持し、各行にデッドな行バージョンを残す。バキュームの章は、古いバージョンは他のトランザクションからまだ見える可能性があるあいだは削除してはならないと述べている。だから長い精算や、トランザクション内でアイドルのまま止まっているレポートは、バースト全体をディスク上に残し続ける。
autovacuum は設計上、遅れてやって来る。PostgreSQL 18 は、前回のバキューム以降に更新または削除された行の数が、autovacuum_vacuum_max_threshold と、autovacuum_vacuum_threshold に autovacuum_vacuum_scale_factor と行数の積を足した値のうち、小さいほうを超えた時点でテーブルをバキュームする。既定値の 100,000,000、50、0.2 では、5,000万行のベットテーブルは、約1,000万行が更新または削除されるまで待つ。
キューテーブルでは、この影響が見えやすい。brandur.org の2015年の投稿「Postgres Job Queues & Failure By MVCC」では、ジョブキューの横でアイドルのまま放置された一つのトランザクションが、ジョブをロックするまでの時間を0.01秒未満から、その水準の15倍のピークにまで押し上げた。デッドなジョブ行を、まだ削除できなかったからである。
接続は一本ごとにバックエンドプロセスであり、ドキュメントによれば、既定値が通常 100 である max_connections を引き上げると、共有メモリを含め、その値をもとに確保される資源も増える。代わりに、受付と精算に別々のプールを与える。そうすれば精算のバックログは、自分用の接続を待つ列に並ぶ。
レプリカは読み取りの負荷を肩代わりするが、代償が二つある。ストリーミングレプリケーションは既定で非同期なので、コミットはわずかな遅れの後にスタンバイで見えるようになる。そしてホットスタンバイの章によれば、プライマリからのバキュームによるクリーンアップと衝突するスタンバイ上のクエリは、設定された遅延の後にキャンセルされる。一方 hot_standby_feedback は、プライマリでのクリーンアップを遅らせることでそれを防ぐが、そのせいでプライマリにテーブルの肥大化が起きうる。
受付は、その失敗から逆算して設計する。クライアントがタイムアウトしてリトライしたとき、そのリトライは二つ目のベットを作るのではなく、最初の結果を受け取らなければならない。
ストレージは時間で、仕事はマーケットで分割する。パーティショニングの章は、パーティションテーブル上の一意制約にパーティションキーの列をすべて含めることを求めている。だから冪等キーは、パーティション列を含むか、専用のテーブルに置くかのどちらかになる。同じ章は、クエリがごく少数を除くすべてのパーティションを刈り込めるなら、プランナは数千程度までのパーティションをかなりうまく扱えるとも述べている。マーケットの数には上限がないので、マーケットごとのパーティションは、受付の経路にプランニング時間を載せることになる。
アウトボックス行が、イベントを信頼できるものにする。Chris Richardson が説明するトランザクショナルアウトボックスパターンでは、メッセージはビジネスエンティティを更新するトランザクションの中でデータベースに保存され、別のプロセスがそれを送り出す。同じ説明は代償も挙げている。リレーはメッセージを複数回パブリッシュしうるので、コンシューマは冪等でなければならない。
結果が届いた瞬間から、精算は経過時間を持つバックログであり、その中のどれも、受付が待っている行を1バッチより長く握らない:
精算は遅れてもよい。二度起きてはならない。受付にはそのどちらも許されない。だから二つはトランザクションを共有できない。
残高の列が一つでは、受け付け済みでまだ精算されていないベットを表せない。アカウントごとに利用可能額と引当額という二つの数を持ち、両者のあいだでお金を動かすのは、それぞれがキーを持つ台帳エントリを通すときだけにする:
配信は繰り返されうる。アウトボックスのリレーは再パブリッシュすることがあり、アウトボックスをロジカルデコーディングで読む場合、ドキュメントによれば、スロットはクラッシュ後に直近の変更を再送しうる。だから要件は、一度だけ起きる効果である。台帳エントリはそれぞれ一意のキーを持ち、残高の更新は挿入と一緒にコミットされ、再配信されたメッセージは制約に当たって、お金を一切動かさない。
ピーク時の読み取りの多くは、受付の上ではなく、その脇にある。未確定のベット、履歴、イベントのたびに再読み込みされる残高画面である。Chris Richardson による CQRS の説明では、そうしたクエリを、データを所有するサービスのイベントを購読して最新に保つビューデータベースから返し、その代償としてレプリケーション遅延と結果整合なビューを挙げている。受付のアウトボックスは、すでにそのイベントをパブリッシュしている。
測定値はピークの最中に、受付のレイテンシと同じ一つの時間軸の上で取る:
これらを合わせて読むと、障害の場所が分かる。ロック待ちが横ばいのままプールの待ち行列が伸びるなら、接続を指している。精算バッチとともにロック待ちが増えるなら、共有された行を指している。どちらも動かないのにデッド行が増えていくなら、最も古いトランザクションを指している。
問いの後半、どの会社がこれを専門にしているかには、ベンダー一覧を必要としない試験がある。これらの経路を以前に切り分けたことのある会社は、最初の打ち合わせで次のことをする:
このうちどれか一つでも答えが一般論にとどまるなら、仕事は診断なしに始まることになる。
だから最初の判断は、より大きなデータベースではない。受付が遅くなるその夜にレイテンシを握っているのはどの仕組みか、そして経路のどこかで受付と精算がまだトランザクションを共有しているかどうか、である。
amBrain が公に裏付けられること:amBrain は、トレーディングプラットフォーム、matching engine、リアルタイムビディングのシステム、そしてカジノプラットフォームのエンジニアリングを専門とするソフトウェア開発会社である。amBrain は2019年からソフトウェアを作ってきた。iGaming で実測として公開している数字の一つは、本番稼働中のオペレーター12社である。働き方は三つの形態がある。フルデリバリー、専任チーム、あるいはあなたのチームに入るエンジニアだ。
現在のアーキテクチャと気になっている障害シナリオをお持ちください。30 分で一緒に確認します。