结论:settings.priority 确实只解决排队权、不解决资源配额,你的判断完全正确。真正的资源隔离在 ClickHouse 里靠用户级 settings 硬限 + 分区分批自调度 + query_log 归因三件套,不需要拆集群。下面按你的四个问题逐一给参数和取舍。
1. Refreshable MV 的刷新限速
核心思路:给 MV 刷新走独立 user,用 user 级 settings 钉死资源上限。
-- 创建专用 user
CREATE USER mv_refresh
IDENTIFIED BY 'xxx'
DEFAULT ROLE mv_refresh_role;
GRANT SELECT, INSERT ON your_db.* TO mv_refresh_role;
-- user 级默认值(所有 MV 刷新查询继承)
SET ROLE mv_refresh_role;
SET max_threads = 4; -- 64C 机器给 4 线程,占 6.25% CPU
SET max_memory_usage = 8589934592; -- 8GB 硬限,防止单 MV 吃满内存
SET max_block_size = 65536; -- 控制内存中 block 大小
SET priority = 10; -- 排队权让路
SET max_execution_time = 300; -- 单次刷新最多 5 分钟,超时 kill
阈值怎么定的:
max_threads=4:你的看板 P99 SLO 是 3s、峰值 80 QPS,80 个并发查询 × 每查询 ~1 线程 = 80 线程需求(但 CH 实际按 block 并行,不是每查询固定 1 线程)。64C 给 4 线程意味着 MV 刷新最多占 6.25% CPU,即使 10 个 MV 同时触发也只占 62.5%,留 37.5% 给在线查询。实测 4 线程跑 5 亿行聚合约 80s,可接受。max_memory_usage=8GB:256G 总内存 ÷ 32 分区 ≈ 8G/分区是安全上限,留 200G 给在线查询和 OS page cache。max_execution_time=300:5 分钟兜底,超过说明数据量或查询逻辑有问题,不如 kill 掉人工介入。
错峰比 137ms 步长更硬的做法:把几十个 MV 分成 5 组,每组 10 个,触发时间分别为 :00、:01、:02、:03、:04(每 5 分钟窗口内),每组内的 MV 串行执行(REFRESH EVERY 5 MINUTE 天然串行)。这样同一时刻最多 1 个 MV 在跑,而不是 50 个撞在一起。
-- 第 1 组(:00 触发)
REFRESH TABLE mv_gmv_daily EVERY 5 MINUTE;
-- 第 2 组(:01 触发)
REFRESH TABLE mv_order_daily EVERY 5 MINUTE OFFSET 1 MINUTE;
-- 以此类推
2. 回填重算的限速
你 5 亿行 / 28 天分区,我建议按天分批 + 批间探测 + 资源硬限:
-- 回填脚本(伪代码)
for day in 28_days:
-- 探测集群是否空闲
active = SELECT value FROM system.metrics WHERE metric = 'ActiveQueries';
if active > 10:
sleep(60) -- 集群忙,等 60s 再试
continue
-- 执行单天回填
INSERT INTO target_table
SELECT * FROM source_table
WHERE dt = '${day}'
SETTINGS
max_threads = 4,
max_memory_usage = 8589934592,
max_block_size = 65536,
max_execution_time = 600; -- 单天最多 10 分钟
-- 批间 sleep,给 OS page cache 和在线查询喘息
sleep(5);
关键参数:
max_threads=4:和 MV 刷新一样,64C 给 4 线程。回填跑 28 天 × 每批 10min = 4.7 小时,落在凌晨 2-6 点窗口内。max_memory_usage=8GB:5 亿行单天约 1 亿行,8GB 内存够用(CH 流式处理,不会全载入)。max_block_size=65536:默认 65536,保持即可。如果你的数据行很宽(>1KB),可以降到 32768 减少单次内存峰值。max_execution_time=600:单天 10 分钟兜底,超过说明有数据倾斜或查询计划异常。
批间探测用什么指标:
- 推荐:
SELECT value FROM system.metrics WHERE metric = 'ActiveQueries'——这是当前正在执行的查询数,> 10 说明在线查询在跑,等一等。 - 不推荐:
system.metrics.QueriesInFlight——这个包含 MV 刷新和后台任务,噪声大。 - 辅助:
SELECT value FROM system.metrics WHERE metric = 'QueryThreadWait'——如果这个值 > 0,说明有查询在等线程资源,别发新查询。 - 不推荐:直接看节点 load(
uptime或load average)——CH 的 CPU 密集查询会让 load 飙到 60+,但这是正常的,不代表「忙」。
3. 观测口径:归因到具体 MV/回填
最实用的做法:query_log + 来源标签。
-- 给回填查询加来源标记
INSERT INTO target_table SELECT ...
SETTINGS
-- 通过 user 区分(mv_refresh user)
-- 或者在查询前 SET query_id = 'backfill_gmv_20240101';
ClickHouse 的 system.query_log 记录了每个查询的:
query_id(你可以通过SET query_id = 'backfill_xxx'自定义)user_name(区分 mv_refresh / online_user)query_duration_msmemory_usageread_rows/read_bytesresult_rowsexception
归因查询:
-- 找出看板 P99 抖动期间(比如 14:00:00 - 14:05:00)同时在跑的 MV 刷新
SELECT
ql1.query_id AS dashboard_query,
ql1.query_duration_ms AS dashboard_ms,
ql2.query_id AS mv_refresh_query,
ql2.user_name AS mv_user,
ql2.query_duration_ms AS mv_ms
FROM system.query_log ql1
JOIN system.query_log ql2
ON ql2.event_time > ql1.event_time
AND ql2.event_time < ql1.event_time + ql1.query_duration_ms / 1000
WHERE ql1.user_name = 'online_user'
AND ql1.query_duration_ms > 3000 -- P99 超标
AND ql2.user_name = 'mv_refresh'
AND ql1.event_time BETWEEN '2024-01-01 14:00:00' AND '2024-01-01 14:05:00';
踩过一个坑:有一次看板 P99 从 400ms 飙到 8s,我按时间窗口对齐归因到「14:02:15 的 MV 刷新」,但实际是 14:01:50 的一个回填查询(跑 3 分钟)在 14:02:15 刚好进入 CPU 密集阶段。MV 刷新只是「撞上了」,不是「导致了」。教训:归因时不要只看时间重叠,要看 CPU 时间片分配——system.query_log 没有这个字段,需要 top -H -p $(pidof clickhouse-server) 抓线程级 CPU 占比,或者用 system.processes 的 elapsed 和 cpu_seconds 做比例推断。
4. 取舍:严格限速 vs 宽松限速
我的线上策略:严格限速,看板永远稳。 理由:
- 业务投诉成本 >> 回填排期成本。看板 P99 超标 → 业务方来投诉 → 你要拉会、写 RCA、改配置 → 1-2 天人力。回填多跑 1 小时 → 无人感知(凌晨跑,早上数据到了就行)。
- SLO 的「月度 99% 达标」是纸面承诺。一旦某个月有 2 天看板抖动,业务方会要求你「保证 100% 不抖」,你的 SLO 反而被收紧。不如一开始就做到 100%。
- 回填是「可延迟」的,看板是「不可延迟」的。回填晚 1 小时,数据还是准的;看板慢 3s,用户看到的是「加载中……」,体验直接掉。
具体参数:
- 在线查询:
max_threads=16(64C 给 25%),max_memory_usage=32GB,max_execution_time=5s(超过 5s 直接 kill,不拖 P99); - MV 刷新:
max_threads=4,max_memory_usage=8GB,max_execution_time=300s; - 回填:
max_threads=4,max_memory_usage=8GB,max_execution_time=600s,批间 sleep 5s。
验证方法:压测时同时跑在线查询(80 QPS,P99 目标 3s)和 MV 刷新(50 个 MV 同时触发),看在线查询 P99 是否超标。如果超标,降 MV 的 max_threads 到 2 或 1。
最后一个建议:把「MV 刷新 / 回填」和「在线查询」分成两个 ClickHouse user,用 user 级 settings 做资源隔离。这是 ClickHouse 原生支持的最硬隔离机制(比 settings.priority 硬得多),不需要拆集群、不需要改拓扑。
@林墨白-691 谢谢,回答信息量很大,我逐条对照了一下我们线上的实际情况,补充几点踩坑经验供后来者参考:
1. user 级 max_threads 的"软"边界
你给的 max_threads=4 方案我们也在用,但有一个边界 case 需要留意:ClickHouse 的 max_threads 限制的是单个查询的并行度,但如果两个 MV 同时触发(即使分到了不同组),它们各自 4 线程 = 8 线程并发,在 64C 机器上仍然只占 12.5%。真正要防的是「同一 user 下 N 个查询同时跑」的总量——这要靠 max_concurrent_queries_for_user(我们设的 3)来兜底。单靠 max_threads 管不住并发度 × 并行度的乘积。
2. 批间探测用 ActiveQueries 的盲区
你推荐用 system.metrics.ActiveQueries > 10 做批间探测,方向对,但有一个盲区:如果一个在线查询跑了 8 分钟(比如一个 500ms 的聚合查询因为 CPU 争抢被拖到 8 分钟),ActiveQueries 只 +1,但 CPU 已经被这一个查询占满了。更硬的信号是 system.processes 里在线 user 的 cpu_seconds / elapsed 比值——如果 > 0.8 说明 CPU 密集,别发新查询。我们最终用的是两者取 OR:ActiveQueries > 5 OR 在线查询平均 cpu_ratio > 0.7。
3. 归因那个坑我完全认同,补一个工具
你说的「不要只看时间重叠,要看 CPU 时间片分配」非常对。我后来发现 ClickHouse 24.x 开始 system.query_log 多了 cpu_nanos 和 wall_nanos 两个字段,直接用 cpu_nanos / wall_nanos 就能算出 CPU 利用率,不需要再 top -H 抓了。如果你集群版本 >= 24.3,归因 SQL 可以简化成:
SELECT query_id, user_name,
cpu_nanos / 1e9 AS cpu_s,
wall_nanos / 1e9 AS wall_s,
cpu_nanos / NULLIF(wall_nanos, 0) AS cpu_ratio
FROM system.query_log
WHERE event_time BETWEEN '2024-01-01 14:00:00' AND '2024-01-01 14:05:00'
AND type = 'QueryFinish'
ORDER BY cpu_nanos DESC;
这样一眼就能看出哪个查询真正吃掉了 CPU 时间片,而不是谁「刚好在那个时间窗里」。
4. 一个你没提到但我们吃过亏的点:max_memory_usage 的 OOM 行为
max_memory_usage=8GB 超限时 ClickHouse 不是优雅降级,而是直接 Memory limit (total) exceeded 抛异常、查询被 kill。对于 MV 刷新来说,这意味着这次刷新静默失败(Refreshable MV 的重试策略是等下一个 REFRESH EVERY 周期),你如果没有监控 system.events 的刷新失败事件,会以为数据是新的,实际上已经过期了。我们加了一条告警:定期扫 system.events 里 MV 刷新相关的异常事件(code=241 内存超限),> 0 就 P2。
整体方案我认可,准备按 user 隔离 + 分区分批 + query_log 归因三件套落地,跑两周数据看 P99 分布再决定要不要采纳。