ClickHouse 性能结论判定规则:如何从 CI 噪声中识别真实回归与真实优化
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
本文围绕 ClickHouse 仓库中
.claude/skills/perf-comparison性能对比技能的判定参考文档verdict-rules.md展开,系统讲解 ClickHouse 在评估 PR 性能测试结果时使用的一套证据驱动判定框架:判定工作的职责是判断一个性能结果是否“可采取行动”(actionable),而不是机械复述仪表盘上的某一行数据。读完本文,你将掌握一张完整的证据核对表、七种判定结论的适用条件、跨多次 PR 运行与 master 历史趋势的解读规则,以及如何写出可评审、可追溯的性能结论报告——这套规则同时适用于阅读 CI/仪表盘数据与本地perf.py复现结果两种场景。
判定任务的定位:结论不等于复述仪表盘
在 ClickHouse 的研发流程中,.claude/skills/perf-comparison/SKILL.md定义了一个核心问题:
Is this ClickHouse performance result real, flaky/noisy, already known from history, or worth deeper investigation?
即:这次性能结果是真实变化、偶发噪声、历史上已知波动,还是值得深入调查?判定技能的职责由此明确为判断结果是否 actionable——当一份性能测试表被呈现在你面前,你的任务不是重新声明“这一行比那一行慢/快”,而是综合 CI 差分、多次运行、置信度、master 历史、覆盖率与火焰图等多路证据,给出一个有依据的判定。
因此 verdict-rules 文档首先强调一个纪律:没有任何单条证据可以单独定案。原文档(.claude/skills/perf-comparison/references/verdict-rules.md)与 SKILL 都指出两类典型错误:
- 仅凭一个 changed row 就判定回归为真实(未核对重复运行与历史趋势);
- 没有任何历史/趋势证据就武断地把回归当成 flaky。
下文的所有规则,都是为了让结论有据可查、可复现、可被同行评审。
证据核对表(Evidence checklist)
verdict-rules 给出了一张面向每个可疑变化(suspicious change)的八行证据核对表。判定者应尽量为每一行填上“强信号”或“弱/噪声信号”,而不是只挑对自己结论有利的一列:
| Evidence | Question | Strong signal | Weak/noisy signal |
|---|---|---|---|
| Changed metric | 原始差分是否超过阈值? | 差异大,方向一致 | 仅勉强超过阈值 |
| Repeated PR runs | 该变化是否在多天/多次运行中复现? | 同一 query/metric/arch 反复出现 | 只出现一次/消失/方向翻转 |
| Confidence | 置信度保持还是下调? | tier/reason 支持原始变化;命名的检查项解释了原因 | tier 为 noise/suspect;自适应阈值或原始样本将证据降级 |
| Trend/history | 所选运行是否落在正常散布范围之外? | 超出 p95/近期区间,历史稳定 | 落在近期 p95 以内,存在频繁尖峰/突变点 |
| Related metrics | CPU/real/client/memory 是否彼此一致? | 各项指标方向一致 | 指标互相矛盾 |
| Coverage/PR diff | PR 是否触达了被测试执行到的代码? | 改动的文件与覆盖文件相交 | 无重叠或文件不相关 |
| Flamegraph | 栈增量能否解释指标变化? | 热点与 PR/查询吻合 | 热点缺失/无关/混杂 |
| Local run | 受控的本地对照能否复现? | 可重复,p 值显著 | 一次性结果或受工具链偏差影响 |
值得注意的是原文档的措辞:表格既问了“是否有强证据”,也明确列出“弱/噪声信号”是什么样——例如“barely above threshold”(仅勉强超阈值)、“appears once / disappears / flips”(出现一次/消失/翻转)等。填表过程中若多数行落在弱信号列,判定自然倾向噪声或需重跑;若多行落在强信号列,才足以支撑真实回归。
七种判定结论(Verdict labels)
real regression(真实回归)
当大部分下列条件成立时使用:
- CI 差分高于阈值;
- 同一 query/metric/arch 在多次 PR 运行中重复出现,或置信度数据支持;
- 历史数据中并不常见同样的随机波动;
- 相关指标(CPU/real/client/memory)方向一致;
- PR/覆盖率关联合理,或火焰图指向被改变的行为。
verdict-rules 特别强调:不必要求每一项都满足;对缺失的证据要明确说明为什么缺失、为何不影响判定。比如没有火焰图数据时,可写“flamegraph not available”,而不是装作它支持了结论。
likely noise(疑似噪声)
当出现以下情形时使用:
- 变化的行只在一次 PR 运行中出现、后续运行中消失;
- 所选数值落在正常趋势/历史散布区间内(如近期 p95 以内);
- 置信度模块下调了原始证据的等级;
- 相关指标与头条指标互相矛盾;
- PR 与测试之间没有合理解释,也没有 profile 证据支持。
一个关键纪律:即便如此,仍然要把证明它是噪声的数据展示出来(原文档原文 "Still show the data that proves this"),而不是一句“可能是噪声”带过。类似地,SKILL 也强调"dismissal needs evidence"——降噪同样要给出历史或重复运行数据。
needs rerun(需要重跑)
当以下情形出现时使用:
- 原始差分很大,但置信度/历史/重复运行结果彼此矛盾;
- 只有一次运行存在;
- 结果接近阈值(near threshold);
- 环境或基础设施可能扭曲了测量(机器争抢、构建差异等);
- 本地结果与 CI 结果不一致。
推荐动作(Recommended action)是:重跑性能 CI,或做一次聚焦的本地对照实验。从判定语义看,needs rerun 是“证据冲突但值得再确认”,而不是放弃。
unstable test(测试本身不稳定)
当以下情形出现时使用:
- 趋势/历史显示频繁出现同等量级的尖峰;
- 同一查询在不同运行中时而变慢、时而变快(direction flips);
- 仪表盘将该指标标记为 unstable;
- 自对照或重复运行本身就噪声很大。
verdict-rules 特意写了一条使用约束:不要仅仅因为结果不合意(inconvenient)就使用此标签。unstable 必须由可展示的运行间波动数据支撑——比如后文 Report wording 一节给出的跨运行差分序列例子。
real improvement(真实优化)
判定门槛与real regression对称:
- 加速幅度高于阈值;
- 重复运行/置信度/历史数据支持;
- 它并不是随机不稳定的简单反向翻转(即不是噪声的偶然回落)。
同时ci-api.md提醒一个细节:当前置信度模块的 tier 名称/reason 可能是按变慢(slowdown)方向措辞的,不要把速度提升行打印成confirmed_regression;只有当周边证据支持时,才把加速行称为 stable speedup/change。
local-only evidence(仅有本地证据)
当本地perf.py显示出有意义的结果,但 CI/仪表盘证据缺失或尚未核对时使用。它的语义是:本地结果在当前阶段只是本地证据,直到与 CI/history 对照后才能升级判定。
not enough evidence(证据不足)
当缺少关键的“身份信息”(identity)时使用,例如:
- run 未知(unknown run);
- query index 未知;
- 指标/架构选错(wrong metric/arch);
- 历史不可得,且没有重复运行或本地证据。
这一类是最低信息条件下的兜底结论:不要硬凑一个方向性结论。
重复 PR 运行规则(Repeated PR run rules)
检查某个 PR 时,应列出该 PR 的全部 performance.ci 运行,然后在不同运行间比较同一个 test/query/metric/arch:
curl -fsS "$BASE/runs?q=$PR" | jq .其中$BASE为 performance.ci 的标准 API 前缀(具体端点形态可参见 .claude/skills/perf-comparison/references/ci-api.md),?q=$PR返回该 PR 的所有运行,注意搜索可能带回超集,务必按identity.prNumber == PR过滤。
原文档给出的解读规则非常明确:
- N/N 次重复出现且 diff 相似→ 强信号(strong);
- 仅 1/N 出现一次→ 弱信号(weak),很可能需要重跑或属噪声,除非 diff 极大且 profile 能解释;
- 方向反复翻转→ 测试不稳定(unstable);
- 只有旧运行有该变化、最新运行干净→ 很可能已解决或属噪声;
- 最新运行在早期干净运行之后新出现了该变化→ 需要调查:可能是 PR 新改动引入,也可能是 master 基线移动导致。
这些规则直接对应到 helper 脚本,可对每个 PR 做跨运行对比:
# 列出某 PR 的所有 performance.ci 运行 python3 .claude/skills/perf-comparison/scripts/perf_api.py runs --pr "$PR" # 纵向对比某 test/query/metric/arch 在该 PR 全部运行中的表现(oldest→newest diff 图表) python3 .claude/skills/perf-comparison/scripts/perf_api.py pr-query-history \ --pr "$PR" --test aggregation_in_order_2 --query-index 4 \ --metric client_time --arch armmaster / history 趋势规则
判定噪声与真实效果的关键差异在于:把所选运行放到 master 长期历史里看,而不是凭感觉。verdict-rules 要求直接使用仪表盘的 trend/history 数据。
支持 flakiness/noise 的证据:
- 所选点落在近期正常区间内,例如低于/接近 p95;
- 没有该 PR 时也会出现同等量级尖峰;
- 置信度自适应阈值(adaptive threshold)大于原始效应;
changePoints显示 master 在 PR 运行前后存在基线突变;periodComparisons显示历史上频繁出现类似的波动。
支持“新 PR 效应”的证据:
- 所选点以有意义的倍数超出近期 p95;
- 历史中不存在可比的尖峰;
- 多次 PR 运行呈现同样的变动;
- PR 确实改动了被覆盖且相关的代码。
历史描述语句的“允许/禁止”清单
verdict-rules 对报告措辞立了硬性格式规范:
✅ 必须使用的历史描述格式(required wording),形如:
History center trend: 976 points over 2026-04-24 → 2026-06-15; all p05/p50/p95 = ..., recent 30 points over 22.7h p05/p50/p95 = ...; candidate is 3.06x p95.
即:必须说明点的数量与时间跨度、给出历史总体分位数(p05/p50/p95)与近期窗口(如最近 30 个点及其时间跨度)的分位数,并把候选运行与 p95 做倍数比较。
❌ 禁止使用的历史描述(forbidden wording):
min=..., median-ish=..., max=..., last=... .
只报 min/max/median/last 无法反映散布形态,无法支撑噪声与真实变化的区分。这条禁止项与ci-api.md的“required summary shape”互相印证:要报周期、全量历史 p05/p25/p50/p75/p95、近期窗口 p05/p50/p95,并将选中点的 old/new 值与 p50、p95 比较。
覆盖率 / PR 关联规则
Coverage 接口回答的问题是:这个测试是否执行了 PR 所改动的代码?它不能回答:PR 是否导致了变慢?——原文档用两句话划清了这条因果边界。
因此 Coverage 只用于“表述可能性”(phrase plausibility):
intersectingFiles > 0:PR 改动与测试覆盖相交 → 关联合理,应进一步查看改动 hunk;- PR 只改了文件、但与测试覆盖无重叠 → 关联不那么直接;
- 只有测试覆盖文件、PR 并未改动它们 → 可能是间接行为变化;
- 覆盖率数据不可用 → 明确写 unavailable,退而求其次用 PR diff 与查询文本人工比对。
对应的抓取方式(fileSet=intersecting返回 PR 改动与测试覆盖相交的文件集,可取files[].path、files[].patch、coverageRanges等字段):
curl -fsS "$BASE/runs/$RUN_ID/tests/$TEST/coverage?fileSet=intersecting" | jq .一个已观测到的 API 陷阱(记录在 .claude/skills/perf-comparison/references/ci-api.md):当相交文件数为 0 时,响应可能省略totals.intersectingFiles,需用len(files)作为 0 的兜底,不要误判为“数据缺失”。
火焰图(Flamegraph)规则
火焰图只能用于精确匹配的证据组合:run id、test、query index、metric、arch、trace type 全部一致,跨维使用没有意义。
解读方向:
- CPU diff 栈在候选侧增长 → 计算热点(compute hotspot)候选;
- Real 时间增长但 CPU 不增长 → 更可能是等待/I/O/锁/调度(wait/lock/scheduler);
- Memory trace 增长 → 更可能是内存分配压力(allocation pressure)。
最重要的边界是:如果栈增量无法与 query/PR 改动产生关联,就不要声称找到了根因(Do not claim root cause if stack delta does not connect to query/PR changes)。
报告措辞同样有硬性要求(required wording):
- 首先给出 UI 查询页链接(格式形如
/runs/<runId>/tests/<test>/queries/<queryIndex>),因为该页面内含 Flamegraphs 卡片; - 原始 API 链接只作为机器可读的辅助证据(backup);
- 列出 top sample deltas;
- 明确说明哪些栈帧是缺失的(absent)。
❌ 禁止的模糊措辞(forbidden wording):
flamegraph diff exists and contains relevant samples.
原文档评价这种说法 "too vague to review"(过于含糊,无法评审)。SKILL 的推荐写法是给出一张“叶函数/子系统 | baseline samples | candidate samples | delta | interpretation”的表格。
另外ci-api.md记录:火焰图端点可能返回 404 或空帧——缺失火焰图数据是“证据缺席”,不代表噪声,报告里应写no frames returned/not available,不能反过来当噪声证据用。
本地 perf.py 规则
本地复现只有在满足严格条件时才“有意义”(meaningful):
- 足够的运行次数;
- p 值 ≤ 0.05;
- diff 超过测试阈值;
- 结果重要时能重复稳定;
- 数据集完全一致、服务器设置可比(identical datasets and comparable server settings)。
本地结果在被 CI/history 核对之前,只能按 local-only 上报("Local-only result should be reported as local-only until checked against CI/history")。这与判定标签local-only evidence呼应。
本地对照的标准姿势(详见 .claude/skills/perf-comparison/references/local-perf.md)是对旧/新两个二进制各起一个隔离 server,再用仓库自带的tests/performance/scripts/perf.py跑同一份性能测试 XML:
mkdir -p tmp tests/performance/scripts/perf.py \ --host 127.0.0.1 127.0.0.1 \ --port 9000 9001 \ --runs 7 \ tests/performance/aggregation_in_order_2.xml | tee tmp/aggregation_in_order_2.perf.tsv然后用本技能的解析器把输出转成可读报告:
python3 .claude/skills/perf-comparison/scripts/parse_perf_py.py tmp/aggregation_in_order_2.perf.tsv解析器(.claude/skills/perf-comparison/scripts/parse_perf_py.py)实现了几条与本规则一致的判定逻辑,值得对照阅读源码:
- 阈值取自 perf.py 输出的
report-threshold行; - 判定函数
verdict(diff, pvalue, threshold):abs(diff) < threshold→ within threshold;pvalue > 0.05→ not statistically significant;否则按 diff 正负判 candidate slower / candidate faster; - diff 行为
(new_median - old_median) / old_median:正为变慢、负为变快; - 无
diff行时提示 "either only one server was used, too few samples were collected, or no query passed perf.py's diff/p-value gate"。
也就是说,“超过阈值 + p≤0.05 + 足够运行数 + 可重复”这些门槛都被写进了工具层,而非仅停留在文档约定。
报告措辞示例:好写法与坏写法
verdict-rules 用成对的正反例说明了什么叫“可评审的报告措辞”。以下引用均来自原文档,逐条保留了格式要求。
好写法(Good)——结论由证据链导出,而非表格复述:
The latest PR run shows
aggregation_in_order_2/4client_time/armat+23.2%, above a21.7%threshold. The same query appears in 3/4 PR runs, history does not show comparable spikes, and coverage intersectssrc/AggregateFunctions/...; verdict: real regression, medium confidence.
这条例文完整串联了证据核对表的多个维度:原始 diff 与阈值、跨运行复现次数、历史无同类尖峰、覆盖率与src/AggregateFunctions/...相交——最后才落到 verdict 与 confidence 等级。
坏写法(Bad)——直接把表格行为当结论:
This is a regression because the table says slower.
坏写法(Bad)——输出仪表盘内部代码而不解释(SKILL 的第 4 条核心原则也禁止这一点):
Confidence:
M1:downgrade, M2:neutral, M3:downgrade.
好写法(Good)——把置信度翻译成 tier + reason + 具名检查项:
Confidence tier is
noise: History Adaptive Threshold downgraded because observed 47.2% is below the 94.8% threshold from recent master noise; Raw Sample Evidence also downgraded.
坏写法(Bad)——无数据支撑的模糊断言:
Cross-run behavior is unstable.
好写法(Good)——用完整跨运行序列证明不稳定:
Cross-run chart oldest→newest: -15.3%, +19.8%, +6.6%, -10.7%, +15.3%, -15.6%, -3.1%, +22.5%; direction flips repeatedly, so verdict: unstable.
好写法(Good)——用更新的重跑结果取代旧数据,并说明取代理由:
The latest rerun no longer reproduces the earlier
+24%row: the same test/query/metric/arch is now+1.2%, below threshold. The older row is superseded, so verdict: likely noise or resolved.
坏写法(Bad)——在更新的干净重跑之后仍展示旧的+24%表格而不加任何解释。
这组正反例背后是一条贯穿始终的原则:报告必须区分“当前被接受的有效证据”与“已被取代的探索性旧数据”,新旧矛盾时要亮出更可信的一方并说明为什么。
判定规则在仓库中的工程支撑
这套判定规则并非孤立的文档,而是 ClickHouse 仓库中一整套“性能对比技能”的组成部分,各文件职责如下:
| 仓库文件 | 职责 |
|---|---|
| .claude/skills/perf-comparison/SKILL.md | 技能入口:总体原则、数据源优先级、证据阶梯(11 步)、报告模板、常见错误清单 |
| .claude/skills/perf-comparison/references/verdict-rules.md | 本文主体:判定标签与各维度证据解读规则 |
| .claude/skills/perf-comparison/references/ci-api.md | performance.ci API 的端点、字段与实测陷阱(confidence、trend、history、coverage、flamegraph) |
| .claude/skills/perf-comparison/references/local-perf.md | 本地 perf.py、server 启动、数据集与二进制来源约定 |
| .claude/skills/perf-comparison/scripts/perf_api.py | 汇总 CI/API 数据的 helper(runs、pr-inventory、query、pr-query-history、master-checks 等) |
| .claude/skills/perf-comparison/scripts/parse_perf_py.py | 解析本地 perf.py TSV 输出并给出 verdict |
| .claude/skills/perf-comparison/scripts/test_perf_api.py | 对分类逻辑的测试,复刻了ci/jobs/scripts/perf/compare.sh的 changed/unstable 判定 |
| tests/performance/aggregation_in_order_2.xml | 例文中的具体性能测试(含 substitution、create/fill/query 结构) |
| tests/performance/scripts/perf.py | 本地性能测试运行器,输出report-threshold/diff ... pvalue供判定 |
| tests/performance/scripts/compare_perf_tsv.py | TSV 级比较工具 |
例文中的aggregation_in_order_2是仓库里真实存在的性能测试:该 XML 将optimize_aggregation_in_order等设置写入<settings>,用<substitution>把uniqs取 100/10000/1000000 三种规模,对一张 30 分区的 MergeTree 表执行sum/groupArray/uniqExact三种分组聚合查询(tests/performance/aggregation_in_order_2.xml)。因此aggregation_in_order_2/4这样的标识、以及src/AggregateFunctions/...的覆盖率关联,都能在真实数据中落地验证。
值得说明的数据分类边界(记录在 .claude/skills/perf-comparison/scripts/test_perf_api.py 中):is_changed = abs(diff) > changed_threshold and abs(diff) >= stat_threshold,is_unstable = (not is_changed) and stat_threshold > unstable_threshold,方向direction = slowdown if diff > 0 else speedup。这个逻辑告诉我们:“变化行”与“不稳定行”在分类层面就是互斥的两类——这正解释了判定规则为何反复强调不要用 unstable/high-noise 行去混充已变更行,也印证了 verdict-rules 中“unstable test 不得因结果不合意而滥用”的约束。
一份合格结论报告的最小结构
把以上规则汇总成可执行的最小流程(与 SKILL 的 Required report format 一致),一份合格报告应包含:
- Verdict 头:结论标签(七个之一)+ 置信度等级(high/medium/low);
- 已核对证据清单:run/API URL、test/query/metric/arch、old/new/diff/threshold、重复 PR 运行结果、置信度 tier/reason、历史趋势(含分位数)、覆盖率/PR 关联、火焰图(UI 查询页链接 + top sample deltas,或明确 not available)、本地 perf.py(若有);
- Changed metrics 表:清晰区分信号行与噪声/不确定行,不让 unstable 行混入变更表;
- 结论理由:说明为何当前有效证据支持该结论,旧数据如何被取代;
- 推荐下一步动作:no action / rerun performance CI / run local perf.py / inspect flamegraph hotspot / inspect PR code around covered files / bisect or targeted local validation。
这套判定框架的本质是“证据加权”:真实回归需要跨运行复现、历史无同类尖峰、相关指标一致、PR 关联合理等条件互相支撑;而噪声、不稳定与证据不足也各有其必须展示的“反证数据”。无论数据来自 CI 仪表盘还是本地perf.py,规则都要求把结论写成人可复核、机器可追溯的语句——这正是 ClickHouse 长期演进过程中控制性能回归工程实践的核心方法论。
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考