Transactional Outbox パターンとは?データベースとメッセージングの一貫性を保つ方法

1. はじめに

なぜこの技術を学ぶのか?

マイクロサービスアーキテクチャでは、複数のシステム間でのデータの一貫性を保つことが課題になります。特に、データベースへの書き込みと外部のキューやイベントシステムへの送信を同時に行う場合、トランザクションの管理が重要になります。Transactional Outbox はこの問題を解決するための設計パターンです。

知らなかったことで困ったこと

あるプロジェクトで、データベースの更新後に Kafka にイベントを送信する処理を実装した際、DB の書き込みは成功したものの、ネットワーク障害により Kafka への送信が失敗しました。このような場合、DB には変更が適用されているが、他のサービスには伝わっておらず、データの整合性が崩れる問題が発生しました。

Transactional Outbox を学ぶメリット

2. Transactional Outbox とは?

送信箱(Outbox)の概念

Transactional Outbox は、通常のメールの「送信箱(Outbox)」のように、一時的に送信すべきメッセージを保管し、それを後で処理する仕組みです。これは、データベースへの書き込みとイベントの送信をアトミックに保証できない という課題を解決するために導入されます。

2つのシステムに書き込む際のトランザクションの課題

例: 注文確定処理での課題

graph TD;
    U[ユーザー] -->|注文確定| A[アプリケーション];
    A -->|DBに注文を保存| B[データベース];
    B -->|在庫システムに通知| C[イベントキュー];
    C -->|在庫減少処理| D[在庫システム];
    B -.->|通知失敗| C;

Transactional Outbox の仕組みとトランザクションの課題の解決策

graph TD;
    subgraph トランザクション
        A[アプリケーション] -->|DB 書き込み| B[データベース];
        A -->|Outboxテーブル更新| O[Outboxテーブル];
    end
    B -->|トランザクションコミット| O;
    O -->|Outbox Processor による処理| P[Outbox Processor];
    P -->|イベント送信| C[イベントキュー];
    C -->|イベント処理| D[イベント処理サービス];

ポイントは次の3点です。

OutBox Transactional pattern の特徴

  1. アプリケーションがデータを更新

    • 通常のデータテーブル(例: orders)にデータを保存する。
    • 同時に Outbox テーブルにもイベントを記録する。
  2. トランザクションのコミット

    • アプリケーションのデータと Outbox のデータを同じトランザクションで確定させる。
  3. Outbox Processor がポーリング(または CDC を利用)

    • 一定間隔で Outbox テーブルをスキャンし、未処理のイベントを取得する。
  4. メッセージングシステムにイベントを送信

    • SQS/Kafka などのメッセージングシステムに送信し、他のシステムと非同期に連携する。
  5. 処理済みのイベントを削除

    • イベントの送信が成功したら、Outbox テーブルから削除するか、processed_at フィールドを更新する。

3. どのように実装するのか?

データベースの設計(Outbox テーブルのスキーマ)

CREATE TABLE outbox (
    id SERIAL PRIMARY KEY,
    event_type TEXT NOT NULL,
    payload JSONB NOT NULL,
    status TEXT CHECK (status IN ('PENDING', 'PROCESSED')) NOT NULL DEFAULT 'PENDING',
    created_at TIMESTAMP DEFAULT NOW()
);

イベント通知の仕組み(ポーリング vs. CDC)

Outbox Processor は、未処理のイベントを取得し、メッセージングシステムに送信する仕組みが必要です。 DB の変更をリアルタイムに検知する CDC(Change Data Capture)を利用することで、ポーリングによるDBへの負荷を軽減できます。

メッセージの送信(SQS, Kafka, SNS との連携)

Outbox Processor は、未処理のイベントを取得し、メッセージングシステムに送信する必要があります。 同一メッセージが複数回送信される可能性があるため、メッセージングシステムのSubscriber側で冪等性を担保する必要があります。 メッセージングサービスの中には、冪等性を担保するための機能を提供しているものもあります。 例えば、Kafka の enable.idempotence=true や SQS の MessageDeduplicationId を利用することで、重複メッセージを排除できます。

4. 運用上の課題と解決策

4.1. Outbox テーブルの肥大化 → 定期削除・アーカイブ戦略

Outbox テーブルは、送信されたイベントの記録を保持するため、時間が経つにつれてデータ量が増加し、クエリ性能が低下する可能性があります。 解決策として、定期的にデータを削除するなどの対策が必要です。

4.2. イベントの二重送信 → Idempotency(冪等性)の担保

イベントが送信される際に、ネットワーク障害やプロセスのクラッシュなどにより、同じイベントが複数回処理される可能性があります。 イベントの購読側で冪等性を担保するのが良いですが、メッセージングシステム側で冪等性を担保することもできます。

4.3. ポーリングの負荷 → CDC(Change Data Capture)との比較

ポーリング(定期的にDBをチェックする方式)は、頻繁にクエリを実行するため、データベースの負荷が増加します。 以下の対策を検討し、負荷を軽減することが重要です。

4.4. 送信失敗時のリトライ → DLQ(デッドレターキュー)の活用

ネットワークエラーや一時的な障害によってイベントの送信に失敗した場合、適切なリトライ戦略が必要です。 リトライが成功しない場合は、DLQ(デッドレターキュー)に送信し、手動で処理するなどの対策が必要です。

4.5. 送信遅延の問題 → ポーリング間隔の最適化

ポーリング方式では、DBの状態を定期的にチェックするため、リアルタイム性が低下する可能性があります。 リアルタイム性が重要な場合は、CDC を活用するなど、適切な方法を選択することが重要です。

5. DynamoDB を使った場合の課題

CDC(Change Data Capture)を利用する場合、RDBMS と比較して DynamoDB は変更をリアルタイムで検知しやすいという利点があります。 しかし、DynamoDB を利用する場合、ホットキー問題やシャード数の変動など、特有の課題が発生する可能性があります。

DynamoDB Streams でのイベント通知の違い

DynamoDB では、トランザクション内でイベントを Outbox テーブルに保存する代わりに、DynamoDB Streams を利用して変更を検知し、イベントを処理することが可能です。

ホットキー問題(特定のキーに負荷が集中する場合)

DynamoDB はパーティションキーを元にデータを分散するが、特定のキーに書き込みが集中するとホットキー問題が発生し、スループットが制限される。 パーティションキーの設計に注意することで、ホットキー問題を回避することができます。

例: orderId ではなく orderId#shard-0, orderId#shard-1 のようにランダムな識別子を追加する。

シャード数の増減とスケーリングの仕組み

DynamoDB Streams のシャード数は、テーブルのパーティション数に基づいて決定される

対策

6. まとめ

参考

comments powered by Disqus