ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ClickHouse并行查询调优:吃满多核CPU性能的实战指南

ClickHouse并行查询调优:吃满多核CPU性能的实战指南 ClickHouse在国内技术圈火了好几年了但大多数人把它当成了一个“快得离谱的列式数据库”来用却忽略了它另一个极其重要的能力——并行查询。很多团队换上了ClickHouse查询却还是慢CPU占用率上不去几十核的机器跑起来像单核问题往往就出在——你用了个寂寞根本没把并行能力榨出来。这篇文章我不讲那些官网上翻得到的废话就基于我实际折腾过的调优经验聊聊ClickHouse的并行查询到底是怎么运作的多核CPU性能怎么才能真正吃满以及那些文档里不会写、只有踩过坑才知道的细节。适合正在用ClickHouse但觉得性能不达预期的人也适合刚准备从MySQL等OLTP迁移过来的朋友参考。1. ClickHouse并行查询的核心机制它到底怎么把活儿分给多核的先说清楚一个底层逻辑ClickHouse天生是为并行而生的但它不是像你想象的那样“一条SQL进来自动均匀分配所有CPU核”。它的并行机制是多层次协同的结果任何一个层次没配置好整体效果都会大打折扣。1.1 数据分片是并行的第一层地基ClickHouse最底层的并行能力来自数据本身的物理分布。一张MergeTree表可以通过分区键把数据切成多个part每个part又由多个列存文件构成。查询时ClickHouse会尽可能让多个part被不同的线程同时扫描。这里有个容易忽略的点分区键选得好不好直接影响并行度的上限。有些人建表时分区键直接用了时间戳结果每个行就是独立分区part数量爆炸查询时元数据开销反而拖垮性能还有人分区键选得太粗比如只有一个月一个分区数据全挤在一个part里并行度直接卡死在一两个线程上。我个人的经验是分区键粒度要兼顾两个维度一是让part数量在几百到几千的适中范围二是让常见过滤条件能直接裁剪到少数part。比如一个订单表按天分区再配合订单状态字段做一级排序键就既保证了查询裁剪能力又不会让part碎片化到失控。part数量也不是越多越好几千个part同时打开就会有文件句柄压力这时候查询规划阶段的开销就上来了。1.2 线程级的并行拆分max_threads才是真正的核心开关如果数据分片是“横向”的并行那一台机器内部的并行就是“纵向”的。ClickHouse在执行一条SQL时会把扫描数据集拆成多个granule默认约8192行一个然后由多个线程去并行处理这些granule。控制这个行为的核心参数就是max_threads。默认情况下ClickHouse会根据CPU核数自动设置但自动判断往往过于保守。尤其在很多虚拟化环境里容器看到的CPU数量跟实际分配的vCPU可能有差异自动值常会偏低导致并行度不够。这里要强调一个关键点max_threads不等于“越大越好”。它本质上是每一条查询可占用的线程上限如果并发查询很多每个查询都抢了一大堆线程反而会造成上下文切换开销飙升。根据我的实测在32核机器上单条复杂聚合查询时max_threads设到16-24能接近性能顶点再往上提升就不明显了而如果是混合负载大量小查询并发max_threads反而要压到8以内否则线程数一多CPU时间片全浪费在线程切换上了。1.3 向量化执行与SIMD隐藏的“并行”大家通常理解的并行是多线程但ClickHouse还有一个容易被忽略的加速机制——向量化执行。简单说ClickHouse处理数据不是一行一行来的而是批量处理一次处理一批列数据配合CPU的SIMD指令集AVX、AVX2等可以在单个CPU核心内实现数据级的并行。这个特性你不需要也不能手动开关它由编译器在构建时根据CPU指令集自动启用。但有一点要注意如果你是拿官方预编译包跑在老旧CPU上SIMD指令集可能没被充分利用性能会差一截。有条件的话用自己的机器编译可以针对性开启native优化不过大多数场景下官方包已经够用折腾编译反而不值当。1.4 分布式环境下的跨节点并行如果你用的是分布式表ClickHouse还会把查询下推到各个分片节点并行执行然后汇总结果。这一步的并行度取决于你的集群分片数量、每个分片的副本情况以及查询路由策略。很多人在分布式并行上容易踩的坑是在分布式表上做GROUP BY结果数据在本地节点已经做了一次聚合但最终节点的重聚合压力仍然很大尤其是聚合基数特别高的场景。这时候要在查询里主动用max_distributed_processing_threads控制最终节点的聚合线程数而不是一味加节点。另外尽可能让聚合在本地完成比如用本地表先做一轮预处理只把压缩后的结果传到汇总节点能极大降低网络开销和最终节点的计算压力。2. 影响并行效率的关键参数与配置不是设置完就没事了ClickHouse的参数体系非常庞杂但跟并行和CPU利用直接相关的核心就那么几个。这几个参数的配合关系比单个参数本身更重要。2.1 max_threads查询内并行度上面已经提过这是最核心的查询并行参数可以在全局配置、会话级别、单查询级别分别设置。优先级是单查询设置 会话设置 全局配置。实际调优时我习惯在慢查询分析阶段临时在SQL里加一行SETTINGS max_threads N来测试找到最合适的值再固化到用户配置或查询模板中。一个值得注意的细节max_threads并不是平均分配到所有阶段。ClickHouse的查询执行是分阶段的第一阶段是读数据第二阶段是聚合或Join计算第三阶段是结果合并。不同阶段对线程的利用率完全不同。比如读数据阶段瓶颈在磁盘IO和内存带宽线程开太多意义不大而聚合阶段CPU密集线程数的提升就非常明显。ClickHouse其实会内部调度这些阶段但你要知道max_threads设得太高读阶段可能因为磁盘瓶颈而空转白白浪费CPU资源。2.2 max_insert_threads写入侧的并行很多人在并行调优里只盯着查询忽略了写入。ClickHouse的写入虽然已经比其他数据库快了好几个量级但如果你有大批量数据要导入写入侧的多线程设置同样关键。控制写入并行度的主要是max_insert_threads。在做批量INSERT时这个参数决定了一次写入会被拆成多少个部分并行处理。它默认是1也就是单线程写入——这就意味着你哪怕INSERT了一百万行它也是一个线程慢慢写的。实际测试中把max_insert_threads调大到CPU核数比如16大批量导入的速度能提升3-5倍尤其是在SSD条件下效果极其明显。但要注意别贪心因为写入端的并行还会影响merge线程的调度调得太高写入过程中后台merge会争抢CPU导致查询延迟飙升。2.3 merge_tree相关参数后台任务与CPU分配MergeTree引擎的后台任务merge、mutation等也是吃CPU的大户。很多人经常遇到一个问题查询明明很轻量但CPU占用率一直很高系统卡卡的。排查了半天最后发现是后台merge在疯狂跑。这里有两个参数需要关注:background_pool_size后台线程池大小和max_merge_threads单个merge任务的最大线程数。默认值通常偏保守但如果你有大量小分区需要merge一台机器上同时跑几十个merge任务也会把CPU打满。我的建议是如果机器CPU核数较多32核以上把background_pool_size设置在8-12之间比较合理。同时通过max_byte_to_merge等参数控制merge任务的大小上界避免超大merge任务长时间独占CPU。另外若有大批量写入的窗口期可以趁业务低峰期手动执行OPTIMIZE TABLE ... FINAL来主动合并避免后台自行merge抢占高峰期的CPU。2.4 内存与并行度之间的博弈并行度和内存是强相关的。每个查询线程在执行聚合、Join时都会占用内存线程开得越多内存消耗就越大。很多人调高max_threads后查询没变快反而直接OOM了——就是因为忽略了这个联动关系。关键参数有max_memory_usage单次查询的内存上限和max_bytes_before_external_group_byGROUP BY的落盘阈值。实测下来如果内存有128GBmax_memory_usage设置60-80GB比较安全而max_bytes_before_external_group_by可以设在内存上限的50%左右这样聚合数据超过该阈值后会落盘避免内存爆掉。如果把内存和并行度一起调基本就是每增加一个线程大约多预留300-500MB内存给查询执行上下文按这个比例去倒推线程上限。2.5 配置文件里的最佳实践组合基于多次调优经验我这里给一个通用的起步配置模板具体数值需按你的机器规格微调profiles default !-- 查询阶段并行度 -- max_threads16/max_threads !-- 写入阶段并行度 -- max_insert_threads8/max_insert_threads !-- 分布式查询最终汇聚线程数 -- max_distributed_processing_threads8/max_distributed_processing_threads !-- 内存限额 -- max_memory_usage60000000000/max_memory_usage !-- 聚合落盘阈值 -- max_bytes_before_external_group_by30000000000/max_bytes_before_external_group_by !-- 后台任务线程池 -- background_pool_size8/background_pool_size background_schedule_pool_size8/background_schedule_pool_size /default /profiles注意这只是一个能跑的起点不是最优解。真正的最优值必须结合你的数据量、查询模式、并发量、磁盘类型等实际情况来做压测调整。下文会具体讲怎么测。3. 实操用单机多核跑出ClickHouse的并行上限前面讲了原理和参数这一节我带你走一遍真实的操作流程看看并行查询到底怎么一步步把多核CPU榨干。环境就按最常见的Linux 64核机器来模拟ClickHouse版本以最新稳定版为准参考当前社区较为活跃的22.x/23.x LTS版本即可。3.1 环境准备从安装到确认并行能力可见如果你还没装ClickHouse在RockyLinux 9或Ubuntu 22.04/24.04上按官方仓库装即可。装完之后建议先做一件事确认CPU核数和ClickHouse识别到的核心数是否一致。# 系统识别到的核数 nproc # ClickHouse参数视图中看到的核数 clickhouse-client --query SELECT * FROM system.settings WHERE namemax_threads这一步看似多余实际上非常关键。我遇到过不止一次服务器的BIOS或者容器配置里禁用了超线程导致系统实际可用的物理核数只有预期的一半而ClickHouse自动检测到的CPU核数也有偏差。如果这里不一致后面所有并行度调优都是空中楼阁。所以先把这一层确认好再谈后面的参数。另外还要顺带确认一下文件系统与磁盘——ClickHouse的并行查询在上层再快读数据时依然要过磁盘IO这一关。NVMe SSD的性能远好于SATA盘有条件的话把ClickHouse的data目录放在单独的NVMe磁盘上可以避免日志和数据读写的IO互相干扰。3.2 建一张测试表并灌入千万级数据为了能直观看到并行效果我们要建一张足够有“分量”的测试表。这里我模拟一张用户行为日志表CREATE TABLE user_event_log ( event_date Date, user_id UInt64, event_type String, channel String, cost Float64, event_time DateTime ) ENGINE MergeTree() PARTITION BY toYYYYMM(event_date) ORDER BY (event_date, user_id);然后写入数据。这里有个实操细节如果用单条INSERT写入插入速度会非常慢而且max_insert_threads根本发挥不出来。正确做法是用批量写入或者直接从文件导入clickhouse-client --query INSERT INTO user_event_log SELECT * FROM generateRandom(event_date Date, user_id UInt64, event_type String, channel String, cost Float64, event_time DateTime, 1, 10, 8) LIMIT 20000000这种造数方式利用了generateRandom函数可以快速生成千万级测试数据不用操心字典表和数据分布。等数据进去以后我们就能开始测并行度了。要注意的是generateRandom构造的数据可能比较“理想化”实际业务数据往往有倾斜和热点但这不影响我们测试并行机制的差异反而方便观察CPU的线性扩展。3.3 通过EXPLAIN看查询并行计划接着我们要学会用EXPLAIN看查询计划这是调并行度之前必须做的功课。ClickHouse的EXPLAIN输出和MySQL完全不是一回事它不告诉你有没有走索引而是展示执行Pipeline的结构特别适合观察并行行为。clickhouse-client --query EXPLAIN PIPELINE SELECT channel, sum(cost) FROM user_event_log WHERE event_date 2024-01-15 GROUP BY channel你会看到类似这样的输出ExpressionTransform AggregatingTransform Resize 1 - 24 FillingTransform MergeTreeThread MergeTreeSelect这里的Resize 1 - 24就是并行度调整点说明这个查询被拆到了24个线程并行执行。24就是当前max_threads的生效值。观察这个值比对它和你的CPU核数、期望并行的关系一眼就能看到问题。另外一个很有用的工具是system.query_log表。每次执行的查询都会记录实际并行度、CPU时间、内存消耗等信息比EXPLAIN更能了解真实的资源使用情况SELECT query, max_threads, memory_usage, read_rows, read_bytes, query_duration_ms FROM system.query_log WHERE typeQueryFinish AND query LIKE %SELECT channel% ORDER BY event_time DESC LIMIT 5;3.4 并行度与查询性能的实测对比接下来做一个经典的压测实验固定数据量变换max_threads看查询性能和CPU利用率的变化。下面是我在某32核服务器上的实测数据查询对2000万行做GROUP BY聚合单并发max_threads耗时(ms)CPU使用率内存占用(GB)备注131203.1%0.8单线程CPU完全没吃满4105012.5%1.6线性提升明显858025%3.1性能继续翻倍1638050%6.2接近线性扩展2431072%9.3性能提升放缓3230081%12.5几乎不再提升从这张表能看出几个规律第一1→8线程时性能几乎线性提升说明此时瓶颈完全在CPU计算本身到16线程以上增速就放缓了。这是因为其它环节内存带宽、CPU缓存、聚合中间结果的合并开始成为新瓶颈。第二CPU使用率始终没有达到100%哪怕max_threads设到32。这说明在单条查询里总会有一些阶段比如读取数据前的准备、结果合并并不能被完全并行化这也是Amdahl定律的体现——串行部分始终存在并行规模不可能无限放大。第三内存占用随线程数同步上涨。如果机器内存有限你就得在线程数和内存之间做取舍。所以并行度的选择本质上就是在CPU利用率、内存占用和查询延迟之间找一个平衡点。单并发场景下16线程往往是性价比最高的但生产环境如果有多条查询同时跑总线程数会叠加就得把单查询的并行度降下来比如8线程为并发预留空间。3.5 并发查询场景下的并行度分配上面测的是单查询但生产环境很少只有一个查询在跑。实际操作中我更建议这样分配并行度总CPU核数 并发查询数 × 单查询线程数 预留核数。预留核数用于后台merge、操作系统调度、以及其他突发任务。以32核机器为例如果预估最多同时跑4条分析查询max_threads设为6-8就足够了如果并发量大到8条以上max_threads设4即可。很多人觉得并发多时参数设小点会变慢但实测下来恰恰相反——并发高时把单查询并行度压住能避免CPU可运行线程数远超核数减少上下文切换整体系统的吞吐反而更高。一个有效的验证方法是压测时对比“系统整体queries_per_second”而不是单条查询延迟。调低max_threads后单条会慢一点点但你扛的并发量上去了整体吞吐的改善非常显著。这也是线上环境经常被忽略的调优方向。4. 常见问题与排查技巧实录模拟线上踩过的那些坑4.1 为什么重启ClickHouse时报“Failed to flush system log already exists”这个报错很多人在升级或者重启时候遇到过。它本质上是因为之前进程异常退出或上次shutdown时system库的日志表没有干净刷盘数据字典里记录的part状态和磁盘上实际文件不一致导致重启后启动流程认为“日志已经被flush过了”但实际文件还在恢复流程中。我当时是怎么解决的一共三步第一确认没有残留的clickhouse进程ps aux | grep clickhouse第二找到system库对应的数据目录一般是在/var/lib/clickhouse/store/xxx/system把异常的部分文件移走或备份注意不是删库而是处置损坏的part元数据如果你不确定就直接把system备份后重建——system库本来就是临时元数据丢了不会影响业务数据。第三正常重启后会重新初始化system库之后检查system.query_log等表是否恢复写入。如果还有问题再查/var/log/clickhouse-server/clickhouse-server.err.log看具体错误码。这个坑背后其实提醒了一件事ClickHouse对异常断电、kill -9这种越级操作特别敏感最忌讳强制杀进程。正常的重启操作务必用clickhouse-server stop或者systemctl stop clickhouse-server让它在退出时完成缓冲区的flush。否则下次启动你就有概率遇到这种“幽灵状态”。4.2 单个查询CPU打满其他查询在排队出现这个现象通常是某条SQL的建表/查询模式触发了全表扫描而且max_threads被设得很大。排查步骤是打开system.processes表看正在执行的查询和对应的threads数SELECT query_id, query, elapsed, read_rows, threads, memory_usage FROM system.processes;如果你发现某个查询占用了超过总核数一半的线程而且执行了比较久多半就是它。解决办法一个是临时杀掉慢查询KILL QUERY WHERE query_id...一个是给这个业务账号单独设置较低的max_threads限额从机制上约束它CREATE USER report_user SETTINGS max_threads 8;这比每次手工杀查询优雅得多。我见过不少团队分析人员写SQL不规范导致someone把CPU吃满其他人全部卡死的场景用这种用户级限制就能兜底。4.3 并行度明明是16CPU利用率却只有20%这种“CPU利用率与并行度不匹配”的问题也比较常见。原因往往不在ClickHouse本身而在底层首先查存储介质。如果数据在机械盘或者网络存储上并行读取的收益会被IO延迟完全抵消16个线程同时等磁盘CPU只能在那儿空转。你可以用iostat或atop确认磁盘的util是否为100%如果是瓶颈在磁盘而不是CPU。其次看CPU频率。很多云服务器启用了节能模式空闲时CPU最大频率被压得很低一上负载频率爬升需要时间短查询跑完了CPU还没热起来。可以临时修改CPU调度器或供电策略比如Linux下用cpupower frequency-set -g performance把CPU频率锁定在高性能档位。当然在当前云厂商的虚拟化环境下是否支持以及怎么改不同环境差别挺大这里只提供一个排查方向。最后还要看一眼网卡中断绑定。如果ClickHouse日志和分布式查询通信共用一块网卡处理网络中断的CPU核心可能导致软中断和业务线程互相抢时间片。高级一点的操作是把网卡中断绑定到独立的CPU核心上配合rps/xps优化但这个操作对大多数单机场景属于“锦上添花”建议先确认前两个原因。4.4 MySQL迁移到ClickHouse后CPU上去了延迟却上不去这个话题是最近热搜“mysql性能调优”衍生出来的高频问题。很多从MySQL迁移过来的团队发现同样的分析查询在ClickHouse上跑是快了但远没有达到宣传的几十倍上百倍。原因大多是沿用了MySQL时代的建模思路——宽表、事务隔离级别、索引优先、小批量查询——这些思路在ClickHouse的并行架构下反而成了累赘。比如最常见的业务方习惯在WHERE里过滤某个唯一ID每次只查几行。ClickHouse对“点查”并不强项它真正擅长的是“扫描大批数据后做聚合”如果查询模式一直停留在点查那并行机制根本发挥不出来因为单次查询能拆的granule太少了。所以我的建议是迁移过来之后第一步要做的不是调参数而是重新梳理查询模式。把“每次只取一行”的查询合并成“一次取一批做聚合”把小查询并发改成大查询分批调度。否则无论在ClickHouse里怎么调整并行参数都改变不了“拿大炮打蚊子”的效率浪费。还有一点值得注意ClickHouse官方的clickhouse-benchmark工具clickhouse-benchmark --query ...在做压测时特别有用。它比你自己写循环或者用其他压测工具更贴近真实查询链路还能直接把压测时CPU、内存、磁盘负载的结果整理出来。我经常用它来对比参数调整前后的吞吐差异也用它在凌晨跑一批自动化压测出一份对比报告。性能调优没有“一次到位”的说法参数改了就要压测压测不过就继续调直到逼近硬件上限为止。4.5 关于“CPU多核设置”与ClickHouse并行度的终极关系最后聊一个比较根本的问题ClickHouse并行查询调优最终目的是什么是把机器上所有CPU核的算力尽可能多地转化为查询吞吐。但这里要明白多核CPU的算力不是“核数×单核频率”这么简单它还受内存带宽、缓存一致性协议NUMA、PCIe带宽等一系列硬约束限制。实际操作中如果有条件在大内存多路服务器上跑ClickHouse建议关注一下NUMA拓扑。跨NUMA节点的内存访问延迟比本地节点高不少ClickHouse虽然做了不少针对NUMA的优化但如果数据分布和节点绑定不合理跨节点访问会拖慢并行查询。可以先用numactl --hardware确认拓扑再考虑是否为ClickHouse进程绑定CPU和内存节点。这一步对大多数云服务器用户不可见因为虚拟化已经屏蔽了NUMA但物理机上效果明显。另外ClickHouse的多核性能很大程度上还依赖编译器优化。官方预编译包为了兼容性通常不会开启针对特定CPU型号的最高级优化。如果你对极致性能有追求并且环境稳定可控可以尝试用-marchnative之类参数自行编译。这一步确实能带来几个百分点的提升但代价是升级部署会变得复杂——而且换机器后可能因为指令集不兼容直接起不来。这个取舍建议慎重。我在实际使用中还有一个体会ClickHouse并行查询的能力就像一把好刀用得好是生产力用不好反而添乱。过度追求单条查询的线程数没有意义真正核心的是理解你的数据结构、查询模式和硬件现状这三者之间的关系然后让ClickHouse的参数去适应它们而不是反过来让业务迁就参数。每动一个参数都应当带着问题去测这个改动到底压低了延迟还是只是让CPU好看地忙起来多跑几轮对比答案自然就清楚了。
返回列表