ClickHouse 分区变更后计费任务卡死:一次隐藏锁竞争的排查与修复

2026-05-14 24 预计阅读时间:1 分钟
来源:blog.cloudflare.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

Petabyte 级 ClickHouse 集群上做了一次看似合理的分区调整,结果核心计费任务集体卡死。监控面板一片绿,错误日志寥寥无几——问题不在数据量,不在磁盘 IO,而是藏在查询规划器里的一把锁。

从"正常"到"停滞"只有一步

故事的开头很典型:团队为了优化查询性能,调整了一张大表的分区键。变更上线后,写入和日常查询看起来一切正常,但每天凌晨跑的计费聚合任务突然从 30 分钟拖到数小时,甚至无法完成。

第一反应是看系统表 system.metricssystem.processes,CPU 和内存占用平稳,磁盘 IO 没有突增。system.query_log 里也没有报错——查询都在"运行中",就是不结束。

这种"无声的卡死"比报错更难定位。报错至少给你方向,卡死只给你沉默。

逐层剥开:从查询卡住到锁竞争

排查的第一步是看哪些查询在等什么。在 ClickHouse 中,可以通过 system.processes 观察当前活跃查询,再结合 system.query_logquery_duration_ms 做趋势对比。

关键发现:计费任务涉及的多条聚合查询几乎同时进入"长时间运行"状态,但 CPU 占用极低。这意味着查询不是在计算,而是在

接下来用 ClickHouse 自带的追踪手段进一步确认:

-- 查看当前所有正在执行的查询及其等待时间
SELECT
    query_id,
    query,
    query_start_time,
    now() - query_start_time AS elapsed_seconds,
    thread_ids,
    peak_memory_usage
FROM system.processes
ORDER BY elapsed_seconds DESC
LIMIT 20;

结果显示多条查询的 elapsed_seconds 持续增长,peak_memory_usage 却几乎为零——典型的"等锁"特征。

进一步,通过 ClickHouse 的 system.trace_log(需事先开启查询追踪)可以看到查询规划阶段的耗时分布:

-- 如果开启了 trace_log,查看规划阶段耗时
SELECT
    trace_type,
    count() AS cnt,
    avg(duration_ns) / 1e6 AS avg_ms
FROM system.trace_log
WHERE query_id IN ('<卡死查询的query_id>')
GROUP BY trace_type
ORDER BY avg_ms DESC;

追踪数据指向查询规划器(QueryPlan 构建阶段)耗时异常。结合分区变更的上下文,根因浮出水面:新的分区键导致查询规划器需要处理远更多的分区裁剪路径,而规划器内部的全局锁在并发场景下被激烈争抢

锁在哪:ClickHouse 查询规划器的瓶颈机制

ClickHouse 的查询规划流程大致是:解析 SQL → 构建逻辑计划 → 优化(分区裁剪、投影选择等)→ 生成物理执行计划。在分区数量激增后,优化阶段的分区裁剪逻辑需要遍历更多分区元数据,这个过程持有一把内部互斥锁。

当数十个计费聚合查询同时进入规划阶段,每个查询都要争抢这把锁来读取和裁剪分区信息。结果是:查询排队进入规划器,规划时间从毫秒级膨胀到秒级甚至分钟级,而执行阶段反而很快——但整体耗时被规划阶段完全拖垮。

这不是数据量问题,不是 IO 问题,是并发规划时的锁竞争问题。单条查询跑没问题,并发一上就卡死。

实操:如何检测和缓解 ClickHouse 规划器锁竞争

以下是一套可落地的检测与缓解方案,适用于任何大规模 ClickHouse 集群。

1. 开启查询追踪,捕获规划阶段耗时

config.xml 中启用追踪:

<!-- /etc/clickhouse-server/config.xml -->
<query_trace_log>
    <database>system</database>
    <table>trace_log</table>
    <flush_interval_milliseconds>7500</flush_interval_milliseconds>
</query_trace_log>

重启后,system.trace_log 会记录查询各阶段的耗时。定期检查规划阶段占比:

-- 统计最近1小时内查询规划阶段耗时占比
SELECT
    query_id,
    sum(duration_ns) / 1e9 AS total_trace_sec,
    sumIf(duration_ns, trace_type = 'QueryPlan') / 1e9 AS plan_sec,
    plan_sec / total_trace_sec AS plan_ratio
FROM system.trace_log
WHERE event_date = today()
  AND event_time >= now() - INTERVAL 1 HOUR
GROUP BY query_id
ORDER BY plan_ratio DESC
LIMIT 10;

如果 plan_ratio 超过 0.5 且 plan_sec 远大于单查询正常值(通常 < 0.1s),就要警惕锁竞争。

2. 监控并发规划查询数

-- 实时查看处于规划阶段的查询数量(通过极低内存+长时间运行特征推断)
SELECT count() AS stuck_in_planning
FROM system.processes
WHERE peak_memory_usage < 1000  -- 几乎未分配内存
  AND now() - query_start_time > 5;  -- 运行超过5秒但内存极低

这个数字如果和你的并发任务数接近,基本可以确认规划器锁是瓶颈。

3. 缓解措施

短期:降低并发度。 计费任务如果用并发聚合加速,改为串行或低并发(2-3 个查询同时跑),锁竞争立即缓解:

# 示例:将原本10并发改为3并发执行计费聚合
# 假设原始脚本用 xargs -P10 并发,改为 -P3
cat billing_queries.sql | xargs -I{} -P3 clickhouse-client --query "{}"

中期:减少分区数量。 评估分区键是否过度细分。ClickHouse 单表分区超过几千个时,元数据遍历成本急剧上升:

-- 检查当前表的分区数量
SELECT count() AS partition_count
FROM system.parts
WHERE database = 'billing_db' AND table = 'billing_events'
  AND active = 1;

如果 partition_count 超过 5000,考虑合并分区键粒度(例如从 toYYYYMMDD 改为 toYYYYMM):

-- 重新定义分区键(示例:从按天改为按月)
ALTER TABLE billing_events
  MODIFY PARTITION BY toYYYYMM(event_time);

注意:MODIFY PARTITION BY 会触发底层数据重组,Petabyte 级表需在低峰期操作并预留充足时间。

长期:补丁修复。 原文团队的做法是向上游 ClickHouse 提交补丁,将规划器中的全局锁改为更细粒度的锁(例如按表或按分区组加锁),从根本上降低争抢范围。如果你有 C++ 能力,可以参考 ClickHouse 源码中 QueryPlanOptimize 相关的互斥锁逻辑,提交或应用社区 patch。

4. 自动化检测脚本

把检测写成定时任务,每天跑一次:

#!/bin/bash
# check_planning_lock.sh — 每小时检测规划器锁竞争迹象
CH_CLIENT="clickhouse-client --host=ch-node1 --user=monitor --password=xxx"

STUCK_COUNT=$($CH_CLIENT --query="
    SELECT count() FROM system.processes
    WHERE peak_memory_usage < 1000 AND now() - query_start_time > 10
")

PARTITION_COUNT=$($CH_CLIENT --query="
    SELECT count() FROM system.parts
    WHERE database = 'billing_db' AND table = 'billing_events' AND active = 1
")

echo "$(date): stuck_in_planning=$STUCK_COUNT, partition_count=$PARTITION_COUNT"

# 告警阈值:超过5个查询疑似卡在规划阶段 或 分区数超过3000
if [ "$STUCK_COUNT" -gt 5 ] || [ "$PARTITION_COUNT" -gt 3000 ]; then
    echo "ALERT: potential planner lock contention" >&2
    # 可接入 PagerDuty / 钉钉 / Slack 告警
fi

经验与权衡

做法 收益 风险/代价
降低计费任务并发度 立即缓解锁竞争,零改动 总耗时可能略增(串行化)
合并分区键粒度 减少分区数,降低规划器遍历成本 查询裁剪精度下降,需评估业务影响
应用细粒度锁补丁 根治并发规划瓶颈 需维护自定义分支,跟进上游版本
开启 trace_log 持续可观测规划阶段耗时 额外存储和微量性能开销

核心教训: ClickHouse 的性能瓶颈不一定在执行阶段。分区变更后,务必关注分区数量对查询规划器的影响,尤其在高并发场景下。标准监控指标(CPU、IO、错误率)对锁竞争几乎无感——你需要专门看"查询在等什么",而不是"查询在算什么"。

下次做分区变更前,跑一遍上面的分区数量检查和并发规划检测脚本,比事后排查省几天时间。


相关推荐