1. はじめに
ClickHouse Managed Postgres と ClickHouse Cloud を ClickPipes CDC でつなぐ構成を、これまで何度か試してきました。CDC を触っていて気になっていたのが「どれくらい書き込めば、CDC 経由で下流の ClickHouse 側に効いてくるのか」です。軽い書き込みだと ClickHouse はまったくの無風で、効かせるにはかなりの書き込み負荷が必要になりそうでした。
そこで今回は、CDC 対象テーブルへ直接 INSERT/UPDATE を叩き込む 書き込み増幅ワークロードで、変更行レートを段階的に最大 28万行/秒まで引き上げて、どこで効いてくるのかを試してみました。狙いは一点です。
どれだけ書き込みレートを上げれば、CDC 経由で ClickHouse 側に負荷をかけられるのか?
1.1. 今回の検証ゴール
| # | 検証項目 | OK 条件 |
|---|---|---|
| G1 | 書き込みレートを上げたとき CDC のレプリケーションラグが伸び始める点を特定 | 変更行レートを 1k→28万 行/秒で振り、レプリケーションスロットのラグ(バイト)が増え続けるようになるレートを示す |
| G2 | 同じ取り込み負荷の下で ClickHouse の OLAP 応答が劣化するか | 各段階で OLAP を並走計測し、負荷なし(P0)比で示す。parts・CH CPU もあわせて取得 |
| G3 | パイプ自体をチューニング(Pull batch size 拡大)すれば上限は上がるか | batch size を 10倍にした A/B で取り込みレートを比較 |
1.2. 結論(先出し)
- INSERT 負荷・UPDATE 負荷のどちらも、最大 28万行/秒まで ClickHouse の OLAP 応答はほとんど変わらず(重いクエリで 2.0 秒 → 最大 2.7 秒、約 1.3 倍にとどまる)、active parts も 4〜10 で急増せず、UPDATE 負荷後の
FINALクエリも 0.035 秒で返る - ボトルネックは ClickHouse の計算ではなく CDC パイプのスループット。持続的な取り込みの上限は 約 3〜5万行/秒(約 13 MB/秒の WAL 相当) で、これを超えるとレプリケーションスロットが GB 級に膨らみ、レプリケーションラグが際限なく伸びる
- Pull batch size を 10倍(10万→100万行)にしても上限は動かない。バッチが大きくなった分だけ処理時間も比例して延びるだけで、これはデフォルトの CDC コンピュートでの単一ストリーム上限。引き上げるにはバッチではなく取り込み側コンピュートの増強が要る(公式の手段。本記事では未検証)
1.3. 検証環境
| 項目 | 値 |
|---|---|
| Managed Postgres | ClickHouse Managed Postgres、r8gd.4xlarge(16 vCPU / 128 GiB、No standby)、PostgreSQL 18.4 |
| ClickHouse Cloud | 1 replica / 12 GiB / 3 vCPU(オートスケール固定 12 GiB ⇄ 12 GiB)、ClickHouse 25.12.1 |
| CDC | ClickPipes Postgres CDC(PeerDB ベース)、Pull batch size 100,000 / Sync interval 60 秒 |
| CDC 対象テーブル |
cdc_probe(id bigint PK / ts_pg timestamptz / payload text) |
| 負荷ツール | pgbench、書き込み増幅(INSERT 負荷 / UPDATE 負荷) |
| クライアント | ローカル環境 |
ClickHouse 側は 意図的にミニマル構成のまま固定しています。小さい ClickHouse の方が低いレートで限界に達するため、現実的な負荷で「ClickHouse 側の効果」に到達できるという狙いです。さらに 1 replica に固定することで、CDC の取り込み(書き込み・マージ)と OLAP クエリが同じコンピュートを奪い合う状態を保ち、もし干渉が起きるなら見えるようにしています。
ClickHouse Cloud はオートスケールが効くと、負荷の最中にコンピュートがスケールアウトされるため、「固定 3 vCPU での限界点」が測れなくなります。今回はスケーリング設定を 12 GiB ⇄ 12 GiB(固定)にし、検証中ずっと 1 replica のままで推移する事を確認しています。
2. 検証スクリプトとワークロード
CDC 対象テーブルへ直接書き込む方式にしたのは、連続 INSERT が本質的にシーケンシャルで、中規模の Postgres でも 10万行/秒を軽く超えられるためです。複雑な OLTP を組むより、変更行を効率よく大量生成できます。
CDC 対象には、既に ClickPipes パイプに乗って同期済みだった cdc_probe テーブルを流用しました。id は主キー、ts_pg に書き込み時刻を入れておき、ClickHouse 側の最新 ts_pg と現在時刻の差でレプリケーションラグを測ります。
2.1. 書き込み増幅の 2 モード
| モード | 内容 | 狙い |
|---|---|---|
| A: INSERT 負荷 | 100行ずつのバッチ INSERT を連続投入(id はシーケンス採番) | 純粋な書き込み負荷 |
| B: UPDATE 負荷 | ホット行集合(id 1〜2000)を範囲更新しバージョンを量産 | ReplacingMergeTree のマージ・FINAL 圧。少ない行数で ClickHouse 側に負荷をかける狙い |
ClickPipes は CDC を ReplacingMergeTree にマッピングするため、UPDATE は新しいバージョン行として追記されます。同じ行を何度も更新するほどバージョンが増えてマージが重くなるはずなので、ClickHouse 側の処理を重くするにはこちらが効くと見込んでいました。
-- モードA: INSERT 負荷(pgbench カスタムスクリプト, batch は -D で指定)
INSERT INTO cdc_probe (ts_pg, payload)
SELECT clock_timestamp(), repeat('x', 200)
FROM generate_series(1, :batch);
-- モードB: UPDATE 負荷(ホット行 id 1..2000 を範囲更新, span は -D で指定)
\set span :span
\set start random(1, 1901)
UPDATE cdc_probe
SET payload = repeat('y', 200), ts_pg = clock_timestamp()
WHERE id >= :start AND id < :start + :span;
2.2. 書き込みレート
各モードを 4 段階のレートで流しました。rows/sec は pgbench の tps × バッチ行数 で制御し、ClickHouse 側の行数増分でも裏取りしています。
| 段階 | 目標 rows/sec | 狙い |
|---|---|---|
| T1 | ~1,000 | ごく軽い負荷(基準) |
| T2 | ~10,000 | ラグが出始めるか |
| T3 | ~50,000 | 遅れ始めの入口 |
| T4 | 最大(無制限・12 クライアント) | パイプ/ClickHouse の限界点 |
2.3. 計測パターンとラグの見方
| パターン | 書き込み | モード | OLAP 並走 |
|---|---|---|---|
| P0 | なし | — | あり(ClickHouse 基準値) |
| Px-A | T1〜T4 | INSERT 負荷 | あり |
| Px-B | T1〜T4 | UPDATE 負荷 | あり |
OLAP には CHBenCHmark の分析クエリから代表 3 本(軽い Q6、中程度の Q10、最も重い Q21)を選び、各段階で実行時間を計測しました。これらは cdc_probe ではなく、別途用意した TPC-C 由来のテーブル(700万行規模)に対して走るため、「CDC の取り込み負荷が、無関係な OLAP クエリの足を引っ張るか(=ClickHouse のコンピュート共有による干渉)」を見ています。
CDC のラグは、「秒単位のデータ鮮度」を 1 つで返す公式メトリクスが見当たらないため、役割を分けて 3 つの信号を併用しました。
| 信号 | 取得元 | 何を見るか |
|---|---|---|
| スロットのラグ(バイト) |
pg_replication_slots + ClickPipes コンソール |
パイプが追いつけず WAL が溜まり始める点(遅れ始めを見る主指標) |
| 取り込みスループット | ClickPipes コンソール(CDC Syncs / Rows synced) | 実際にどれだけ捌けているか |
| レプリケーションラグ(秒) | ClickHouse の now64() - max(ts_pg)
|
読者目線の「データが何秒古いか」 |
レプリケーションラグ(秒)は ClickPipes のバッチ間隔が下限になるため、低負荷では負荷に起因する変化が埋もれます。パイプが遅れ始めたかは、スロットのラグ(バイト)が増え続けるかを主に見ます。
3. 検証結果
3.1. 達成した書き込みレート
12 クライアントの無制限実行で、クライアント側は楽に 28万行/秒を生成しました。
| 段階 | INSERT 負荷(行/秒) | UPDATE 負荷(行/秒) | データ量目安(MB/秒) |
|---|---|---|---|
| T1 | 998 | 987 | 約 0.2 |
| T2 | 10,055 | 10,331 | 約 2 |
| T3 | 50,182 | 49,814 | 約 10 |
| T4 | 284,560 | 279,260 | 約 57 |
データ量目安は 1 行あたり約 200 バイト(payload)で換算したざっくり値です。Postgres 側で生成される WAL や CDC が運ぶ量は、付帯情報ぶんこれより大きくなります。
これらの数値は「1 トランザクション 100 行・1 行約 200 バイト」という書き込み形状での値です。1 トランザクションあたりの行数を増やす(バルク INSERT や COPY)と、論理デコードのトランザクション単位のオーバーヘッドが薄まり、同じパイプでも行/秒はもう少し伸びる可能性があります(逆に 1 行ずつコミットすると遅くなりやすいと考えられます)。一方で MB/秒の上限(後述の約 13 MB/秒)はバイト処理側の制約に近いとみられ、行サイズが大きいほど行/秒は下がります。コミット粒度・行サイズを変えた比較は今回は未検証で、今後の課題とします。
ClickPipes の CDC Table Stats でも、cdc_probe への Inserts 約 3,498万 / Updates 約 1,974万が記録され、2 モードの書き込みが想定どおり流れたことが確認できます。
3.2. ClickHouse OLAP はどのレートでも変わらない
OLAP の応答時間(秒)は、負荷なし(P0)と最大負荷(T4、28万行/秒)でほぼ変わりませんでした。
| クエリ | P0(負荷なし) | INSERT T4(28万/秒) | UPDATE T4(28万/秒) |
|---|---|---|---|
| Q6(軽) | 0.030 | 0.03〜0.05 | 0.03〜0.04 |
| Q10(中) | 1.34 | 1.27〜1.42 | 1.29〜1.44 |
| Q21(重) | 2.09 | 2.31〜2.68 | 2.09〜2.19 |
T1〜T3 も同様にフラットでした。一番重い Q21 が INSERT T4 でやや上振れしますが、レートに対して単調ではなく(UPDATE T4 では P0 並みの 2.1 秒台)、CDC 取り込みによる明確な劣化とは言えません。
3.3. ボトルネックは CDC パイプ — スロットのラグが伸び続ける
一方で、レプリケーションスロットのラグ(バイト)はレートに応じてはっきり伸びました。
| 段階(達成 行/秒) | スロットのラグの推移 | レプリケーションラグ(秒) |
|---|---|---|
| INSERT T2(1万) | 約 32 MB で追従(鋸歯) | ~10 |
| INSERT T3(5万) | 71 → 101 → 132 MB(増え続ける=遅れ始め) | 4 → 9 |
| INSERT T4(28万) | 1.35 → 2.24 → 3.06 GB(急増) | 13 → 30 |
| UPDATE T4(28万) | 1.4 → 2.15 → 2.85 GB(急増) | 57 → 68 |
5万行/秒あたりからスロットが溜まり始め、28万行/秒では GB 級まで膨らみます。ClickPipes の CDC Syncs を見ると、10万行のバッチを 1 本ずつ逐次処理 しており、INSERT では 1 バッチ約 3 秒、UPDATE 負荷では 約 5〜6 秒かかっていました。UPDATE の方がバッチ処理が重く、実効的な取り込みレートがさらに落ちます。
ドレイン中に ClickHouse の行数増分を測ると、15 秒で約 70万行=持続的な取り込みレートはおよそ 4.6万行/秒でした。Managed Postgres のネットワーク送出も約 13 MB/秒で頭打ちになっており、これがちょうど 5万行/秒で遅れ始める閾値と一致します。
3.4. ClickHouse 側はずっと余裕
ClickHouse 側に効かせる狙いだった UPDATE 負荷でも、ClickHouse 側は終始余裕でした。
- active parts はどのレートでも 4〜10 で、バージョンが増えても急増しない(ClickPipes が効率よくバッチ・マージしている)
- UPDATE 負荷を最大レートで流した後、
cdc_probeはcount()で 2,098万バージョンを抱えていましたが、FINAL付きクエリは 0.035 秒で返りました - ClickHouse の CPU はピークでも約 1.5 / 3 コア(コンソールの CPU usage per replica)。28万行/秒の取り込み中でも半分しか使っていません
つまり、ミニマルな 3 vCPU の ClickHouse でも、この CDC 経路の取り込みでは計算が飽和しませんでした。
3.5. パイプをチューニングしても上限は動かない
「パイプ自身を速くすれば ClickHouse がボトルネックに変わるのでは」と考え、Pull batch size だけを 10万 → 100万行(10倍) に変えて INSERT の最大負荷を測り直しました(単一変数の A/B、Sync interval は 60 秒のまま)。
| 指標 | batch=100,000 | batch=1,000,000 |
|---|---|---|
| 1 バッチの行数 | 10万 | 100万 |
| 1 バッチの所要 | 約 3 秒 | 約 30 秒 |
| 取り込みレート | 約 4.6万 行/秒 | 約 3.3〜4.6万 行/秒(ほぼ同じ) |
| スロットのピーク | 約 3.0 GB | 約 4.6 GB(悪化) |
| レプリケーションラグの粒度 | 数秒刻み | 100万行ごとにまとめて反映され、値が大きく上下する(悪化) |
| ClickHouse CPU(最大) | 約 1.5 / 3 コア | 約 1.5 / 3 コア(不変) |
ドレイン時、ClickHouse の行数はちょうど 100万行ずつ・約 30 秒間隔で増えました。バッチが 10倍になった分、処理時間も約 10倍。取り込みレートは変わりません。バッチを大きくしても ClickHouse CPU は 1.5 コアのままで、ボトルネックに変えることもできませんでした。むしろスロットのピークが増え、レプリケーションラグが粗くなるデメリットだけが出ています。
4. 考察
4.1. なぜ ClickHouse ではなくパイプが先に頭打ちになるのか
ClickPipes(PeerDB)の CDC は、Postgres のレプリケーションスロットからの論理デコードで WAL を読み出します。論理デコードは 1 スロットあたり実質シングルストリームで、ClickPipes 側も 10万行のバッチを 1 本ずつ逐次に pull → push します(3.3 のコンソール画面で、Pulling 状態のバッチが常に 1 本だけなのが確認できます)。
このため、書き込みレートをいくら上げても、CDC が運べるのは「単一ストリームのバッチ処理スループット」が上限になります。今回の構成では約 3〜5万行/秒(約 13 MB/秒)で、Managed Postgres のネットワーク送出量とも整合しました。ClickHouse 側に届く時点で既に流量が絞られているので、ClickHouse の計算は飽和しようがない、という構図です。
4.2. UPDATE 負荷が INSERT 負荷より重い理由
UPDATE 負荷ではバッチ所要が 3 秒から 5〜6 秒へ延びました。CDC の UPDATE は ReplacingMergeTree へのバージョンの積み増し(実質 upsert)として適用されるため、INSERT より 1 行あたりの処理が重くなります。結果として UPDATE の方が実効的な取り込みレートは低く(約 1.7〜2万行/秒相当)、同じ 28万行/秒を流してもレプリケーションラグがより大きく出ました(68 秒)。ただし parts は 4〜10 のまま、FINAL も 0.035 秒で、ClickHouse 側のマージが追いつかなくなる兆候は見られませんでした。バージョンが溜まっても効率よく解決できていると考えられます。
4.3. パイプのパラメータでは限界を越えられない
Pull batch size を 10倍にしても取り込みレートが変わらなかったことから、この上限は「1 バッチあたりの固定オーバーヘッド」ではなく、デコードと転送・適用そのもののスループットで決まっていると考えられます。ClickPipes の他のパラメータのうち、Parallel threads や Snapshot 系は初期ロード専用で定常 CDC には効かず、Sync interval は backlog がある間はバッチが連続実行されるため上限には影響しません。
4.4. 公式が挙げる継続 CDC のスケール手段(今回は未使用)
ここで補足しておくと、ClickHouse の公式ドキュメントは CDC のスケール手段を分けて説明しています。
- 初期ロードの並列スナップショット:初期ロード・バックフィルを高速化するための並列化で、定常 CDC のスループットには効きません。今回 Parallel threads / Snapshot 系を変えても定常 CDC が動かなかったことと一致します。
- 取り込み側コンピュートの増強:ClickPipes の CDC コンピュート(レプリカの CPU/メモリ。スケーリング API で 1〜32 コアまで)を増やす方法と、ClickHouse 側の複数レプリカ+チャンクによる並列取り込みです。
つまり、本記事の約 3〜5万行/秒は CDC コンピュートをデフォルトのまま固定したときの単一ストリーム上限であって、絶対的な限界ではありません。書き込みレートをさらに捌きたい場合の正攻法は、Pull batch size のようなパラメータではなく、この取り込み側コンピュートを増やすことになります(本記事では未検証で、今後の課題とします)。
5. まとめ
| 観点 | 結果 |
|---|---|
| ClickHouse OLAP(最大 28万行/秒の取り込み下) | P0 比でほぼ不変(重い Q21 で 2.0 → 最大 2.7 秒) |
ClickHouse の parts / FINAL
|
parts 4〜10、FINAL 0.035 秒。マージは追いつく |
| ClickHouse CPU | ピーク約 1.5 / 3 コア。常に余裕 |
| CDC パイプの取り込み上限(デフォルトコンピュート時) | 約 3〜5万行/秒(約 13 MB/秒)。超過でスロットが GB 級まで増加 |
| Pull batch size 10倍 | 取り込み上限は不変。スロット・レプリケーションラグはむしろ悪化 |
「CDC 経由で ClickHouse にどれだけ負荷をかけられるか」という問いの答えは、この経路では ClickHouse の計算にはほとんど負荷をかけられない、でした。ボトルネックは単一ストリームの CDC パイプ側にあり、書き込みを増やしても増えるのは ClickHouse の負荷ではなくレプリケーションラグです。なおこの上限は CDC コンピュートをデフォルトで固定したときの値で、公式が挙げる手段(取り込み側コンピュートの増強)で引き上げられます(本記事では未検証)。
5.1. 今後の課題
-
書き込み形状を変えた比較:1 トランザクションあたりの行数(バルク INSERT・
COPY)や 1 行のサイズを変え、取り込みレート(行/秒・MB/秒)がどう動くか - 取り込み側コンピュートの増強:ClickPipes の CDC コンピュート(CPU/メモリ、1〜32 コア)や ClickHouse 側の複数レプリカを増やし、本記事の約 3〜5万行/秒がどこまで上がるか(公式が挙げるスケール手段の実測)
- 他のマネージド/セルフホスト Postgres での再現:CDC は WAL ベースの汎用的な仕組みのため、同様の傾向が得られると考えられる
参考
- ClickPipes for PostgreSQL CDC | ClickHouse Docs — ClickPipes CDC の設定・制約
- Scaling ClickPipes for Postgres CDC | ClickHouse Docs — CDC コンピュート(CPU/メモリ、1〜32 コア)のスケーリング
- Postgres CDC in ClickHouse, A year in review | ClickHouse Blog — 並列スナップショット(初期ロード)・チャンク/複数レプリカによる並列取り込み
- ReplacingMergeTree | ClickHouse Docs — CDC の UPDATE/DELETE がバージョンとして積まれる仕組み
- PostgreSQL: Logical Decoding — レプリケーションスロット/論理デコード
- Adjusting ClickHouse Cloud Scaling — オートスケールの挙動(固定にする根拠)
- ClickHouse Managed Postgres の CDC はどれくらい速い? — 関連: CDC 遅延の実測






