先纠正一个说法
ClickHouse 不是“不能并发”,而是不擅长让大量资源密集型 OLAP 查询同时无约束运行。它通过多线程并行加速单条查询,因此单查询性能很强,但高并行度会放大多查询之间的资源竞争。
并发能力必须结合查询重量描述。几百个只读少量数据的查询与几十个扫描数十亿行的聚合,压力完全不同。
OLTP 与 OLAP 的并发模型差异
MySQL 常见请求通过 B+Tree 定位几行,执行时间短、单请求资源较小,因此能够承载大量并发短查询。
ClickHouse 查询通常会:
- 读取多个 Part 和大量压缩数据块。
- 启动多线程读取与表达式计算。
- 构建 GROUP BY 哈希表。
- 执行排序、JOIN 或窗口函数。
- 在分布式集群传输中间结果。
单个请求可能占用多个 CPU 核、数 GB 内存和大量磁盘带宽。当许多此类请求同时执行,瓶颈不是连接数量,而是物理资源。
原因一:单查询会使用多个线程
ClickHouse 通过 max_threads 等配置让一条查询并行读取与计算。假设一条查询使用 16 个线程,20 条并发重查询理论上就可能争用数百个执行线程。
线程数超过 CPU 有效并行能力后,会出现上下文切换、缓存失效和调度开销。降低单查询线程数可以提高可并发查询数量,但单条查询延迟可能上升,这是一种吞吐与延迟取舍。
原因二:聚合和 JOIN 占用大量内存
高基数 GROUP BY 要为大量 Key 保存聚合状态。哈希 JOIN 需要把一侧数据构建到内存。多条查询同时执行时,峰值内存会近似叠加。
达到内存阈值后可能触发外部排序、外部聚合或查询失败。外部化能避免 OOM,却会把压力转移到磁盘,导致整个集群延迟升高。
不能只把 max_memory_usage 调大,因为节点总内存是有限的。单查询允许 20GB、同时进入 20 条查询,就存在远超物理内存的风险。
原因三:磁盘带宽是共享资源
列式扫描即使压缩率高,仍可能读取数十 GB 数据。NVMe 顺序读很快,但多个查询、后台 Merge、Mutation 和 TTL 同时读写会竞争磁盘带宽与 IO 队列。
当磁盘饱和时,增加查询并发不会增加吞吐,只会让每条查询等待更久,最终形成超时和重试风暴。
原因四:后台 Merge 持续消耗资源
MergeTree 写入生成 Part,后台必须合并。Merge 会读取旧 Part、解压、归并、压缩并写出新 Part,消耗 CPU、磁盘和临时空间。
除了 Merge,还有 Mutation、TTL、复制获取和物化视图写入。前台查询和后台任务共享节点资源,高峰写入时查询能力会下降。
原因五:分布式查询会放大请求
一条 Distributed 查询可能向每个分片和副本发送子查询。入口只有 50 条并发,若集群有 20 个分片,底层可能产生大量并发子任务和网络连接。
协调节点还需要汇总、排序或合并各分片结果,容易成为 CPU、内存或网络热点。返回大结果集时,协调节点压力更明显。
原因六:复杂查询重量差异巨大
同一个查询模板使用不同时间范围、租户或过滤条件,读取量可能相差几个数量级。如果没有查询成本治理,一个用户查询全年数据就可能影响所有在线报表。
只限制连接数无法解决问题。需要限制读取行数、字节数、执行时间、内存和线程,并对不同用户实施不同资源策略。
原因七:查询启动存在固定成本
解析、计划、线程调度、打开 Part、读取索引和建立流水线都有固定成本。对于每秒数万次、每次只查一行的请求,这些成本相对数据处理本身过高。
此类请求更适合缓存、MySQL、Redis 或面向点查的存储。把 ClickHouse 暴露为无限制在线明细 API 通常不是好设计。
高并发下的典型表现
- 查询 P99 急剧上升,但单独执行很快。
- CPU 长时间接近满载,上下文切换增加。
- 内存快速增长并频繁触发超限。
- 磁盘利用率和 IO 等待升高。
- Merge 队列、Part 数量和复制队列持续积压。
- 查询超时后客户端自动重试,形成流量放大。
如何提高并发承载能力
1. 减少单查询读取量
优化排序键、强制时间范围、只读取必要列、预聚合热点指标。减少 read_rows/read_bytes 是最根本的办法。
2. 预计算与缓存
高频固定报表使用物化视图和聚合表;相同参数的结果使用缓存。缓存必须包含租户、权限和指标版本,防止越权或旧口径。
3. 设置并发队列
与其让所有查询同时进入并争抢资源,不如限制活跃查询数,剩余请求排队或快速失败。排队能提高总体吞吐稳定性,但要设置最大等待时间,避免无限堆积。
4. 分类资源隔离
在线查询、内部分析和离线任务使用不同用户、Profile、配额或独立集群。限制每类请求的:
max_threadsmax_memory_usagemax_execution_time- 最大读取行数与字节数
- 并发查询数量
离线大查询不应与面向用户的秒级报表争抢同一资源池。
5. 读副本与水平扩展
增加副本可以分散只读流量,增加分片可以分担数据和计算。但如果每条查询仍广播到所有分片,单纯增加分片也会增加协调和网络成本。
扩容前应确认瓶颈是 CPU、IO、内存、热点分片还是单协调节点。
6. 应用层合并请求
页面加载可能同时发起几十个相似指标查询。可以合并为一次查询、批量查询或后端统一聚合,减少重复扫描和连接开销。
7. 背压与禁止无界重试
当集群达到容量时,应返回明确的限流或超时结果。客户端采用指数退避和有限重试。立即重试会把短暂拥塞放大为雪崩。
一个容量估算思路
假设节点有 32 个 CPU 核,一条核心报表使用 8 个线程时耗时 2 秒。理论上同时运行 4 条可占满 CPU,但实际还要为后台 Merge、系统进程和查询波动留余量。
将每条查询线程数降到 4,可能允许更多请求并行,但单条耗时会增长。需要通过压测找到“吞吐最大且 P99 可接受”的点,而不是按 CPU 核数直接套公式。
压测应使用真实数据分布和查询混合,包括热点租户、长时间范围、写入与 Merge 同时发生的情况。
面试案例回答模板
我们曾遇到大促期间报表并发上升,ClickHouse CPU 95%,查询从 2 秒增加到 20 秒。query_log 显示多条查询重复扫描相同明细,每条使用 16 个线程。我们先限制分析用户并发并禁止自动重试止损;随后建立小时聚合表,将在线查询 max_threads 调整为 4,并把离线查询迁移到独立副本。最终读取字节下降约 90%,峰值并发下 P99 稳定在 3 秒左右。
真实面试时应替换为项目中的实际指标和改动,不要编造数字。
核心考点清单
- ClickHouse 高吞吐来自单查询并行,这也会降低同时承载大量重查询的能力。
- 真正瓶颈通常是 CPU、内存、磁盘和网络,而非连接数。
- 后台 Merge、Mutation、TTL 与复制会和前台查询争用资源。
- 分布式查询可能把一个入口请求放大成多个分片子任务。
- 提升并发的首要方式是减少单查询工作量,其次才是限流、隔离和扩容。
- OLAP 并发必须以查询重量和 P99 描述,不能只说 QPS。
高频追问与参考回答
追问 1:降低 max_threads 为什么能提高并发?
它减少单查询占用的 CPU 线程,让更多查询同时获得执行机会;代价是单查询可能变慢,需要通过压测寻找整体吞吐与延迟平衡。
追问 2:增加副本一定能提高并发吗?
只读查询能被路由到不同副本时通常有帮助。但若查询仍广播到所有副本或瓶颈在协调节点、网络和共享存储,收益有限。
追问 3:ClickHouse 能支持多少并发?
没有固定数字。取决于查询读取量、聚合基数、线程数、硬件、后台写入和延迟目标。应通过真实查询混合压测给出安全并发。
追问 4:为什么并发越高总吞吐反而下降?
资源饱和后新增查询只增加上下文切换、缓存失效、IO 排队和内存压力,每条查询都变慢,超时重试还会进一步放大流量。
追问 5:如何区分查询问题还是 Merge 问题?
对照 query_log、系统资源、Part 数量和 Merge 队列时间线。若无查询时 IO 仍高且 Merge 积压,重点检查写入批次和 Part;若特定查询进入后 read_bytes 与内存突增,则优化查询和模型。
追问 6:为什么不用 ClickHouse 直接做高并发明细接口?
单行点查无法充分利用列式扫描和向量化优势,却承担查询启动与资源调度成本。高并发点查通常由 MySQL、Redis 或专门 KV 服务承接更合适。
机制全景图
下面把「ClickHouse 为什么对高并发支持不足?」从输入到结果压缩成一条可复述的主链路。面试时先用图建立全局坐标,再进入局部实现,能避免只背零散结论。
flowchart LR
A["请求进入查询队列"]
A --> B["分配线程与内存"]
B --> C["并行扫描和聚合"]
C --> D["争用磁盘/CPU/网络"]
D --> E["限流或溢写后完成"]
完整链路:从输入到结果
沿着「请求进入查询队列 → 分配线程与内存 → 并行扫描和聚合 → 争用磁盘/CPU/网络 → 限流或溢写后完成」观察输入、状态与输出,下面每个阶段都对应一个可以在源码、日志或系统表中验证的位置。
1. 请求进入查询队列
单个查询会使用多个线程和较大内存,几十个复杂查询可能放大为数百执行线程。
2. 分配线程与内存
并发查询共享 CPU、page cache、磁盘和网络,资源达到饱和后尾延迟非线性上升。
3. 并行扫描和聚合
聚合、排序和 Join 的哈希表按查询独立分配,内存总量不能只看单查询限制。
4. 争用磁盘/CPU/网络
后台 merge、mutation 和复制也消耗 I/O 与 CPU,会与前台读取竞争。
5. 限流或溢写后完成
max_concurrent_queries、用户配额、队列和 workload management 应按业务优先级设置,而不是让所有请求无限进入。
源码与实现定位
| 入口 | 阅读重点 |
|---|---|
| system.processes | 运行查询资源 |
| system.query_log | 峰值内存与排队 |
源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。
参数配置与可复现实验
SELECT user,count(),sum(memory_usage) FROM system.processes GROUP BY user;
逐级增加并发直到 P99 非线性上升,记录拐点作为配额。
验证步骤与预期结果
1. 固定输入和基线
先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「running queries」为主基线,记录值应满足「记录稳态基线」;同时保存 运行/排队查询数、总内存与查询峰值,使后续变化能够回到同一时间轴比较。
2. 从实现入口确认路径
在「system.processes」确认请求确实进入「运行查询资源」对应的实现,再沿「system.query_log」观察「峰值内存与排队」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。
3. 注入本文特有的失败模式
优先复现「只限制单查询内存忽略并发总量」,并把单一变量逐级放大,直到「running queries」越过「超过拐点」。随后再分别验证「merge 与查询争用磁盘未隔离」和「客户端超时后查询仍在服务端运行」,三类故障分开执行,避免多个变量同时变化而无法归因。
4. 执行止损和根因修复
第一轮只应用「按 workload 配额」,确认它能控制影响范围;第二轮应用「客户端超时同步取消」,验证核心链路恢复;最后落实「预聚合重复查询」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。
5. 通过退出条件
实验只有同时满足三项才算通过:「running queries」回到「记录稳态基线」、「running queries P99」回到「同负载可复现」、「结果核对」回到「差异为 0」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。
量化基线
| 指标 | 样例基线/口径 | 风险线 | 结论 |
|---|---|---|---|
| running queries | 记录稳态基线 | 超过拐点 | 资源饱和 |
| running queries P99 | 同负载可复现 | 超过基线 2 倍 | 资源饱和 |
| 结果核对 | 差异为 0 | 任意非零 | 停止切换并修复 |
这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。
事故复盘:BI 用户并发刷新使核心看板超时
每个看板触发十余个重聚合查询,上午整点同时刷新,磁盘和 CPU 饱和。把低优先级探索查询限流、缓存共享结果并对核心看板预聚合后,资源隔离才生效。
| 失败模式 | 首要证据 | 第一处置动作 |
|---|---|---|
| 只限制单查询内存忽略并发总量 | 运行/排队查询数 | 按 workload 配额 |
| merge 与查询争用磁盘未隔离 | 总内存与查询峰值 | 客户端超时同步取消 |
| 客户端超时后查询仍在服务端运行 | 磁盘带宽/IO wait | 预聚合重复查询 |
发布与回滚检查点
- 发布前:确认「system.processes」对应实现和上述配置在目标版本仍然有效,并保存「running queries」基线。
- 灰度中:同时观察 运行/排队查询数、总内存与查询峰值、磁盘带宽/IO wait;任一指标越过表中风险线,就停止继续扩量。
- 回滚时:先执行「按 workload 配额」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
- 发布后:至少覆盖一个完整峰值周期,确认「只限制单查询内存忽略并发总量」没有再次出现,才关闭变更观察窗口。
方案对比与选型
| 方案 | 更适合的场景 | 主要收益 | 代价与边界 |
|---|---|---|---|
| 全局并发限制 | 实例资源统一保护 | 配置简单 | 无法区分业务优先级 |
| 按用户/工作负载配额 | 多租户和核心/离线混部 | 隔离清晰 | 配额设计与动态调整复杂 |
| 预聚合+结果缓存 | 重复查询比例高 | 减少根本计算量 | 数据新鲜度与失效治理 |
选型至少带上 日增量、分区规模、查询并发、扫描行数和压缩比,并用上面的量化基线验证;未知数据应明确为待测假设。
设计边界与工程取舍
ClickHouse 高吞吐依赖让少量查询充分并行,不等于无限并发;并发控制应保护总资源并为核心业务保留预算。
工程落地遵循:以数据布局减少扫描,以批量写入减少小 Part,避免照搬行存思路。回答时直接引用「system.processes」、配置实验和事故数据,比复述固定模板更有说服力。