メインコンテンツまでスキップ

スケーリングパターン

実現パターン で 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: 余力がある時だけ処理 │
└────────────────────────────────────────────────────┘

パターン4: 読み取りモデルの非正規化

Consumer が遅い原因が「集計処理の重さ」なら、読み取りモデルを非正規化して計算を減らす。

正規化(遅い):
event 到着 → tenant テーブルと JOIN → user テーブルと JOIN → INSERT

非正規化(速い):
event にテナント名・ユーザー名を含めておく → そのまま INSERT
→ JOIN が不要になり Consumer が高速化

パターン5: サンプリング

全イベントを処理する必要がない場合、サンプリングで負荷を下げる。

秒間10万イベントの統計集計:
全件処理: 秒間10万 → Consumer が追いつかない

1%サンプリング: 秒間1000 → 余裕
→ COUNT(*) の結果を100倍すれば近似値が得られる
→ ダッシュボードの「概算値」としては十分

ClickHouse にも SAMPLE 句がある:
SELECT count() * 100 FROM events SAMPLE 0.01

レイヤーごとのスケーリング上限の目安

レイヤー技術目安スケール方法
書き込みPostgreSQL秒間数万Read Replica で読み取り分離
書き込みKafka秒間数百万パーティション追加
伝搬CDC (PeerDB)秒間数万通常十分
伝搬Kafka秒間数百万ブローカー追加
読み取りモデルClickHouse秒間数十万(バッチ)シャード追加
読み取りモデルElasticsearch秒間数万ノード追加

監視すべき指標

バックプレッシャーの兆候を早期に検知するための指標。

指標正常危険対応
Consumer Lag(未処理件数)数百以下数万以上で増加中Consumer スケールアウト
処理遅延(イベント発生〜読み取りモデル反映)数秒数分以上バッチサイズ調整
Consumer スループット安定低下傾向ボトルネック調査
キューのディスク使用量安定増加中消費速度の改善
Kafka の場合:
Consumer Lag = (最新オフセット) - (消費済みオフセット)

正常: Lag = 100 (数秒分)
警告: Lag = 100,000 (数分分) で増加中
緊急: Lag = 1,000,000 で加速的に増加中

まとめ

┌────────────────────────────────────────────────────────────┐
│ 高速・大量トランザクションの対処法 │
├────────────────────────────────────────────────────────────┤
│ │
│ 1. ボトルネックは「消費者」側で起きる │
│ 書き込み(追記)は速い、消費が追いつかないのが問題 │
│ │
│ 2. まずバッチ集約 │
│ 1件ずつ処理 → まとめて処理で10-100倍高速化 │
│ │
│ 3. 次にパーティション並列 │
│ テナントID等でパーティション分割→Consumer並列化 │
│ │
│ 4. 優先度制御で重要イベントを守る │
│ セキュリティアラートは即時、統計は遅延OK │
│ │
│ 5. Consumer Lag を監視する │
│ 遅延が拡大し始めたら早めに対処 │
│ │
└────────────────────────────────────────────────────────────┘

関連ドキュメント