スケーリングパターン
実現パターン で CDC やメッセージキューを使ったイベント伝搬の方法を学びました。
しかし、イベントの量が増えるとどうなるでしょうか。秒間1万件、10万件と増えたとき、アーキテクチャのどこが先に限界を迎えるのか。このドキュメントでは、イベント駆動アーキテクチャのボトルネックの見つけ方と対策パターンを学びます。
どこがボトルネックに なるか
┌─────────────────────────────────────────────────────────────────┐
│ 秒間1万イベント発生時の各レイヤーの限界 │
│ │
│ ① 書き込み(イベントストア) │
│ ┌──────────────────────────────────────────┐ │
│ │ PostgreSQL: INSERT 秒間1万 → 余裕 │ │
│ │ Kafka: 秒間100万 → 余裕 │ │
│ │ → 追記のみなのでロック競合なし │ │
│ │ → ここがボトルネックになることは稀 │ ✅ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ② 伝搬(CDC / メッセージキュー) │
│ ┌──────────────────────────────────────────┐ │
│ │ CDC: WAL を読むだけ → 書き込みに追従 │ │
│ │ Kafka: パーティション並列 → 水平スケール │ │
│ │ → 通常はボトルネックにならない │ ✅ │
│ └────────────────────────────────────────── ┘ │
│ │ │
│ ▼ │
│ ③ 読み取りモデル構築(Consumer) ← ★ここが詰まる │
│ ┌──────────────────────────────────────────┐ │
│ │ ClickHouse: バッチINSERTが必要 │ │
│ │ Elasticsearch: インデクシング負荷 │ │
│ │ 外部API呼び出し: レイテンシ │ │
│ │ → 消費速度 < 生産速度 だと遅延が拡大 │ ⚠️ │
│ └──────────────────────────────────────────┘ │
│ │
└────────── ───────────────────────────────────────────────────────┘
イベントの書き込みは追記(INSERT)のみなのでスケールしやすい。 問題は消費者(読み取りモデル構築)側で起きます。
バックプレッシャー
消費者が生産者に追いつかないと、未処理イベントが溜まり続けます。
正常時:
Producer: ████████████ 秒間1万
Consumer: ████████████ 秒間1万
遅延: 一定(数秒)
バックプレッシャー発生時:
Producer: ████████████████████ 秒間2万(スパイク)
Consumer: ████████████ 秒間1万(変わらず)
未処理: ████████ → 溜まる → 遅延が拡大
時刻 未処理キュー 遅延
0:00 0 2秒
0:10 10万 12秒
0:30 30万 32秒
1:00 60万 62秒 ← ダッシュボードが1分遅れ
危険なのは「じわじわ遅延が拡大する」こと
スパイクが収まっても、溜まったキューを消化しきるまで遅延は戻りません。
0:00〜1:00 スパイク(秒間2万)→ 60万件溜まる
1:00〜 通常に戻る(秒間1万)→ でも消化余力がない
Producer 1万 + 未処理消化 = Consumer の限界1万
→ 未処理が減らない → 遅延がずっと続く
対策: Consumer に「通常時は余力がある」状態を作る
通常: Consumer 能力 1.5万 > Producer 1万 → 余力5千/秒
スパイク後: 余力で未処理を消化 → 遅延が回復
対策パターン
パターン1: パーティション並列
イベントをキー(テナントID等)でパーティション分割し、消費者を並列化。
┌─ Partition 0 (tenant A,D,G...) → Consumer 0
events ────────→ ├─ Partition 1 (tenant B,E,H...) → Consumer 1
(tenant_id └─ Partition 2 (tenant C,F,I...) → Consumer 2
で分散)
→ 消費能力が3倍に
スケールアウト:
パーティション数を増やす → Consumer を増やす → 線形スケール
注意: 同一テナントのイベントは同じパーティションに入るため、テナント内の順序は保証される。異なるテナント間の順序は保証しない(通常不要)。
✅ 保証される: tenant-A の LoginSucceeded → LogoutSucceeded の順序
❌ 保証しない: tenant-A の Login と tenant-B の Login のどちらが先か
パターン2: バッチ集約
1件ずつ処理せず、まとめて処理。ClickHouse のバッチ INSERT と相性がいい。
1件ずつ処理(遅い):
event → INSERT → event → INSERT → event → INSERT
秒間: 1,000件
バッチ処理(速い):
event ─┐
event ─┤→ 1万件まとめて INSERT
event ─┤
... ─┘
秒間: 100,000件
実装:
・時間ウィンドウ(1秒ごとにフラッシュ)
・件数ウィンドウ(1万件溜まったらフラッシュ)
・どちらか先に来た方でフラッシュ
Consumer の実装イメージ:
┌─────────────────────────────────────────┐
│ Buffer (メモリ) │
│ ┌──────────────────────────────────┐ │
│ │ event, event, event, event, ... │ │
│ └──────────────┬───────────────────┘ │
│ │ │
│ if (件数 >= 10000 or 経過 >= 1秒) { │
│ → バッチINSERT to ClickHouse │
│ → バッファクリア │
│ } │
└─────────────────────────────────────────┘
パターン3: 優先度制御
全イベントを同じ優先度で処理する必要はない。
┌─ 高優先度キュー ──────────────────────────────────┐
│ LoginFailed, AccountLocked, PasswordChanged │
│ → リアルタイム処理(セキュリティアラート) │
│ → Consumer: 専用、即座に処理 │
└────────────────────────────────────────────────────┘
┌─ 通常キュー ──────────────────────────────────────┐
│ LoginSucceeded, TokenIssued, PageViewed │
│ → バッチ処理(統計・分析) │
│ → Consumer: バッチ集約、数秒遅延OK │
└───────────────────────────── ───────────────────────┘
┌─ 低優先度キュー ──────────────────────────────────┐
│ InspectTokenSuccess │
│ → 間引きまたは遅延処理OK │
│ → Consumer: 余力がある時だけ処理 │
└────────────────────────────────────────────────────┘