Tabelog Tech Blog

食べログの開発者による技術ブログです

食べログネット予約における非同期メッセージ発行の設計パターン - Transactional Outbox のメリット

この記事は 食べログアドベントカレンダー2023 の11日目の記事です🎅🎄


こんにちは。食べログシステム本部 技術部 マイクロサービス化チームの 栗山 です。マイクロサービス化チームは「巨大なモノリシックサービスにおける開発の辛さを解消し、少人数のチームが自律的に意思決定しながら開発するためのシステム基盤を作る」をミッションに活動しています。本記事は以前ご紹介した汎用性の高いマイクロサービス基盤技術 Change Data Capture の活用事例になります。

目次

食べログネット予約のがっかり体験 - 予約を取ろうとしたら既に席が埋まっていた

食べログネット予約の導線はいくつかあり、一番わかり易いのは店舗詳細にあるカレンダーです。 飲食店側で設定した座席やコースのデータから予約ができる日時の残席データを作成し、カレンダー上で予約できる日を表示しています。カレンダーで○や、△、残1のように表示されている日はネット予約の残席があることを示しており予約できる状態です。

予約カレンダー残席表示

食べログネット予約にはこの予約可否を表すカレンダー更新のリアルタイム性が低いと言う課題がありました。カレンダーの表示は予約できそうなのに、操作してみると予約できないことがあり、ユーザーのがっかり体験に繋がっていました。

予約できそうでできない

例えば4人がけテーブルが2つあり、テーブルを移動して8人がけテーブル1つにできるお店のケースを考えてみましょう。このお店は1~4人の予約を2組までとれますが、5~8人の予約を1組とることもできます。このシンプルな例でも2パターンあることからわかるように、座席や予約などのデータから残席を導出する計算はかなり複雑で重たい処理です。なので食べログでは座席の変更やネット予約の成立など大本になるデータの更新後に、非同期処理で残席のデータを更新しています。

座席と予約の関係が複雑な説明

以前のカレンダー更新処理は、数分間隔で実行される cron バッチで DB を SELECT して予約可否が変動する操作を検知し、カレンダー更新処理のジョブワーカーをキックするメッセージを発行するアーキテクチャでした。これではカレンダーへの反映が数分遅れてしまいます。

cronバッチアーキテクチャ

この課題に対して、残席カレンダーの反映処理をエンドツーエンドで完全にイベント駆動化することで対処しました。イベントのトリガーとなるメッセージ発行は Transactional Outbox パターンで設計・実装しました。

イベント駆動アーキテクチャ

本記事ではこのパターンの選択理由とメリットを詳しく説明してゆきます。

メッセージ発行処理の設計パターン選定

食べログではメッセージング基盤に Apache Kafka を利用した非同期処理を採用しています。残席更新処理を完全イベント駆動化するにあたり焦点となったのはメッセージ発行処理の設計で、大きく以下の3パターンが考えられました。

  • 二重書き込み
  • 予約業務テーブルの Change Data Capture
  • Outbox テーブルの Change Data Capture (Transactional Outboxパターン)

それぞれにメリット・デメリットがあり、アプリケーション実装を担当する予約サービスチームと一緒にどの設計パターンを採用するべきか検討しました。

二重書き込み(Dual Writes)

まず古くからあるDB更新(業務データ更新)とメッセージ発行を別々に行うパターンで、これは書籍 データ指向アプリケーションデザイン では二重書き込み(Dual wirtes)と記述されています。

二重書き込み-成功

このパターンのメリットはメッセージ発行を自由に制御できることです。何かの事情に合わせてDBだけ更新や、メッセージ発行だけを行うことができます。

しかしDB更新とメッセージ発行の一貫性を保証するのが困難です。DB更新後にメッセージ発行するシーケンスで実装する場合に、その後のメッセージ発行が失敗する可能性があります。その後のメッセージ発行リトライと、リトライしても成功しなかったときの例外処理をアプリケーションで実現すると大変複雑な設計になります。分散トランザクション技術で解決するという選択肢もありますが、DB上では正しく成立する予約をロールバックするというのは業務に合いません。

二重書き込み-失敗

トランザクション中でメッセージ発行すればDB更新が失敗したときに無駄なメッセージ発行が増えるだけなので、更新処理を冪等にすればよいのでは?と言う考えもありますが、トランザクション完了前に非同期処理が始まり古いデータを参照してしまう問題が発生します。(MySQL InnoDB デフォルトの REPEATABLE READ で運用していればトランザクション完了まで更新中のデータは他のスレッドから見えません。)

予約業務テーブルの Change Data Capture

次からの2パターンはDB更新を Change Data Capture (CDC) により検知してメッセージに変換するパターンで、DB更新とメッセージ発行の一貫性を保てます。

まず座席や予約の予約業務テーブル自体を CDC するパターンが考えられます。予約業務テーブル更新に付随して確実にメッセージ発行されます。

予約業務テーブルのCDC

ただし逆に予約業務の更新だけをするような制御はできません。この点は二重書き込みに劣るデメリットと言えます。1回の操作で複数の予約業務テーブルが更新される場合に余計なメッセージが発行されます。

無駄なメッセージ発行

その他のデメリットにも触れておきます。CDC プロダクトの Debezium はシングルプロセスでしか動かせない制約があり、CDC が単一障害点(SPoF)になるデメリットもあります。しかし食べログでは CDC を Kubernetes で稼働させているためオートヒーリングによる復旧が可能で、メッセージ発行の一貫性と比べるとデメリットは比較的小さいと言えます。また食べログでは全テーブルを CDC していないので、仕様変更により予約業務テーブルが増えたときは CDC 設定変更が必要になりますが、Terraform で管理しているためこれもあまり大きな問題ではありません。

Outbox テーブルの Change Data Capture

最後に予約業務テーブルと同一トランザクションでドメインイベントとしてのデータを別のテーブルに INSERT してそのテーブルを CDC するパターンです。このイベントを記録するテーブルを Outbox テーブルと呼び、この設計パターンは Transactinal Outbox と呼ばれています。

Transactional Outbox

この設計パターンは前二者のいいとこどりと言えます。DB更新とメッセージ発行の一貫性を保てる上に、Outbox テーブルを INSERT する業務ロジックでメッセージ発行を制御できます。逆に Outbox テーブルの更新忘れがあるとメッセージ発行できないデメリットもありますが、これはアプリケーション設計・実装で対処しやすい課題でしょう。

各設計の特長まとめ

以上3つの設計パターンをまとめると以下のようになります。

二重書き込み 予約業務テーブルの Change Data Capture Outbox テーブルの Change Data Capture
= Transactional Outbox
DB更新とメッセージ発行の一貫性
メッセージ発行の制御
単一障害点の存在
仕様変更時のCDC設定変更の手間 -

CDC を利用する2パターンは単一障害点が増えるデメリットもありますが、前述のように Kubernetes のオートヒールにより自動復旧できるはずなので、そのデメリットよりも通常時にDB更新とメッセージ発行の一貫性が保てるメリットが大きいと評価しました。さらに予約業務テーブルの CDC と Outbox テーブルの CDC = Transactional Outboxパターンを比較して、メッセージ発行が制御できる後者のメリットが大きいと評価しました。

実際、リリース後に1操作で Outbox テーブルにデータが3件 INSERT され処理性能を悪化させるケースが発覚しましたが、発行されたメッセージを後から取捨選択やマージで制御するよりも、Outbox テーブルの INSERT を制御する方が容易でしょう。

こうしてDB更新とメッセージ発行の一貫性は基盤技術レベルで保証され、アプリケーションは業務ロジックに集中できる設計になりました。 概ねうまくいきましたが、食べログでは初採用のアーキテクチャだったこともあり、リリース後の半年で Outbox テーブルが20ギガバイトまで肥大するという想定してなかった事態も起きました。運用して初めてわかる知見も得られ、良い経験でした。

まとめ

この事例のようにマイクロサービス化チームでは分散システムに有用な基盤技術を導入してサービス開発エンジニアに提供し、アプリケーションを高凝集・疎結合な設計にリファクタリングする業務をしています。

ご興味のある方は、ぜひ採用サイトからご連絡をください! エンジニア組織の文化風土などを面接前に知りたい方は、本採用プロセス前にカジュアルな面談も可能です。フリーコメント欄に「カジュアル面談希望」とご記載ください。ご応募お待ちしております!

明日は @megumi_k さんの「プロダクト開発一筋だったエンジニアがリファクタリングに挑んで感じたこと」です。お楽しみに!