做过几轮ClickHouse性能调优之后,我最大的感受是:这台引擎大部分时候快得不像真实世界,可一旦慢下来,你往往连怪谁都找不到——是分区裁剪没生效,是CPU耗在某个函数的解析上,还是merge线程把IO挤满了?拍脑袋调max_threads或者加内存,经常白忙。要解决这个问题,手里必须有一套能快速定位瓶颈的工具链。这篇文章就把我实际在用的ClickHouse性能调优工具和完整思路梳理一遍,包括自带系统表怎么读、外部监控怎么搭、压测怎么做,以及一个慢查询排查的真实链路。
1. 先建立基线:ClickHouse性能问题的特殊性才决定工具选型
1.1 瓶颈不一样,工具也得换思路
ClickHouse是列式存储加向量化执行,它的性能瓶颈和MySQL、PostgreSQL完全不是一回事。传统数据库调优,翻来覆去就是看执行计划、检查索引命中率、看锁等待;但在ClickHouse里,这些经验经常失效。它的典型瓶颈点集中在CPU流水线、内存带宽、分区裁剪、MergeTree的parts数量、后台合并线程,以及分布式集群里节点协调的延迟上。所以,上手就装一套"通用数据库监控大盘"的人,大概率会被一堆看不懂的指标淹没。
举个具体例子:一条聚合查询变慢了。在MySQL里,第一反应是哪里没走索引;在ClickHouse里,可能是查询读取的列太多,列式读取的总数据量被撑大了;可能是GROUP BY基数极高,哈希表内存暴涨触发内存限制;也可能是查询里用了一大堆字符串函数,向量化执行退化成逐行处理。这几个原因对应完全不同的优化手段,也对应完全不同的观测工具。先想清楚你的查询属于CPU密集型、内存密集型、IO密集型,还是外部协调依赖,再决定看哪些指标,工具链才会有效率。
1.2 调优前先记录哪些基线指标
没有基线,就没有"优化效果"这回事。我习惯在动任何一个参数之前,先花一到两天时间把线上现状完整记录一遍。这个过程不需要多复杂的平台,把ClickHouse自带系统表的数据导出来,配合外部监控保留曲线,就够了。下面这张表是我每次搭建新集群都会先确认的基线清单。
| 维度 | 主要数据来源 | 核心指标 | 异常信号 |
|---|---|---|---|
| 查询性能 | system.query_log | QPS、P50/P99耗时、read_rows、read_bytes、memory_usage | 同样查询耗时明显上升 |
| 分区与合并 | system.parts | active parts数、partition数量、part层级 | parts数破千,合并进度跟不上 |
| CPU | system.metrics / system.events | 查询并发数、CPU时间占比 | CPU持续高水位且查询排队 |
| 内存 | system.metrics | MemoryTracking、单查询峰值内存 | 接近max_server_memory_usage |
| 磁盘IO | system.disks | 读写速率、IO等待 | 磁盘队列堆积 |
| 协调服务 | 外部监控 | 分布式协调服务延迟、请求量 | 插入和分布式DDL阻塞 |
这套基线数据不是记完就扔,后面每次调优都要拿它做对照。比如说,你看到一个查询从200ms变成800ms,如果没有历史曲线,根本不知道是今天才变的,还是一个月前就已经这样了。有了时间线,你才能把"变更导致的退化"和"数据量自然增长导致的退化"区分开。
2. 自带"诊断三件套":system.query_log、trace_log与metric_log实战读法
2.1 query_log:让每一条查询开口说话
很多人不知道,ClickHouse自带一张非常完整的查询日志表,就是system.query_log,而且默认情况下查询开关是打开着的。这张表记录了每一条执行过的查询的元信息,包括查询ID、开始时间、执行耗时、扫描行数、扫描字节数、内存占用、SQL原文,以及执行结果类型。性能调优的第一步,不是上任何外部工具,而是先把这张表读透。
先看最直接的:过去24小时最慢的20条查询。
SELECT query_id, query, query_duration_ms, read_rows, read_bytes, formatReadableSize(read_bytes) AS read_size, formatReadableSize(memory_usage) AS mem FROM system.query_log WHERE event_time >= now() - INTERVAL 1 DAY AND type = 'QueryFinish' ORDER BY query_duration_ms DESC LIMIT 20;这个查询能帮你快速掌握两个信息:哪些查询在拖累集群,以及这些查询的消耗结构。我见过不少slow query名单,和开发那边理解的"罪魁祸首"南辕北辙。比如有一条SQL扫描了50GB数据,但只花了300ms,优化价值不大;真正要关注的是那些扫描行数不高、却耗时几秒的查询,说明问题多半在CPU计算或者内存分配上。
另一个容易被忽略的视角,是高频小查询。单个查询只要20ms,但每秒被业务调用了几百次,积少成多,集群资源就是这么被吃掉的。
SELECT query, count() AS exec_count, sum(query_duration_ms) AS total_ms, avg(read_rows) AS avg_rows FROM system.query_log WHERE event_time >= now() - INTERVAL 1 DAY AND type = 'QueryFinish' GROUP BY query ORDER BY exec_count DESC LIMIT 20;这种方式特别适合排查"应用层轮询导致集群空转"的问题。很多报表系统每隔几秒就拉一次最新数据,单看每条都无害,聚合到一起才发现占了集群三成以上的查询量。这类问题的解法往往不在ClickHouse本身,而在业务侧的缓存策略。
需要提醒一句:system.query_log也是一张真实的MergeTree表,会持续占用磁盘。不同版本默认保留期不太一样,如果发现这个库膨胀得厉害,要到配置层面设置TTL或定期清理,别让诊断工具自己变成新问题。
2.2 trace_log与metric_log:微观热点和历史曲线
query_log能回答"什么慢",但要回答"为什么慢",就得看system.trace_log。这是一份采样剖析数据,服务器会按照一定时间周期对线程堆栈进行采样,把每次采样的结果记录到这张表里。拿到一个慢查询的query_id之后,从trace_log导出对应时间段的采样数据,聚合后生成火焰图,可以直观看到CPU时间都烧在哪些函数上。
开启方式是在config.xml里配置采样周期参数,比如query_profiler_real_time_period_ns和query_profiler_cpu_time_period_ns。我一般会同时打开实时采样和CPU时间采样,这样既能发现运行时间消耗,也能看到纯CPU计算占用的分布。
system.metric_log则承担了另一个作用:把system.metrics、system.events这些瞬间指标按时间序列保存下来。它的价值在于回溯。比如某个晚上内存告警,你想知道那一小时内存是怎么涨上去的,直接看metric_log里的MemoryTracking曲线就行,不用再蹲守下一次复现。
SELECT toStartOfHour(event_time) AS hour, max(value) AS peak_mem FROM system.metric_log WHERE metric = 'MemoryTracking' AND event_time >= now() - INTERVAL 1 DAY GROUP BY hour ORDER BY hour;这三个系统表配合起来,已经能覆盖"事后查因"的大部分场景。很多团队一上来就搭监控平台,反而忽略了这些内置能力,有点舍近求远。
3. 把指标拉到外部看:Prometheus与Grafana的监控闭环搭建
3.1 从Exporter到可视化面板的最小搭建
自带系统表解决的是"事后查因",但性能问题通常需要"事前告警"和"实时曲线"。这时候就需要把指标拉到外部,搭一套监控闭环。以最常见的开源方案为例:Prometheus负责抓取和存储指标,可视化面板负责绘图和告警,中间用一个导出器把ClickHouse内部指标暴露成Prometheus格式。
先建一个只读的监控账号,不要用管理账号去采集。
CREATE USER monitoring IDENTIFIED WITH plaintext_password BY 'strong_password' SETTINGS readonly = 1;然后按版本给账号授予对system库相关表的只读权限。不同版本权限粒度有差异,稳妥的做法是只要SELECT权限,绝对不给ALTER和DROP。
ClickHouse新版本在服务端配置里可以直接暴露Prometheus端点,在config.xml中加上类似配置,然后重启实例。
<prometheus> <endpoint>/metrics</endpoint> <port>9363</port> <metrics>true</metrics> <events>true</events> <asynchronous_metrics>true</asynchronous_metrics> </prometheus>老版本或者想拿更多自定义指标的情况下,可以用社区维护的导出器,它会去查system.metrics、system.events、system.asynchronous_metrics这些表,再按Prometheus格式供抓取。
一个最小可用的抓取配置大概是这样:
scrape_configs: - job_name: clickhouse static_configs: - targets: ['ck1:9363']抓取间隔我建议15秒。太密集没必要,太稀疏又会漏掉短时尖峰。
可视化面板这边,社区有不少现成的ClickHouse仪表盘模板可以导入,但别直接照搬每一个图表。我建议在面板上至少保留这几类视图:查询并发和QPS、P50/P99查询耗时、CPU与内存使用率、磁盘读写速率、active parts数量、slow query数量。后面两类尤其容易被人忽略,却在实战中救过我好几次。
3.2 上线监控后最容易踩的坑
监控系统本身也有坑,我踩过不少,挑几个印象深的说。
第一,权限别给大。曾经见过某个环境的监控账号居然有DROP权限,某次自动化脚本误操作差点把系统表干掉。只读账号就该老老实实只读。
第二,指标名会变化。ClickHouse升级之后,部分metrics名称或含义可能调整,仪表盘的图表很容易突然变空。每次升级完,要留出时间检查监控指标是否还正常,而不是只看业务查询能不能跑。
第三,告警阈值别抄模板。社区模板里的阈值往往来自别人的机器配置和数据规模,直接套用到你的集群上,要么天天误报,要么真出问题时反而不报警。我习惯用"历史P99的移动基线"做慢查询告警,而不是设一个固定毫秒数——因为不同业务的慢查询标准完全不同。
第四,别忘了监控外部依赖。ClickHouse集群经常依赖分布式协调服务做副本同步和分布式DDL,一旦协调服务延迟变高,整个集群的插入和查询都会变慢,但ClickHouse自身的指标不一定有明显的先兆。把协调服务的延迟也纳入监控,很多疑难杂症才有解释。
4. 给实例施加真实压力:压测工具选择与结果解读
4.1 自带压测工具clickhouse-benchmark:最趁手的回归手段
ClickHouse自带一个轻量压测工具,叫clickhouse-benchmark,它非常适合做配置变更前后的回归对比。用法很简单,把一批SQL放进文件,指定并发数和迭代次数,它就会持续打压并输出QPS和延迟分位数。
clickhouse-benchmark \ --host 127.0.0.1 \ --port 9000 \ --user default \ --password xxx \ --queries queries.sql \ --concurrency 16 \ --iterations 1000 \ --delay 1 \ --randomize输出里重点看两个东西:一个是吞吐量QPS,一个是P50、P95、P99延迟。优化前后的对比,不要只看平均值,平均值会被极端值拉偏,P99能更真实反映用户侧体验。
使用这个工具,我有三个习惯。第一,SQL文件要固定,数据量要固定,这样才能保证对比是纯变量对比。第二,正式开始记录数据之前,先跑几十轮预热,让操作系统页缓存和代码路径稳定下来。第三,每次只改一个变量。比如这次只改max_threads,下次只改索引,不要一次改三个地方,否则出了结果你也分不清是谁的功劳。
clickhouse-benchmark特别适合作版本升级前的预检。升级ClickHouse小版本之前,把生产环境的Top SQL收集下来,在测试环境分别用旧版本和新版本跑一遍压测,如果P99有明显回退,至少能提前发现,而不是上线后被业务侧投诉。
4.2 更接近业务的JMeter与自定义脚本:混合场景怎么压
clickhouse-benchmark解决的是单纯的SQL压测需求,但真实业务往往不是单条SQL反复跑,而是有多种查询模式混在一起,甚至还有写入。这时候可以用JMeter配合JDBC驱动做一个更接近业务的压测脚本。
JMeter里核心就两步:配置JDBC Connection,然后添加JDBC Request采样器。每个线程可以维护独立连接池,注意不要让JMeter自己成为瓶颈。压测用的机器如果太弱,服务器CPU还没跑满,客户端先撑不住了,最后读出来的结果全部失真。
如果不想用JMeter,也可以用Python的asyncio或者Go的goroutine写一个简单压测器,直接通过HTTP接口发SQL。这里有个小技巧:每条压测请求都带上唯一的query_id,之后可以直接在query_log里关联到压测产生的所有查询,方便分析服务端的真实执行情况。
压测结果解读有几个容易看错的点,值得单独提醒。客户端显示高延迟,不代表服务端真的慢,有可能是客户端连接池不够,请求在客户端排队了。反过来,服务端query_log里的执行耗时也在涨,说明确实是引擎层面压力过大。还有,ClickHouse的HTTP接口默认有并发查询限制,比如max_concurrent_queries,压测时并发一旦超过这个值,新请求会被排队或拒绝,这时候你看到的现象是"客户端延迟飙升",但真正的瓶颈是保护机制生效了,不是引擎计算太慢。
要压出贴近生产的效果,我一般会混合读和写:大约80%的聚合查询,加上20%的小批量插入,并持续观察parts数量变化。这种压法能暴露一个问题——大量并发插入产生的小parts会不会把后台合并线程拖垮,进而拖慢查询。
5. 一次慢查询的完整排查链路:从现象到工具链落点
5.1 第一轮定位:query_log确认退化趋势,parts膨胀坐实嫌疑
讲一个真实发生在某套报表环境里的排查过程。有一张主表每天新增大约2000万行,某天开始,业务接口的查询从稳定的200ms左右退化到2秒上下,而且一天比一天严重。
拿到问题之后,我没有直接去翻配置,而是先用query_log定位这个固定SQL的耗时趋势。
SELECT toStartOfHour(event_time) AS hour, count() AS exec_count, avg(query_duration_ms) AS avg_ms, quantile(0.99)(query_duration_ms) AS p99_ms FROM system.query_log WHERE query LIKE '%report_table%' AND event_time >= now() - INTERVAL 3 DAY GROUP BY hour ORDER BY hour;结果很清晰:从某个时间点开始,平均耗时和P99都持续走高,呈单调上升曲线。这种形态基本排除偶发抖动,一定是某个底层条件在持续恶化。
接下来查parts数量。
SELECT count() AS parts, sum(rows) AS total_rows, sum(bytes_on_disk) AS total_bytes FROM system.parts WHERE active AND table = 'report_table';结果让我眼皮一跳:active parts数量已经超过2000,而正常状态应该在几百以内。原因是上游导入任务用大量小批次INSERT灌数据,每次插入都生成新的part,后台合并线程根本来不及消化。查询时,读取路径要把这么多part的索引和数据都纳入考量,IO放大非常严重。
处理办法是先手动触发合并,同时调整合并线程预算,避免后台合并拖垮查询。另外从源头控制导入批次大小,把小批次聚合成中等批次再写入。合并完成后,查询回到800ms左右,比之前好不少,但离200ms的基线还差得远。
5.2 第二轮微观定位:trace_log找到函数热点,索引设计收尾
第一次合并没有完全解决问题,说明这轮慢查询除了parts膨胀,还有别的问题在叠加。我从慢查询里挑了一个query_id,去trace_log里抽采样数据,生成火焰图。
火焰图里暴露出来的热点非常集中——大量CPU时间耗在字符串处理和排序比较上。再回头看那条SQL,发现它在GROUP BY和WHERE里用了一个高基数String字段,这个字段的值分布非常散,基数接近百万级别,每次查询都要在哈希表里做大量字符串比较。
确认热点之后,我做了三个调整,每个调整单独验证,绝不三个一起上。
第一步,把这个高基数字段改成数字字典关联,同时对适合低基数的枚举字段声明LowCardinality类型。字符串操作大幅减少,CPU热点肉眼可见地降下来了。
第二步,给高频过滤条件加bloom_filter跳数索引,让查询在granule粒度就能跳过大量无关数据,而不是逐行扫描。
第三步,重新审视主键顺序。原表的主键没有把高频过滤列放在最前面,导致过滤条件没法走主键裁剪。调整了ORDER BY字段顺序之后,范围过滤的效率明显提升。
每一步改完,都用clickhouse-benchmark跑同一批SQL,记录P99变化。最终结果是,这一轮慢查询的P99从1.2秒降到200毫秒以内,同时集群CPU使用率下降了15%以上。
5.3 复盘:工具链怎么配合,才算真正形成了闭环
回头看这个案例,整个过程没有用到任何一个冷门工具,核心就是query_log、parts表、trace_log和benchmark这几样东西的组合。它们各自分工明确:query_log负责描述现象,parts表负责定位数据布局问题,trace_log负责找到CPU热点,benchmark负责验证每一步调整的效果。
我特别想强调的一点是:不要试图用单个工具一步到位。很多人喜欢上来就抓火焰图,但热点函数背后往往不是函数本身的问题,而是数据模型和查询结构不合理,先让query_log和parts表告诉你该往哪个方向查,再去微观剖析,会高效得多。工具在ClickHouse调优里真正的价值,是让每一次调整都变成有证据支撑的决策,而不是靠猜。
排查结束之后,我通常会把这类慢查询案例整理成一份简短的记录,包括现象、定位过程、改动内容和验证数据。时间一长,这些记录就变成了个人经验库里最有价值的资产。以后再遇到类似报障,翻一下历史台账,往往十分钟就能定位到问题方向。
最后再分享一个小技巧:可以定期把query_log里的慢SQL按天聚合成一张汇总表,专门记录每天Top SQL的耗时、扫描行数和执行次数。这个汇总表不仅能看到Top SQL的整体趋势,还能在业务迭代过程中提前发现性能退化倾向。坚持做一个月,你会拥有一份非常珍贵的业务数据字典和性能台账,这比任何商业监控工具都更贴合你自己的集群。