ClickHouse 采样式查询分析器实战:基于 system.trace_log 的 CPU 热点定位与剖析方法论
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
本篇技术文章围绕 ClickHouse 仓库中.claude/skills/cpu-profile/SKILL.md定义的一套 CPU Profile 分析技能展开:它说明了如何利用 ClickHouse 内置的采样式查询分析器(sampling query profiler)与system.trace_log系统表,对单条查询的 CPU 耗时进行端到端定位——从以可控采样周期执行查询、按 query_id 收集调用栈、聚合 Top 热点函数与完整调用路径,到导出 collapsed stack 生成火焰图。读完本文,你将掌握完整的 trace 采集与分析 SQL、采样参数(query_profiler_cpu_time_period_ns等)的取值策略,并能结合 ClickHouse 源码(TraceLog、QueryProfiler设置定义)理解采样数据从采集到符号化的实现链路。
技能定位:它解决什么问题
该技能(.claude/skills/cpu-profile/SKILL.md)的目标非常聚焦:找出查询的 CPU 热点,分析时间花在哪里,定位性能瓶颈。它依赖 ClickHouse 两个原生能力:
- 内置采样式查询分析器:通过
query_profiler_cpu_time_period_ns/query_profiler_real_time_period_ns两个 Settings 控制采样周期,在查询执行期间周期性抓取当前线程的调用栈; system.trace_log系统表:将采样到的调用栈(地址数组)、CPU 核号、线程 ID、query_id 等落盘,可像查询普通表一样用 SQL 分析。
仓库中对应的系统表实现见 TraceLog.h,每条 trace 记录(TraceLogElement)包含的关键字段包括event_time_microseconds(微秒级事件时间)、trace_type(CPU / Real / Memory 等类型)、cpu_id、thread_id、query_id,以及std::vector<UInt64> trace——即以数组形式存储的调用栈地址列表。这正对应技能文档中“system.trace_log里的堆栈是地址数组,下标 1 是最内层(叶子)帧”的说明。
第一步:确定剖析对象
技能的输入$ARGUMENTS是可选的,分三种情况处理:
| 输入形态 | 处理方式 |
|---|---|
形如 UUID(如a1b2c3d4-e5f6-...) | 视为已有查询的query_id,直接跳到第 3 步分析已存在的 trace |
| SQL 查询或查询描述 | 进入第 2 步,带剖析设置执行该查询 |
| 为空 | 询问用户:给出 query_id / 执行一条 SQL / 查看最近的慢查询 |
当用户希望先看最近有哪些慢查询可剖析时,技能给出如下 SQL(注意system.query_log的查询需要allow_introspection_functions权限):
SELECT query_id, query_duration_ms, formatReadableSize(memory_usage) AS peak_memory, left(query, 120) AS query_preview FROM system.query_log WHERE type = 'QueryFinish' AND event_date >= today() - 1 AND query_duration_ms > 1000 AND query NOT LIKE '%system.%' ORDER BY query_duration_ms DESC LIMIT 20 SETTINGS allow_introspection_functions = 1该查询按耗时降序列出近两天内执行超过 1 秒的非系统库查询,并附带峰值内存,帮助快速圈定值得剖析的目标。
第二步:以剖析设置执行查询
核心动作是生成一个唯一的 query_id 并带激进采样设置运行查询。技能文档推荐使用 100us 采样周期(约 10,000 次采样/秒):
PROFILE_QID="cpu-profile-$(uuidgen)" clickhouse-client --query_id "$PROFILE_QID" -q " SELECT ... SETTINGS query_profiler_cpu_time_period_ns = 100000, query_profiler_real_time_period_ns = 100000 "要点说明:
- 用
clickhouse-client非交互模式并显式指定--query_id,可避免与并发查询产生竞争(race)——这是技能文档明确要求的原因; - 若只能在交互模式下运行,则需要解析
clickhouse-client在每条查询前打印的Query id: <uuid>行来获取 query_id。
执行完毕后,先验证查询完成并收集元数据:
SELECT query_id, query_duration_ms, formatReadableSize(memory_usage) AS peak_memory FROM system.query_log WHERE type = 'QueryFinish' AND query_id = '{query_id}' SETTINGS allow_introspection_functions = 1然后等待约 2 秒让trace_log异步落盘(trace_log是异步系统表,采样数据经内部队列批量写入),再进入第 3 步。
采样参数与源码中的默认值
这两个 Settings 的完整定义在 Settings.cpp 中:
query_profiler_cpu_time_period_ns:CPU 时钟定时器周期,只统计 CPU 时间;query_profiler_real_time_period_ns:真实时钟定时器周期,统计 wall-clock 时间(含 IO 等待),其 Cloud 默认值为3000000000(3 秒);- 两者的默认值都来自常量
QUERY_PROFILER_DEFAULT_SAMPLE_RATE_NS,在 Defines.h 中定义为1000000000——即默认每秒 1 个采样; - 设为
0可关闭对应定时器。
官方设置文档中的推荐值与技能文档一致:单条查询剖析建议10000000(100 次/秒)量级,集群级剖析用1000000000(每秒 1 次)。技能文档给出的经验值是:短查询用 100,000(100us)做精细剖析,长查询用 1,000,000(1ms)。采样越密,trace_log中写入的样本越多,开销也越大,需要按查询时长权衡。
第三步:采集与分析 trace 数据
技能文档要求并行执行三类分析(Top 函数、Top 调用栈、火焰图导出),下面逐一给出完整 SQL。
分析 A:CPU 样本最多的 Top 函数
trace[1]取的是堆栈数组第 1 个元素,即最内层(叶子)帧——采样命中的那一刻真正在执行的函数,这是热点分析的核心视角:
SELECT count() AS samples, round(100.0 * count() / (SELECT count() FROM system.trace_log WHERE query_id = '{query_id}' AND trace_type = 'CPU'), 2) AS pct, demangle(addressToSymbol(trace[1])) AS function FROM system.trace_log WHERE query_id = '{query_id}' AND trace_type = 'CPU' GROUP BY function ORDER BY samples DESC LIMIT 30 SETTINGS allow_introspection_functions = 1该查询输出每个叶子函数的样本数与占比(pct),回答“CPU 时间花在了哪些函数上”。
分析 B:Top 完整调用栈
只看到叶子函数还不够,需要看完整的调用路径(谁在什么场景下调用了它):
SELECT count() AS samples, arrayStringConcat( arrayMap(x -> demangle(addressToSymbol(x)), trace), '\n ' ) AS stack FROM system.trace_log WHERE query_id = '{query_id}' AND trace_type = 'CPU' GROUP BY trace ORDER BY samples DESC LIMIT 15 SETTINGS allow_introspection_functions = 1按trace(整条地址数组)分组并符号化后,得到采样数最多的 15 条完整调用链,展示从外层调用者到最内层帧的完整上下文。
分析 C:导出 collapsed stacks 供火焰图使用
将每条去重后的堆栈反转(arrayReverse,使最外层函数在前、叶子帧在后),用;连接并追加样本计数,输出符合 flamegraph 工具要求的 collapsed 格式:
SELECT concat( arrayStringConcat( arrayReverse(arrayMap(x -> demangle(addressToSymbol(x)), trace)), ';' ), ' ', toString(count()) ) FROM system.trace_log WHERE query_id = '{query_id}' AND trace_type = 'CPU' GROUP BY trace ORDER BY count() DESC SETTINGS allow_introspection_functions = 1 FORMAT TSVRaw技能文档要求将该结果保存为tmp/cpu_profile_{query_id}.collapsed,供第 5 步生成火焰图。
元数据统计
同时收集整体剖析元数据(总样本数、首末样本时间、剖析持续时长):
SELECT count() AS total_samples, min(event_time_microseconds) AS first_sample, max(event_time_microseconds) AS last_sample, dateDiff('millisecond', min(event_time_microseconds), max(event_time_microseconds)) AS profile_duration_ms FROM system.trace_log WHERE query_id = '{query_id}' AND trace_type = 'CPU' SETTINGS allow_introspection_functions = 1其中event_time_microseconds对应 TraceLog.h 中TraceLogElement的Decimal64 event_time_microseconds字段,用它推算剖析窗口可以验证采样是否覆盖了查询执行的完整生命周期。
第四步:综合输出结构化报告
技能文档规定把上述三路分析结果综合为一份结构化报告,包含六个部分:
- Profile 概要:query_id、总样本数、剖析时长、采样率;
- Top 15 CPU 热点函数表:样本数 + 占比;
- Top 5 完整调用栈:按最外层到最内层的可读格式展示调用链;
- 子系统归类:把函数归入以下类别做占比拆分——
- 查询执行(HashJoin、Aggregator、MergeSorter 等);
- 表达式求值(ExpressionActions、内置函数);
- IO(ReadBuffer、WriteBuffer、S3、disk);
- 网络(Exchange、Connection、Protocol);
- 压缩(LZ4、ZSTD 等 codec);
- 内存管理(Arena、Allocator、PODArray);
- 优化器(Cascades、JoinOrder、Statistics);
- 其他;
- 可执行结论:哪些热点不符合预期、哪些地方值得优化;
- collapsed stack 文件位置:供后续生成火焰图。
第五步:可选的深入下钻
报告输出后,技能提供四个继续下钻的选项,并循环直到用户选择结束:
1. 钻入某个函数
按函数名过滤包含该函数的 trace,查看它的所有调用上下文。
2. 对比 CPU 时间 vs 真实时间
对trace_type = 'Real'重复同样的分析。两者相减即可暴露 wall-clock 上多出来的部分——即IO 等待与锁竞争(trace_type = 'CPU'只计 CPU 时间,'Real'计 wall-clock 时间)。
3. 生成火焰图
若环境中有flamegraph.pl:
flamegraph.pl --title "CPU Profile: {query_id}" --countname samples --width 1800 \ tmp/cpu_profile_{query_id}.collapsed > tmp/cpu_flamegraph_{query_id}.svg或者把 collapsed 文件导入 speedscope 这类在线剖析可视化工具。
4. 显示源码位置
用addressToLine把符号映射到源文件:行号,适合在本地源码树中精确定位热点代码:
SELECT count() AS samples, demangle(addressToSymbol(trace[1])) AS function, addressToLine(trace[1]) AS source_location FROM system.trace_log WHERE query_id = '{query_id}' AND trace_type = 'CPU' GROUP BY function, source_location ORDER BY samples DESC LIMIT 30 SETTINGS allow_introspection_functions = 1注意:addressToLine依赖调试信息,需要安装带符号的调试包(技能文档明确指出clickhouse-common-static-dbg必须已安装);本仓库对应的 RPM 打包定义见 clickhouse-common-static-dbg.yaml。
关键约束与源码级注意事项
技能文档 Notes 一节列出的约束,逐条结合仓库实现确认如下:
| 约束 | 说明与仓库依据 |
|---|---|
采样频率由query_profiler_cpu_time_period_ns控制 | 默认 1,000,000,000(每秒 1 样本),见 Defines.h;短查询用 100,000(100us),长查询用 1,000,000(1ms) |
trace_type = 'CPU'vs'Real' | CPU 计数 vs wall-clock(含 IO 等待);对应trace_type = 0关闭定时器 |
allow_introspection_functions = 1 | addressToSymbol、demangle、addressToLine属于 introspection 类函数,必须开启该设置 |
| 符号解析需调试包 | clickhouse-common-static-dbg未安装时,符号化结果会退化为不可读的地址/空符号 |
| 集群/Cloud 环境 | 用FROM clusterAllReplicas(default, system.trace_log)汇总所有节点的 trace |
| 堆栈数组的方向 | 索引 1 是最内层(叶子)帧;导出火焰图时必须arrayReverse使其变根在前 |
从源码结构看,整条数据链路是:查询执行期间QueryProfiler(被 Settings.cpp 中这两个设置驱动)周期性抓取当前线程调用栈 → 通过内部 trace 发送队列异步写入 TraceLog 系统表 → 落盘的trace_log表中trace列为 UInt64 地址数组(见 TraceLog.h 的std::vector<UInt64> trace)→ 查询时用addressToSymbol等函数在线符号化。这也解释了第 2 步中“等待 2 秒再查询”的必要性:trace 写入是异步的。
官方文档中与该流程对应的页面为 采样式查询分析器指南 与 trace_log 系统表参考,可与本文的 SQL 对照使用。
技能的使用示例
技能文档给出的三种调用形态(对应/cpu-profile斜杠命令):
/cpu-profile交互式:由用户选择要剖析的查询。
/cpu-profile a1b2c3d4-e5f6-7890-abcd-ef1234567890分析已有 query_id 的 trace(跳过执行步骤,直接从第 3 步开始)。
/cpu-profile SELECT count() FROM lineitem WHERE l_shipdate > '1995-01-01'立即执行并剖析一条 SQL(以 TPC-H 的 lineitem 表 count 查询为例)。
小结
这套 CPU Profile 技能把 ClickHouse 原生剖析能力组织成了一条可复制的流水线:带--query_id以 100us 采样执行查询 → 等trace_log落盘 → 三路并行 SQL(Top 函数 / Top 调用栈 / collapsed 导出)→ 结构化报告(含子系统归类)→ 按需下钻(函数过滤、CPU vs Real 对比、火焰图、源码行号)。所有步骤只依赖system.query_log与system.trace_log两张系统表及allow_introspection_functions权限,无需 perf 等外部工具;而采样周期的取值、符号化前提(dbg 包)、堆栈方向等细节,在 Settings.cpp、Defines.h 与 TraceLog.h 中都有明确的源码依据,可按当前仓库实际内容核对。
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考