はじめに
2025 年 11 月 18 日、Cloudflare で大規模な障害が発生しました。
Cloudflare が公開した記事によると、この障害の原因は ClickHouse のアクセス制御変更によるもので、system.columnsへのクエリが二重の結果を返すようになったことでした。
本記事では、この障害のメカニズムを解説し、実際に ClickHouse を使って再現してみます。
障害の概要
何が起きたのか
Cloudflare は 11 月 18 日 11:05(UTC)に「データベースアクセス制御の変更」を展開しました。この変更により、ClickHouse の分散クエリが従来のシステムアカウントから「元のユーザーアカウント」での実行に移行されました。
Cloudflare のアーキテクチャ
Cloudflare は ClickHouse を分散構成で運用しているようです。
-
シャード(
r0,r1,r2など): 実際のデータがあるテーブル -
分散テーブル(
default): 実際のデータはなく、見せかけだけのテーブル
default には Distributed というテーブルエンジンが存在します。
このテーブルエンジンはユーザーからクエリを受けた時に、実際のデータを持つシャードで r0, r1, r2 などにクエリを振り分けて発行し、結果をまとめて返すことができます。このエンジンのおかげで、複数のシャードをあたかも 1 つのテーブルのよう扱うことができるのです。
障害発生のメカニズム
Cloudflare はデータベースアクセス制御の変更を行いました。
この時実際に行っていたのは、分散クエリをユーザーアカウントが実行できるようにするため、シャードのメタデータをユーザーにもアクセス可能になるようにしたのです。
-
変更前
- ユーザーは default のみ見える
- 分散クエリはシステムアカウントで実行され、シャードはユーザーからは見えない
-
変更後
- ユーザーは default が見える
- 分散クエリを叩くことができる
- r0 などのシャードをユーザーが見ることができる
問題のクエリ
SELECT name, type FROM system.columns
WHERE table = 'http_requests_features'
ORDER BY name;
権限変更後はユーザーからはdefaultとr0のデータベースにhttp_requests_featuresテーブルが存在していることになります。
このクエリはdatabaseを指定していないため、両方のデータベースから情報を返すようになってしまいました。
Cloudflare のボット管理システムは、特徴量(機械学習で使うデータ項目)を ClickHouse から取得していました。システムは最大 200 個の特徴量を想定して
メモリを確保していましたが、クエリ結果が倍増したことで想定を超え、プログラムがクラッシュしました。
検証
実際に ClickHouse で障害を再現してみます。
環境
- ClickHouse 25.10.3.100(Homebrew)
- macOS
ClickHouse の構築手順は 12/1 のアドベントカレンダーの内容を参考にしてみてください。
手順
1. ClickHouse サーバーを起動
clickhouse server
別のターミナルでクライアントに接続します。
clickhouse client
2. テスト用のデータベースとテーブルを作成
シャードに相当するr0データベースと、ユーザー向けのdefaultデータベースにテーブルを作成します。
-- r0データベースを作成(シャードに相当)
CREATE DATABASE IF NOT EXISTS r0;
-- r0に実テーブルを作成
CREATE TABLE r0.http_requests_features (
id UInt64,
feature_name String,
feature_type String
) ENGINE = MergeTree()
ORDER BY id;
-- テストデータを挿入
INSERT INTO r0.http_requests_features VALUES
(1, 'bot_score', 'Float32'),
(2, 'request_rate', 'Float32'),
(3, 'user_agent_hash', 'UInt64');
-- defaultデータベースに同じスキーマのテーブルを作成
CREATE TABLE default.http_requests_features AS r0.http_requests_features;
3. 障害を再現(database を指定しないクエリ)
SELECT database, table, name, type
FROM system.columns
WHERE table = 'http_requests_features'
ORDER BY name;
結果
┌─database─┬─table──────────────────┬─name─────────┬─type───┐
│ default │ http_requests_features │ feature_name │ String │
│ r0 │ http_requests_features │ feature_name │ String │
│ default │ http_requests_features │ feature_type │ String │
│ r0 │ http_requests_features │ feature_type │ String │
│ default │ http_requests_features │ id │ UInt64 │
│ r0 │ http_requests_features │ id │ UInt64 │
└──────────┴────────────────────────┴──────────────┴────────┘
6 rows in set.
3 カラム × 2 データベース = 6 行 が返ってきました。本来 3 行であるべきところが倍増しています。
4. 修正方法を確認(database を指定するクエリ)
SELECT database, table, name, type
FROM system.columns
WHERE table = 'http_requests_features'
AND database = 'default'
ORDER BY name;
結果
┌─database─┬─table──────────────────┬─name─────────┬─type───┐
│ default │ http_requests_features │ feature_name │ String │
│ default │ http_requests_features │ feature_type │ String │
│ default │ http_requests_features │ id │ UInt64 │
└──────────┴────────────────────────┴──────────────┴────────┘
3 rows in set.
database = 'default'を追加することで、正しく 3 行のみが返されるようになりました。
まとめ
Cloudflare の障害は、ClickHouse の権限変更により、ユーザーがシャードのメタデータも見えるようになったことが原因でした。
system.columnsへのクエリでdatabaseを指定していなかったため、同名テーブルの情報が複数データベースから返され、結果が倍増する状態となっていました。
この状態を防ぐ対策として、メタデータを取得するクエリでは、databaseカラムで絞り込みを行うというのが一旦有効そうかと思います。
-- NG
SELECT name, type FROM system.columns WHERE table = 'xxx';
-- OK
SELECT name, type FROM system.columns WHERE table = 'xxx' AND database = 'default';
