【ITニュース解説】The Dual-Write Problem: Building a Scalable Transactional Outbox for Google Pub/Sub
2026年10月12日に「Dev.to」が公開したITニュース「The Dual-Write Problem: Building a Scalable Transactional Outbox for Google Pub/Sub」について初心者にもわかりやすく解説しています。
ITニュース概要
データベース更新とイベント通知を同時に行うと、片方失敗でデータ不整合が生じる「デュアルライト問題」がある。これを解決するため、データ更新をまず内部の「アウトボックス」に記録し、そこから確実にイベントを発行する方式を採用。これにより、メッセージ消失を防ぎ、システムの堅牢性を高める。
ITニュース解説
システム開発において、複数の異なるシステム間でデータを連携させる場面は頻繁に発生する。特に、あるシステムでデータを更新した後、その変更を他のシステムに通知するイベントを発行する場合、両方の操作が確実に成功することが極めて重要だ。しかし、この「データの書き込み」と「イベントの発行」が、独立した別々の処理として行われるとき、片方が成功し、もう片方が失敗するという問題が起こり得る。これが「デュアルライト問題」と呼ばれる現象である。
例えば、会社の請求システムを考えてみよう。このシステムでは、顧客への請求データがデータベースに記録されると同時に、その情報を元に請求書発行やメール送信などの処理をトリガーするためのイベントが発行される。もし請求データはデータベースに正常に記録されたのに、イベントの発行がネットワーク障害などで失敗してしまったらどうなるだろうか。顧客には請求書が届かず、会社は大きな売上を失うことになる。これは、単なるシステムの不具合で済まされない、金銭的な損失に直結する深刻な問題なのだ。
この問題を解決するために、一般的に用いられるGoogle Pub/Subのようなメッセージングシステムを、BigQueryに蓄積された大量の分析データと連携させるケースを例に考えてみよう。BigQueryはデータウェアハウスとして、膨大なデータを効率的に処理するのに優れているが、リアルタイム性が求められる downstream のサービス(請求書の生成、メール送信、会計台帳の更新など)には、Pub/Subを通じて即座にデータ更新を伝える必要がある。
もし素朴な方法として、BigQueryからデータを定期的に取得し、取得した行ごとにPub/Subへ直接メッセージを発行するプログラムを作成したとする。例えば、Cloud Run Jobや cron ジョブのような仕組みで、BigQueryをクエリし、結果を一つずつ Pub/Sub API に渡すような処理だ。この方法の何が問題かというと、Pub/Sub API にメッセージを送信している最中に、ネットワークの瞬断や Pub/Sub サービスの一時的な不調などが発生した場合、メッセージが失われてしまう可能性がある点だ。データはBigQueryから読み込まれ処理されたが、肝心のイベントがPublishされずに消えてしまう。これがまさにデュアルライト問題であり、請求システムであれば「5万ドルの請求が漏れた」という事態になりかねない。
この問題を解決し、安心してシステムを運用するために、「トランザクショナルアウトボックス」というパターンが考案された。これは、データの抽出とメッセージの送信という二つの処理を分離し、Postgresのような堅牢なリレーショナルデータベースを「アウトボックス」(メッセージの一時保管場所)として利用する方法だ。
具体的には、データの発生源(ここではBigQuery)からデータを取り出す「プロデューサー」と、アウトボックスからメッセージを取り出してPub/Subへ確実に送信する「ディスパッチャー」の二つのコンポーネントに役割を分割する。プロデューサーは、BigQueryから取得したデータを、直接Pub/Subに送るのではなく、まずCloud SQLのようなデータベース内の pubsub_outbox というテーブルに書き込む。この書き込みは、データベースのトランザクション機能(複数の操作を一つの単位として扱い、全て成功するか全て失敗するかのどちらかになることを保証する仕組み)を利用して行われるため、データの書き込みが途中で失敗することはない。
次に、ディスパッチャーはGKE(Google Kubernetes Engine)上で動作するサービスとして、pubsub_outbox テーブルを監視し、そこに格納された未送信のメッセージを順次読み出し、Pub/Subへと送信する役割を担う。ディスパッチャーは、メッセージがPub/Subに正常に送信できたことを確認してから、そのメッセージをアウトボックステーブルから削除するか、「完了」としてマークする。もし送信中にエラーが発生しても、アウトボックスにメッセージが残っているため、ディスパッチャーは後で再試行できる。このように、プロデューサーとディスパッチャーがそれぞれ堅牢な処理を行うことで、全体としてメッセージの信頼性の高い配送を実現するのだ。
ここで疑問に思うかもしれないのが、「なぜディスパッチャーが直接BigQueryをポーリングして新しいデータを探さないのか?」という点だ。これにはいくつかの理由がある。一つはコストだ。BigQueryは分析に特化したサービスであり、クエリごとに処理したデータ量に応じて料金が発生する。新しいデータがないか頻繁に(例えば10秒ごとに)ポーリングを行うと、使ってもいないデータに対して高額な費用が発生してしまう可能性がある。これは FinOps(財務と運用を組み合わせた、クラウドコスト管理のアプローチ)の観点から避けるべきだ。
次に、レイテンシ(処理にかかる時間)の問題がある。BigQueryでのDML(データ操作言語)によるデータの更新は秒単位の時間がかかることがあるが、Postgresのようなリレーショナルデータベースへの書き込みはミリ秒単位で完了する。リアルタイム性が求められるシステムでは、この差は大きい。
さらに、並行性の問題もある。複数のディスパッチャーインスタンスが同時に動いて新しいデータを処理しようとした場合、BigQueryには SELECT ... FOR UPDATE SKIP LOCKED のような、複数のプロセスが同じ行を取り合わないように排他制御を行う効率的な仕組みがない。そのため、複数のインスタンスが同じデータを処理しようと競合したり、同じデータを重複して処理したりするリスクがある。
この並行性の問題に対して、Postgresの SELECT ... FOR UPDATE SKIP LOCKED という機能が非常に有効だ。ディスパッチャーは、このSQLクエリを使うことで、「まだ処理が完了していないメッセージの中から、他のディスパッチャーが現在処理中でないメッセージを100件取得し、自分が処理中であることを示すロックをかける」という操作を安全かつ効率的に行うことができる。これにより、複数のディスパッチャーインスタンスが同時に動いても、それぞれが異なるメッセージを排他的に処理できるようになり、システム全体として水平方向のスケーラビリティ(処理能力を向上させるために、サーバーやコンポーネントの台数を増やすこと)を容易に実現できるのだ。
このアウトボックスパターンは、「アット・リースト・ワンス・デリバリー(最低一回配送)」と呼ばれる配送保証を提供する。これは、メッセージが Pub/Sub に「最低でも一度は配送される」ことを意味するが、システム障害によっては「複数回配送される可能性がある」ことも示唆している。例えば、ディスパッチャーがメッセージを Pub/Sub に正常に送信できたものの、その直後にクラッシュしてしまい、アウトボックスのメッセージを「完了」とマークする前に処理が中断された場合、同じメッセージが後で再度送信される可能性がある。しかし、請求システムのようなミッションクリティカルなシステムでは、メッセージが「失われる」ことによる損失の方が、「重複して届く」ことによる処理のオーバーヘッドよりもはるかに深刻だ。重複メッセージは、受け取り側で適切に処理(例えば、メッセージに一意のIDを付与し、同じIDのメッセージが複数回届いても一度だけ処理するような工夫)することでリカバリー可能だからである。
このアウトボックスパターンのもう一つの大きな利点は、その汎用性にある。pubsub_outbox テーブルに保存されるメッセージのペイロード(データ本体)をバイト配列(BYTEA)として扱うことで、ディスパッチャーはメッセージの内容を一切気にせず、ただ確実にPub/Subに届けるだけの「汎用的な配送エンジン」として機能する。つまり、どのような種類のデータやイベントであっても、このアウトボックスを通せば、確実な配送が保証されるのだ。
これにより、開発チームはデータ連携の信頼性を確保するための複雑なロジックを毎回一から実装する必要がなくなる。新しいデータストリームを追加する場合も、ディスパッチャーのコードを変更することなく、同じ Docker イメージを GKE にデプロイし、環境変数で目的の Pub/Sub トピック名を指定するだけでよい。結果として、システムは「最低一回配送」の保証に加え、エラー時の「指数的バックオフ」(再試行間隔を徐々に長くする方式)や「デッドレター処理」(最終的に配送できなかったメッセージを別の場所に退避させる仕組み)といった信頼性向上のための機能も自動的に享受できる。
このアウトボックスパターンは、データの一貫性と信頼性が求められる分散システムにおいて、非常に強力な解決策となる。ただし、メッセージの受け取り側(コンシューマー)が重複メッセージの処理を適切に行う必要がある。このコンシューマー側での重複対応については、本解説の範囲外となるが、システム設計において重要な考慮事項である。