ClickHouse性能测试实战指南:从环境搭建到查询调优的全流程解析
1. 为什么要做 ClickHouse 性能测试测之前先想清楚这几件事在真正把 ClickHouse 用到线上之前或者在做 ClickHouse 数据库整体迁移的时候有一件事怎么都绕不过去——性能测试与评估。这不是跑几个 SQL 看看快不快就完事的而是要用一套可重复、可比较、能暴露问题的方法把系统的真实能力摸清楚。很多人对 ClickHouse 的第一印象是“查得快”这个印象没错但“快”是有前提条件的。它快在宽表聚合、快在列式扫描、快在按主键过滤的区间查询但在高并发小查询、频繁点查、极端数据倾斜、大面积数据更新这类场景下ClickHouse 的表现远没有宣传的那么完美。如果没测过就直接上线等到线上出现慢查询、资源被打满、查询互相拖垮的时候再来排查代价就高了。所以这篇内容适合谁看适合正在评估 ClickHouse 能不能接住你的业务、准备做迁移前评估、或者已经在用但想搞清楚系统瓶颈在哪的工程师。我不会讲太多官网文档里能查到的东西重点放在“我实际是怎么测的”“测完怎么看数据”“哪些参数帮你解决问题”这几块都是能直接落地参考的。1.1 性能测试不是跑几个 SQL 就完事先纠正一个常见的误区性能测试不等于功能测试。功能测试关心的是“查询结果对不对”性能测试关心的是“在多长时间内、用多少资源、在多高并发下结果能出来”。这两个目标经常是冲突的比如同一条 SQL不同执行计划走出来的耗时可能差很多但结果完全一致。所以做性能测试时我们要控制变量保证每次查询走的是同一条路径测试结果才有可比性。ClickHouse 性能测试至少涉及三个层面单查询性能、并发查询性能和写入性能。单查询性能衡量的是这条 SQL 在这个数据集上的极限耗时回答“最坏情况下要等多久”并发查询性能回答的是“同时 50 个人点报表会不会把 CPU 打满或者互相拖垮”写入性能回答的是“每秒能接住多少条数据批量写和实时写分别什么表现”。另一个容易忽略的点是ClickHouse 的查询性能高度依赖数据分布。同样的 SQL在数据均匀分布的表上可能毫秒级返回在数据倾斜严重的表上可能几十秒都跑不完。测试数据必须尽可能接近真实业务的数据特征否则测出来的数字没有参考价值。1.2 测试目标决定一切先定义“什么算性能好”我见过不少团队做性能测试上来就问“ClickHouse 是不是比 MySQL 快 100 倍”这种问法本身就没法回答。性能测试的第一步不是选工具而是定义“好”的标准。拿一个实际场景举例假设我们要评估 ClickHouse 能否支撑一个用户行为分析平台核心需求是每天接入 10 亿条埋点数据分析师在 BI 工具里做条件筛选和聚合查询P95 响应时间需要控制在 3 秒以内。这个目标定下来之后整个测试方案就有了方向数据集规模应该按 30 天到 90 天的数据量来造查询要覆盖日期范围过滤、多维 group by、去重计数、时间窗口聚合这些典型模式并发数要按团队规模设到 20 到 50 个并发查询最后看 P95 能不能压进 3 秒。反过来如果只是做一个几百台服务器日志查询的内部工具并发要求只有 5 个以内那测试重点就应该放在单查询性能和写入吞吐上并发压测做到 10 个并发基本就够了。测试标准不是越严越好而是贴近真实负载才算有效。所以在正式动手前建议先花半天时间把下面这张表填清楚核心查询场景是什么、表数据量多大、数据增长速率多快、每日查询量级多少、并发峰值多少、延迟要求多少、可用性要求多少。这张表填完测试方案基本就成型了。2. 环境与数据准备测试结果靠不靠谱一半看这里ClickHouse 性能测试有个特点硬件稍微变一变结果能差出一个数量级。磁盘从 HDD 换成 NVMe SSD查询耗时可能直接下降 60% 以上CPU 主频高一点复杂聚合的耗时差别也很明显。所以环境准备阶段的目标不是“跑出最好看的数字”而是“跑出可复现、可对比的数字”。环境搭建这块我先给一个参考配置再解释为什么这么搭。我这次测试用了 3 个节点的 ClickHouse 集群每台机器是 16 核 CPU、64GB 内存、2TB NVMe SSD操作系统是 CentOS 7.9ClickHouse 版本用的 24.3 LTS。三台机器组成一个 cluster副本数设为 2数据分布在两个分片上。这套配置不算豪华但已经能代表大多数中小规模生产集群的真实水平测出来的数据有实际参考意义。2.1 测试环境怎么搭才能让数据“说真话”第一个建议测试环境一定要独立。别为了省事把 ClickHouse 装在正在跑业务的机器上也别和 Kafka、Redis 这些服务混部。原因很简单ClickHouse 压测会把 CPU 和磁盘 IO 打到很高如果旁边还跑着别的服务CPU 争抢会直接污染测试数据。另外 ClickHouse 的内存管理策略比较激进默认会尽量多吃内存混部的话很容易把同机其他进程挤到 OOM。第二个建议操作系统层的参数不要动太多但有几个必须设置。文件句柄数限制ulimit -n要调大官方推荐至少 1048576否则高并发压测时会报 Too many open files禁用透明大页THP因为 THP 在内存分配时可能造成较大的延迟抖动对延迟敏感的查询影响很明显CPU 调频策略设成 performance避免 CPU 频率动态变化导致测试结果不稳定。这几个参数设置完再用 sysbench 或者简单的 CPU 跑分工具做一次基线测试确认三台机器性能一致。第三个建议ClickHouse 的版本要尽早锁定。每个版本的查询优化器、聚合算法、MergeTree 合并策略都可能不一样测试中途换版本是大忌。如果是为了迁移做评估那就用目标生产环境的同版本如果是新项目选型就用最新的稳定版LTS 优先测完记录下来后续升级时还能做对比。还有一个隐蔽的坑不同版本对 SQL 的解析行为有差异比如某些函数支持了新的参数同一句 SQL 在新版里的执行计划可能完全变了这会让跨版本对比失去意义。2.2 造数据测试集设计比你想的重要得多数据集的设计是整个测试里最容易被低估的环节。数据太规整测出来的性能偏乐观上线后真实数据一进来性能直接崩数据太乱又分不清慢是慢在数据还是慢在 SQL。合理的做法是尽量模拟真实数据并且把极端情况也放进去。以我这次的用户行为分析场景为例我造了一张行为日志表包含用户 ID、事件类型、事件时间、页面 URL、设备类型、地域、停留时长等字段总行数 1 亿行时间跨度 90 天。数据特征上做了这么几件事用户 ID 的基数按 1000 万设计保证 group by 是真正的高基数聚合事件类型里埋了一个热点事件占比 30%模拟真实业务里的头部效应地域字段按二八分布少数几个地域占了大部分数据事件时间做了每天 24 小时不均衡分布白天数据量明显高于凌晨。造数可以用 Python 脚本生成 CSV 再导进去也可以用 ClickHouse 自带的generateRandom函数直接生成。generateRandom用起来简单但生成的数据是完全均匀的随机分布和真实业务差距较大建议还是自定义一个脚本用随机权重来控制分布。造数脚本本身不复杂但有个效率问题1 亿行数据如果一条条 INSERT 会非常慢正确做法是先用 Python 按批量生成文件然后用clickhouse-client --queryINSERT INTO table FORMAT CSV data.csv导入。我实测 1 亿行数据大概 30GB单节点批量导入约 15 分钟能完成三节点集群加副本会慢一些。数据造完之后不要急着开测要先做一轮数据质量校验COUNT 总数是否等于预期、关键字段的值分布是否符合预期、各分片数据量是否均衡。数据倾斜在 ClickHouse 里不是小问题如果某个分片上的数据明显多于其他分片查询会把那个节点打满整体性能会被拖垮。我见过有人在测试时发现节点间数据量差了 3 倍查了半天才发现是分片键选错了这种问题越早发现代价越小。3. 查询与写入性能测试从单条 SQL 到并发压测的完整执行记录环境准备好、数据造完之后就进入真正的测试环节。整个测试过程我分成三条线先做单查询性能基线再做并发压测最后补上写入性能测试。顺序上之所以先查后写是因为查询性能是最核心的指标先把查询能力摸清楚再考虑数据能不能喂得进来。3.1 单查询性能基线用 clickhouse-benchmark 跑一轮单查询测试有两种常用方式一种是用官方的clickhouse-benchmark工具另一种是自己写脚本循环执行查询取平均值。我的习惯是两种都用但以官方工具为主原因是它输出了完整的统计信息包括 min、max、mean、quantile 等省得自己写统计逻辑。clickhouse-benchmark的基础用法很简单clickhouse-benchmark --query SELECT count() FROM events WHERE event_date 2024-01-01 AND event_date 2024-02-01 --host 127.0.0.1 --port 9000 --iterations 100 --concurrency 1这条命令会对指定查询跑 100 次并发设为 1输出每次查询耗时、平均值、中位数、P95、P99 等指标。我建议每次跑之前先清空缓存否则第二次跑命中了 page cache数据全在内存里耗时会是冷查询的十分之一不到完全没有参考价值。清缓存的方法是先执行SYSTEM DROP FILESYSTEM CACHE需要管理员权限或者用clickhouse-client执行SYSTEM DROP MARK CACHE和SYSTEM DROP UNCOMPRESSED CACHE。单查询测试的 SQL 集是根据真实业务场景整理的一般 20 到 30 条覆盖这几类简单条件过滤比如SELECT count() FROM events WHERE event_date today()模拟报表请求。多条件组合过滤比如时间范围加设备类型加地域模拟分析师的筛选操作。GROUP BY 聚合查询比如按天、按设备类型统计 PV/UV。多表 JOIN 查询模拟维度表关联。排序加 LIMIT 的分页查询。包含去重逻辑的 UV 类查询uniqExact和uniqCombined都要测因为两者的性能差异很大。测试结果整理成一张表每一条 SQL 对应一行记录 min、avg、P95、P99 四个值。这张表就是后面所有调优工作的基线没有它你根本不知道改动到底起没起作用。3.2 写入性能测试批量导入与实时写入都要测ClickHouse 的写入性能测试经常被忽略但它恰恰是最容易出现“上线后才发现数据进不来”事故的环节。写入测试要从两条路径分别测批量导入和实时写入。批量导入对应的是离线场景比如每天凌晨从离线数仓同步数据。测试方式很简单用之前准备好的数据文件反复导入多张表观察导入耗时、每秒行数、每分钟数据量等指标。批量导入的性能瓶颈主要在磁盘 IO 和 MergeTree 的合并速度上。这里有个细节值得注意导入速度不等于写入速度因为 ClickHouse 的写入是先落内存再异步合并写进去的数据最初是 part 文件后台再慢慢 merge 成大文件。如果批量导入太快后台合并跟不上会导致 part 数量暴涨查询性能大幅下降。测试时除了看导入耗时还要观察system.parts表里的 part 数量变化如果 part 数持续增长不下降说明合并能力已经到瓶颈了。实时写入对应的是流式接入场景比如从 Kafka 消费埋点数据写入 ClickHouse。测试方式是用一个小脚本模拟多线程写入每条数据单条插入观察写入延迟和吞吐量。ClickHouse 官方对实时写入的建议是“攒批”不要一条条写推荐每次插入至少 1000 行或者等待 1 秒再 flush。这是因为每条 INSERT 都是一次独立的写操作会产生一个 part 文件写入过于频繁会导致 part 文件爆炸。实测单条插入吞吐大概在每秒几千行的量级而攒批 1000 行写入可以轻松到每秒几十万行。写入测试要特别注意数据可靠性验证。写完之后立刻查询 COUNT 是否等于写入行数这个步骤不能省。ClickHouse 在异常情况下的写入失败不一定报错尤其在使用异步写入时可能会静默丢数据。我建议在写入测试结束后用系统的CHECK TABLE命令检查数据完整性再用不同时间窗口的 COUNT 和 SUM 做交叉验证。3.3 并发场景用 JMeter 做 SQL 压测的正确姿势并发压测是评估 ClickHouse 生产能力的核心环节。很多人以为并发压测就是起几十个线程同时查询把请求打过去看响应时间其实没那么简单。并发压测的核心是找到系统的“拐点”并发数从 10 涨到 100 的过程中吞吐量是线性增长还是很快触顶延迟是平稳上升还是突然恶化这个拐点决定了系统的容量上限。JMeter 是常用的压测工具方案可以这么设计用 JDBC Connection Configuration 配置 ClickHouse 的 JDBC 驱动连接用 Thread Group 控制并发线程数用 JDBC Request 执行查询 SQL用聚合报告和事务控制器统计结果。具体步骤下载 clickhouse-jdbc 驱动放入 JMeter 的 lib 目录。添加 JDBC Connection Configuration配置数据库 URL格式如jdbc:clickhouse://127.0.0.1:8123/default、用户名、密码空闲连接数设大一些比如 50防止连接成为瓶颈。添加线程组并发数从 10、30、50、100 逐级递增每个线程组循环执行多次查询。添加 JDBC Request写入要压测的 SQL。一条 SQL 对应一个 JDBC Request 比较好如果用 Variable Name 动态传参要确保参数化逻辑正确。添加监听器重点看 Aggregate Report 里的 Average、95% Line、Throughput 指标。这里有一个很多人踩过的坑JMeter 默认的 JDBC 连接池参数如果不调压测时会出现连接获取超时导致大量请求失败。解决方案是把 JDBC Connection Configuration 里的 Max Number of Connections 设为线程数的 2 倍以上Connection Timeout 设成 10000 毫秒。并发压测的执行策略也很有讲究。不要一上来就把并发拉到 100要逐级加压先 10 并发跑 3 分钟再 30 并发跑 3 分钟然后 50、100、200每个并发级别跑完先看吞吐量和延迟数据再继续。如果中途出现查询失败或延迟暴涨说明已经到了系统瓶颈不必强行压上去。还值得一提的是现在有不少团队开始用 AI 辅助生成压测脚本比如让 AI 根据业务 SQL 自动生成 JMeter 脚本模板。这个做法能省不少时间但脚本里参数化、断言、连接池配置这些逻辑AI 生成的版本往往不够精细。我的建议是AI 写的脚本只能当草稿必须人工 Review 一遍连接配置和线程参数最好用一条已知性能基线 SQL 跑一遍校准确认压测结果和数据一致再用于正式测试。4. 结果分析与调优从测试数据里挖出系统的真实瓶颈测试跑完只是第一步真正有价值的工作在分析阶段。很多人测完拿到一堆数据不知道怎么解读或者看到 P95 不准就想调参结果越调越乱。这里分享一套我总结的分析思路先看现象再定位瓶颈最后才动手调优。4.1 先看指标再谈优化响应时间、吞吐量、资源使用率怎么解读拿到压测报告第一件事是看整体趋势而不是纠结某一两个慢查询。先回答下面几个问题吞吐量QPS是随并发数线性增长还是到了某个并发数就停滞了如果停滞停滞在哪个并发级别响应时间P95是从一开始就随并发上升还是到一个阈值后突然恶化资源使用率里CPU 是全部打满还是部分核心忙、部分核心闲内存使用率有没有异常增长磁盘 IO 的等待时间是否明显偏高把这几个问题看清楚基本就能判断系统瓶颈在哪个环节。我举几个常见的案例CPU 打满但吞吐量不涨说明 CPU 是瓶颈。这时候再调高并发只会让延迟增加应该考虑加节点、优化 SQL、减少计算量。CPU 使用率不高但响应时间很高说明系统可能在等锁或者等磁盘 IO。先看磁盘 IO 等待时间再看是不是出现了大量内存溢出、数据落盘。内存持续上涨且触顶可能是查询过于复杂导致中间结果集过大触发了内存上限和磁盘回写。ClickHouse 的日志里会明确记录这种事件。举个例子我之前测一张 10 亿行的日志表执行按用户 ID 去重计数的查询单并发耗时 1.8 秒。看起来不算慢但把并发提高到 30 之后P95 直接恶化到 8.5 秒吞吐量反而低于 20 并发的水平。排查发现是内存不足导致部分聚合操作触发了磁盘临时数据落盘ClickHouse 的 group by 聚合在内存放不下时会 spill 到磁盘这个过程会急剧拖慢查询。解决办法是调整max_bytes_before_external_group_by参数让它在内存用到一定程度时就提前启用外部聚合避免内存打满才触发导致性能雪崩。4.2 常见的调优方向MergeTree 家族、索引、分区与参数分析完瓶颈之后调优才有方向。ClickHouse 的调优维度很多我重点说四个我实践下来收益最大的方向。第一个是表引擎选择。同样是 MergeTree 家族不同引擎的特长完全不同。如果数据有明确的业务时间维度用MergeTree配合时间分区如果业务是“只追加不改历史”可以用Log或TinyLog类型查询更轻量如果数据量极大且经常要按主键点查可以考虑ReplacingMergeTree或AggregatingMergeTree做预聚合。表引擎选错后续不管怎么调参都是隔靴搔痒。第二个是 ORDER BY 键和主键索引设计。这是我见过最多人犯错的地方。ORDER BY 决定了 ClickHouse 稀疏索引的排布方式直接影响查询的过滤效率。基本原则是高频过滤条件的列放在前面区分度高的列放在前面。比如一个用户行为表业务查询经常按event_date加user_id过滤那 ORDER BY 里的顺序应该是event_date, user_id而不是反过来。如果 ORDER BY 把高基数列放太靠前前缀匹配会很快失去过滤效果导致扫描大量无关数据。第三个是分区设计。分区粒度不是越细越好分区太细会产生大量小文件合并压力大查询时也要扫描多个分区分区太粗则过滤效果差。一般按天或按周分区比较合适如果历史数据需要长时间保留可以按 TTL 自动过期避免手动删除分区带来的性能波动。我建议用EXPLAIN语句检查查询覆盖的分区数如果一个查询扫了 90 个分区但实际只需要最近 7 天的数据说明查询条件没有走分区裁剪这时要检查查询条件是否用上了分区键或者分区键设置是否合理。第四个是查询参数调优。有几组参数对查询性能影响很大max_threads控制每个查询使用的 CPU 线程数默认是 CPU 核数但对小查询来说线程太多反而会增加调度开销可以调到 8 到 16max_memory_usage控制单个查询的最大内存使用防止大查询把整机内存吃光max_bytes_before_external_group_by控制 group by 时是否启用外部聚合建议在内存的 60% 左右设置max_concurrent_queries控制同时执行的查询数超过这个数的查询会排队等待对控制系统的稳定性很重要。调优是一个反复循环的过程调整参数或表结构重新跑同样的 SQL 集对比基线数据如果不改善就回滚。不要凭感觉一次改好几个参数那样出了问题完全无法定位。我习惯一次只改一个变量记录前后数据差异形成一个小的调优日志时间久了这就是宝贵的数据库优化经验库。5. 测试过程中踩过的坑与排查实录写这篇文章的时候我把这些年做 ClickHouse 性能测试遇到的高频问题整理成了一份排查速查表。这些问题看似零散但都真实影响过测试结果的准确性分享出来能帮你避开不少冤枉路。5.1 内存超限导致查询直接 OOM这是我遇到最多的问题。ClickHouse 默认配置下一个复杂的 GROUP BY 查询可以把内存吃到十几个 GB如果并发查询一多机器直接 OOM。第一次遇到我还以为是 ClickHouse 的 bug后来才发现是参数没有调好。解决方案分三个层次先用max_memory_usage把单查询内存限制住比如设成 16GB。超限后查询会因为内存不足报错但至少不会拖垮整个节点。如果业务允许用max_bytes_before_external_group_by让 ClickHouse 在内存接近上限时就启用外部聚合把中间结果写到磁盘。实测这个参数可以把内存压力降低 50% 以上代价是查询时间从秒级变成十几秒但比 OOM 强多了。最后的兜底方案是给 ClickHouse 进程配置 cgroup 或者 systemd 的内存限制避免进程无节制地吃内存把操作系统拖死。不过这个属于运维手段最好还是从查询和参数层面解决。顺便提醒一下ClickHouse 的 OOM 并不总是立刻报错有时候表现出来的是查询突然变慢。因为操作系统内存不足时会先做 swap 或者触发 page cache 回收磁盘 IO 飙升查询性能全面恶化。所以压测时遇到“没报错但性能明显下降”的情况优先看一下dmesg里有没有 OOM 记录以及系统的内存水位。5.2 并发测试线程数上不去连接数被卡死JMeter 压测时经常遇到并发数提到 50 以上就开始出现大量连接超时报错。最开始我以为是 ClickHouse 服务端的问题查了半天system.metrics里的当前连接数发现远没到上限后来才定位到是 JMeter 的 JDBC 连接池配置有问题。点击 JMeter 里的 JDBC Connection Configuration里面有个 Max Number of Connections 参数默认值通常比较小如果并发线程数大于这个值线程就会排队等连接超时后报错。解决方法是把最大连接数设成并发线程数的 2 到 3 倍同时把 Connection Timeout 设长一些比如 10000 毫秒。ClickHouse 服务端的连接数限制也要注意默认的max_connections一般不用调但如果压测机和服务端之间有负载均衡或者代理连接数会被代理层限制住这时候排查链路可能花不少时间。我有个经验技巧压测时用watch -n 1 clickhouse-client --querySELECT count() FROM system.metrics WHERE metric LIKE \TCPConnection%\;实时观察连接数变化如果卡在某个值上不去基本就是链路中间层的问题。5.3 结果波动大可能是系统层面在捣乱性能测试最怕的就是“测试结果不可复现”。同样的 SQL第一次跑 P95 是 2 秒第二次变成 4 秒第三次又回到 2.5 秒这种波动会让所有分析失去意义。引起波动的原因通常是这几个page cache 没有清理。第一次查询之后数据已经加载到系统内存的 page cache 里第二次查会快很多。解决方法是查询之间清理缓存或者冷热数据交替测试。后台合并进程MergeTree 的 merge在跑。如果表里正在发生大规模的 part 合并磁盘 IO 和 CPU 都会被占掉查询性能自然下降。测试前最好先执行SYSTEM STOP MERGES暂停合并测完再恢复。CPU 频率动态调整。服务器设置了省电模式的话CPU 频率会在负载变化时波动。测试前确认 BIOS 或系统层面设成 performance 模式。其他进程抢占资源。这个前面说过压测机器不能混部其他服务。还有一个比较容易忽略的因素ClickHouse 的查询日志。如果压测过程中有大量system.query_log写入也会占用资源。建议测试完再整理日志而不是测试过程中让日志实时写入。5.4 测试数据落差大可能是数据分布没模拟好这个坑我提过一次但值得再强调如果真实业务的数据分布和测试数据差距过大那么测试结果基本没有参考价值。一个比较典型的场景是时间字段的数据分布。真实业务的数据往往有很强的周期性比如白天是晚上的好几倍但测试数据如果均匀分布在每天那按天分区的查询性能测试结果就会偏乐观。实际跑的时候白天时间段的数据量更大扫描的分区大小不均查询性能反而不如均匀分布时理想。另外还要注意 NULL 值和空字符串的处理。ClickHouse 对有空值的列在存储和计算上都有自己的内部实现如果真实数据里有大量空值测试数据里也要按比例加入否则字段的压缩率和索引选择都会和真实情况有偏差。结尾做一轮完整的 ClickHouse 性能测试工作量比想象中大。最花时间的不是跑测试本身而是数据准备和结果分析。数据造得越贴近真实测试结果越有价值指标看懂了才知道瓶颈在 CPU、内存还是磁盘上。我个人在实际操作中最深的体会是性能测试不要只做一次。系统的负载特征会随业务发展而变化数据量从 1 亿涨到 10 亿SQL 逻辑越来越复杂ClickHouse 的性能表现也会完全不同。建议至少每季度做一次基准回归测试把当时记录的性能基线数据翻出来对比这样能及早发现性能劣化的趋势而不是等用户投诉了才想起来排查。最后再分享一个小技巧测试完成后把 ClickHouse 的system.query_log和system.part_log里的关键信息导出备份这些日志保留了实际查询的执行计划、扫描行数、耗时统计等数据比你自己记录的测试结果要精确得多。后续调优时翻出这些日志做二次分析经常能发现当初没注意到的问题。