アウトボックスパターン
Outboxは、ドメインサービスが1つのローカルトランザクションにビジネス変更と対応するイベントをリポジトリに書き込むアーキテクチャパターンです。イベントを外部のバス/キューに公開するには、'outbox'テーブルを読み取り、レコードをリレーする別のセキュアプロセス(パブリッシャー)によって非同期に実行されます。このアプローチは「、最初にデータベースに、次にバスに」レースを排除し、失敗した場合でも信頼性の高い配信を提供します。
1)いつ適用するか
適合:- コンテキスト間のイベントを含むマイクロサービスとモジュラーモノリス。
- 「イベントが失われない状態が固定されている」こと↔確認する必要があります。
- 私たちはidempotenceと制御された再配達が必要です。
- いくつかのリソースでの厳しいグローバルトランザクションは重要です(明示的な契約を持つTCC/sagasよりも優れています)。
- 真実の専用ソースはありません(状態はイベントが生成された場所に保存されません)。
2)目的・財産
アトミック書き込み:ドメインレコード+アウトボックス-1つのトランザクションで。
少なくとも一度は出版物:私達は反復を許可し、損失を除いて下さい。
消費者のアイデンティティ:加入者側に対する保護。
正確に一度有効:outbox+idempotent consumer+dedupの組合せによって達成される。
明確なテレメトリー-ビジネス取引やイベントを相関させます。
3)データスキーマ(例)
sql
-- Domain table (example: orders)
CREATE TABLE orders (
id UUID PRIMARY KEY,
tenant_id TEXT NOT NULL,
status TEXT NOT NULL,
total_amount NUMERIC(12,2) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT now()
);
-- Outbox
CREATE TABLE outbox (
id UUID PRIMARY KEY, -- event_id aggregate_type TEXT NOT NULL, -- 'order'
aggregate_id UUID NOT NULL, -- order_id tenant_id TEXT NOT NULL,
type TEXT NOT NULL, -- 'OrderCreated'
payload JSONB NOT NULL, -- serialized headers event JSONB NOT NULL DEFAULT '{}':: jsonb,
occurred_at TIMESTAMP NOT NULL, -- time in domain transaction available_at TIMESTAMP NOT NULL, -- earliest publish time (backoff)
published_at TIMESTAMP, - is filled by the attempts INT NOT NULL DEFAULT 0,
error TEXT
);
CREATE INDEX ON outbox (available_at) WHERE published_at IS NULL;
CREATE INDEX ON outbox (tenant_id, available_at) WHERE published_at IS NULL;
4)アプリケーション層
pseudo begin tx domainChange () # INSERT/UPDATE in domain table insert into outbox (event) # event with aggregate/tenant commit tx keys
コミットが成功すると、アウトボックス内のイベントが存在することが保証されます。コミット後にアプリケーションが低下すると、パブリッシャーは追いつくでしょう。
5)出版社(読者→出版社)
タスク:- 定期的に未発表のイベント('published_at IS NULL'と'available_at<=now()')、バッチを読み取ります。
- バス/キューに公開してみてください。成功した場合は'published_at'とマークします。
- エラーの場合-'attempts'を増加させ、'available_at'を未来(指数関数的バックオフ)に置き、'error'と書く。
- テナント/キー(公平性)の制限を尊重し、製品をブロックしないでください。
pseudo loop:
events = select from outbox where published_at is null and available_at <= now()
order by occurred_at limit BATCH_SIZE for update skip locked
for e in events:
try:
broker. publish(topicFor(e), serialize(e. payload), headers(e))
markPublished(e. id, now())
except Retryable:
backoff = computeBackoff(e. attempts)
reschedule(e. id, now()+backoff, attempts+1, last_error)
except NonRetryable:
moveToDLQ (e) or markError (e) # by sleep (POLL_INTERVAL) policy
6) Idempotencyおよび重複除外
消費者側(受信トレイ/Idempotencyストア):sql
CREATE TABLE inbox (
consumer_name TEXT,
event_id UUID,
processed_at TIMESTAMP NOT NULL,
PRIMARY KEY (consumer_name, event_id)
);
アルゴリズム:イベントを受信するときは、まず'inbox'の'INSERT'を試してみてください。重要な競合がある場合、イベントはすでに処理されています→no-op。次はビジネスロジックです。
発行者側:ヘッダーの'Idempotency-Key'(例えば、'event_id')で、bus/broker/proxyが重複をフィルタできるようにします。
7)秩序と因果関係
'agregate_id'によるローカルオーダーは'occured_at'をソートして「by key」を発行することで提供される。
パーティションを持つログバスの場合-1つの集計のイベントが同じパーティションになるように、'agregate_id'/'tenant_id'キーを持つパーティション。
注文が重要な場合は、クロスフローのシングルキーパブリッシャーレースを避けてください。
8) CDC(変更データキャプチャ)
アクティブなパブリッシャーの代わりに、CDCを使用できます。エンジンはデータベースのトランザクションログを読み取り、'outbox'行をバスに変換します。デメリット-操作の複雑化とDBMSの詳細へのネクタイ。どちらのアプローチも有効です。能力とSLOによって選択します。
9)エラー、DLQ、 Redrive
Retryable (network、 limits)-'attempts'を増やし、'available_at'(指数バックオフ+ジッタ)を延期します。
再試行不可(無効なスキーム/契約)-豊富なメタデータを持つDLQ/Dead-Letterトピックに転送されます。
Safe Redrive:バッチ、レート制限、スキームの検証、本番トラフィックの優先度。
10)複数のテナントおよび限界
必要なタグ: 'tenant_id'、 'plan'、 'region'-'outbox。「ヘッダー」
テナントごとの公平性:出版社は出版物の「窓」とテナントへの試みの限界を配布します。
レジデンシー:ドメインデータと同じ領域にアウトボックスを保存します。地域間の出版物-集計/要約のみ。
11)安全性とコンプライアンス
テナント/リージョンポリシーのペイロード/ヘッダーのPIIエディション。
バスが外国の場合、ペイロードの署名/暗号化。
すべての状態遷移を監査:作成、公開、エラー、再描画。
12)観測可能性
メトリクス:- Publication lag ('now-occurred_at' p50/p95/p99)。
- 成功率、エラー率、原因分布。
- Outboxのサイズ(未発表の数)、再試行/秒
- テナントごとのグラフのスループットと遅延。
- 相関'event_id'/'aggregate_id'/'saga_id';「db-tx」、 「publish」、 「retry」に相当します。
- 注釈:'attempt'、' backoff_ms'、 'dlq=true'。
- 成功のための短い記録;エラー/再描画ごとに完全な詳細。
13)テストおよび混乱
Atomicity test:公開前にドメイントランザクションをコミットした後、人工的に「落下」する-イベントは後でリリースする必要があります。
重複テスト:同じイベントを数回公開します。消費者は正確に1つの効果(受信トレイ)を実行します。
Order test: 1つの集計によるイベントのバッチ-シーケンス/idempotenceチェック。
カオス:ブローカー障害、データベース遅延の増加、スプリットブレインパブリッシャー、クロックスキュー。
14)コンフィギュレーションテンプレート(例)
yaml outbox:
poll_interval_ms: 200 batch_size: 200 order_by: occurred_at backoff:
strategy: exponential_full_jitter initial_ms: 250 max_ms: 10_000 max_attempts: 20 fairness:
per_tenant_parallelism: 4 per_key_serial: true
publisher:
rate_limit_per_sec: 500 headers:
idempotency_key: event_id schema_version: v3 dlq:
enabled: true topic: myapp. events. dlq include_metadata:
- error
- attempts
- source_table
- tenant_id
- aggregate_id
15)サガとリトリートとの統合
Outbox-sagaステップの「セキュリティトランスポート」:ローカルトランザクションの書き込みエフェクトとコマンド/イベント;出版-信頼性と投与。
リピートとバックオフポリシーは、'Retry-After'とCircuit Breakerと一致している必要があります。「リトレイストーム」を避けてください。
16)典型的なエラー
ドメイン状態のコミット後にイベントを作成します。フォール中に損失が発生する可能性があります。
'outbox'→publishing latencyにインデックス/アーカイブはありません。
「SKIP LOCKED」なし、またはシャーリングなしのパブリッシャー-競争とブロック。
消費者の間の特権の欠如-重複と副作用。
DLQ/ログにマスキングせずに混合するPII。
公平性のない単一のグローバルパブリッシングキュー-「騒々しい」テナントは誰もが遅くなります。
遅延監視の欠如→潜在的な劣化。
17)クイック戦略の選択
開始レベル:データベースからのポーリング、100-500バッチ、フルジッターバックオフ、消費者のための受信トレイ。
高負荷:トランザクションログからのCDC、 'tenant_id/aggregate_id'、テナントによるWFQ。
集計による厳密な順序:キーごとのシリアル出版(mutex)、キーでトピックのパーティション。
コンプライアンス/PII:ペイロード暗号化、DLQエディション、リージョナルアウトボックス。
18)売り上げ前のチェックリスト
- 同じトランザクションでドメインの変更と'outbox'への書き込みが発生します。
- パブリッシャーはバッチを処理し、'SKIP LOCKED'、ジッタとリミット付きのバックオフを使用します。
- 消費者はidempotent(テーブル'受信トレイ'/デッドアップログ)です。
- DLQとSecure Releaseが設定されています。
- p95/p99スレッショルドのラグ/エラーとアラートメトリック。
- キーの順序は保証されます(バッチ/連続)。
- アーカイブ/保持'アウトボックス'とクリア公開されたレコード。
- PIIポリシーと状態移行監査。
- commitとpublish、 duplicates、 orderの間でテストをドロップします。
- イベント契約ドキュメント(スキーマ/バージョン/互換性)。
結論
アウトボックスパターンは、「DB ↔ bus」の「脆弱な」バンドルを信頼できるパイプラインに変えます。原子状態固定、保証(「少なくとも一度」ではありますが)出版、idempotent加入者、制御された再描画です。適切なテレメトリー、制限、スキーマ規律により、正確に一度の動作を実用的に提供し、分散トランザクションの複雑さを軽減し、クラッシュやピーク負荷に対するシステムの回復力を高めます。