我对 ClickHouse 部署最强烈的感受是,它逼你放弃“有没有标准架构”这种偷懒问法,转而老老实实回答自己到底想解决哪一类瓶颈。
核心判断
ClickHouse 的部署问题不能只问“单机还是集群”。更准确的问法是:
- 需要的是扩容还是容灾;
- 需要的是更低单机压力还是更低存储成本;
- 需要的是同地域高可用还是跨地域冗余;
- 需要的是热数据高性能还是冷数据低成本长期保留。
官方文档给出的答案不是一套唯一架构,而是一组可组合能力:ReplicatedMergeTree、Distributed、ClickHouse Keeper、storage_policy、对象存储、filesystem cache。我很喜欢这种表达方式,因为它要求使用者自己承担架构判断,而不是期待文档替你选型。
分片与副本不是一回事
clickhouse-replication-and-scaling 让我把这两个常被混说的词真正拆开了。很多部署讨论一开始就糊掉,就是因为扩容和容灾根本不是同一个问题。
- 分片解决的是单机容量、I/O 和并行度上限;
- 副本解决的是节点故障下的数据冗余与服务连续性。
因此,一个比较自然的扩展顺序往往是:
- 单机先跑到接近容量或并发瓶颈;
- 需要横向扩容时增加 shard;
- 需要高可用时再给每个 shard 增加 replica。
先纵向扩展,再横向扩展
clickhouse-13-mistakes 又补上了一个我觉得很重要的判断:ClickHouse 新手经常过早水平扩展。因为 Kubernetes 让“多节点部署”看起来很自然,很多人会直接跳到几十个节点、复杂编排和大量 shard。
但 ClickHouse 本来就很擅长吃满单机资源。过滤、排序、聚合这些分析查询阶段通常可以在单机内并行化,优先纵向扩容往往更便宜、运维面更小,join 等操作也少了跨网络搬数据的成本。
这不是说不要 shard,而是不要把 shard 当成默认起点。更稳的顺序是:
- 先确认单机资源是否真的成为瓶颈;
- 需要高可用时加副本;
- 容量、I/O 或并行度超过单机上限时再加 shard;
- 引入 shard 后,重新评估跨 shard 聚合、Top-N、写入路由和 Keeper 压力。
Keeper、Distributed 与 ON CLUSTER 的角色分工
这套拓扑能工作,不只是因为“多几台机器”,而是因为三个角色分得很清楚:
- Keeper:维护复制与协调状态,生产环境最好独立部署;
Distributed表:承担跨 shard 的查询和写入入口;ON CLUSTER:负责把 DDL 发到整个集群,但不承担 DML 分发。
这是个很重要的边界。很多系统把“集群”想成一个黑箱,而 ClickHouse 明确要求你区分:
- 元数据变更怎么扩散;
- 数据写入怎么路由;
- 副本同步又依赖什么协调层。
Keeper 与 ZooKeeper 的选型边界
clickhouse-keeper 把这个边界写得很清楚:ClickHouse Keeper 与 ZooKeeper 协议兼容,但它不是要成为通用 ZooKeeper 生态的完整替身。
这让我对部署拓扑又多了一层理解:
- 如果协调层只为 ClickHouse 服务,Keeper 往往是更自然的默认项;
- 如果组织已经把 ZooKeeper 作为跨系统共享的基础设施,继续沿用 ZooKeeper 也可能更稳;
- 从 ZooKeeper 迁移到 Keeper 时,应把它当作一次受控切换,而不是“先混着跑再慢慢替换”。
也就是说,协调层选型不是单纯的“技术优劣”问题,而是专用化与通用化之间的运维边界选择。对我来说,这种判断比单点 benchmark 有意义得多。
Operator 下的 replicated 默认值
clickhouse-operator-introduction 让我补上了 Kubernetes 语境里很重要的一层:
- Operator 推荐生产环境使用
Replicated数据库引擎; - 它会帮你同步数据库定义;
- 但表数据复制仍然依赖 ClickHouse 自己的复制机制。
这意味着“用了 Operator”不等于“复制问题已经自动解决”。如果目标是副本级高可用,那么数据库层和表层都要选对 replicated 路径。
oneuptime-replicated-replacingmergetree 还把表层 replicated 的操作面补得更具体:每个副本需要通过 {shard}、{replica} 这类宏落到稳定的 Keeper 路径上,运行后还要通过 system.replicas 观察 absolute_delay、queue_size、parts_to_check 等状态。也就是说,表引擎选择不是 DDL 结束时就完成了,复制队列和合并健康度会持续决定这张表是否真的处在高可用状态。
存算分离是另一条独立维度
clickhouse-separation-storage-compute 说明,ClickHouse 可以把 MergeTree 数据放在 S3 等对象存储上,让计算和存储独立扩缩容。这个决策和“要不要分片”并不是同一个问题:
- 分片主要解决单机处理能力不够;
- 存算分离主要解决数据规模大、冷数据多、对象存储更省钱。
官方还明确提醒,这条路线更复杂,自管成本更高。也就是说,它不是默认升级路径,而是当对象存储的成本优势和弹性价值足够大时才值得引入。
ClickHouse Cloud 把存算分离做成了默认
clickhouse-cloud-architecture 让我看到,Cloud 版并不是”在自管版上加个 UI”,而是把整个底座换成了对象存储:
- 所有服务的数据落在共享对象存储的不同 subpath 上,由 IAM 或独立 bucket 做隔离;
- 计算节点不再长期持有数据,而是按需从对象存储拉取,本地 SSD 只做 cache;
- 计算层可以自动扩缩容,甚至 idle 到零,需要时自动 resume。
这意味着 Cloud 架构下的扩容逻辑和自管集群完全不同。自管集群加 replica 是为了数据冗余和读并发;Cloud 架构里数据冗余已由对象存储保证,加 replica 主要是为了提升查询并发和降低单节点负载。
Compute-Compute Separation
Cloud 架构里我最喜欢的设计是 compute-compute separation:多个计算节点组共享同一份底层对象存储,每个节点组有自己独立的 endpoint 和扩缩容边界。
这让我可以把写入负载和交互式查询负载彻底分开:
- 一个节点组负责批量写入和 ETL;
- 另一个节点组负责面向用户的实时查询;
- 两者看到的数据完全一致,但资源争夺为零。
这比自管集群里”用同一份资源既做写入又做查询”的模型干净得多。代价是你必须接受 ClickHouse Cloud 的托管边界:网络、IAM、计费和版本升级都不再完全由自己控制。
SharedMergeTree:Cloud 架构的引擎基石
clickhouse-shared-merge-tree 让我理解了 ClickHouse Cloud 为什么能做到”无分片 + 数百 replica”——底层引擎已经不是 ReplicatedMergeTree,而是 SharedMergeTree。
核心差异:从”复制”到”共享”
| 维度 | ReplicatedMergeTree | SharedMergeTree |
|---|---|---|
| 数据存储 | 每个 replica 持完整数据副本 | 数据在共享对象存储,所有 replica 共享 |
| 元数据存储 | 每个 replica 本地持有,需要 replica 间复制 | 元数据在 ClickHouse-Keeper,所有 replica 共享 |
| replica 间通信 | 需要直接通信复制数据和元数据 | 不直接通信,通过共享存储和 Keeper 协调 |
| 复制模型 | 同步/异步 leader-follower | 异步 leaderless |
| 扩容速度 | 需要复制数据到新 replica | 新 replica 直接从共享存储读取元数据,秒级 |
| 最大 replica 数 | 受复制开销限制 | 可支持数百个 replica |
这意味着什么?
- 扩容不再是”加 replica 等数据复制”,而是”新 replica 订阅 Keeper 元数据即可”
- 写入性能不再受 replica 间复制带宽限制,只受共享存储吞吐限制
- 一致性不再需要在 N 个 replica 间确认,只需要在 Keeper quorum 间确认
- 用户写
ENGINE = MergeTree时,Cloud 环境会自动转换为SharedMergeTree,完全透明
这也解释了为什么 ClickHouse Cloud 可以支持 compute-compute separation 和动态扩缩容:底层引擎已经为”共享存储 + 无状态计算节点”做好了设计。自管集群即使配置了对象存储,仍然使用 ReplicatedMergeTree,replica 间仍然需要复制,扩容速度天然受限。
Parallel Replicas:无分片架构下的查询并行化
clickhouse-parallel-replicas 解决了一个 Cloud 架构下的核心问题:如果没有 shard,单个查询怎么利用多个 replica?
传统思路是”一个 shard 一个任务”,但 Cloud 架构下所有 replica 数据相同。Parallel replicas 的解法是把 granule 作为最小工作单元:
- 查询到达的节点成为 coordinator;
- coordinator 收集各 replica 的 announcement(当前拥有哪些 parts),避免把任务分配给缺数据的 replica;
- 把 granules 拆成任务集,通过 dynamic coordination 让 replica 主动领取而非一次性派发;
- 用
hash(part + granules) % replica_count保证重复查询能命中同一 replica 的本地 cache; - 如果某个 replica 变慢,其他 replica 可以 steal 它的任务,降低尾延迟。
这套机制让我觉得 ClickHouse Cloud 在”无分片”和”高并行”之间找到了一个务实的平衡点:不需要维护复杂的 shard 路由逻辑,查询仍能横向扩展到多个 replica。
但也有明确的限制需要注意:
- 复杂查询(CTE、JOIN、子查询)效果可能不佳;
- 小查询的协调开销可能抵消并行收益;
- 使用
FINAL或 projections 时不可用; - 必须开启新 analyzer。
也就是说,parallel replicas 不是”开启就能加速所有查询”的开关,而是在 granule 级别有充足并行空间时的加速器。
冷热数据分层本质上是存储策略组合
clickhouse-external-disks-for-storing-data 没把“冷热分层”包装成单独概念,但它给出了足够完整的底层原语。这里的结论有一部分是我基于文档做的归纳:
- 冷层:对象存储,承接大容量、低成本数据;
- 热层:本地磁盘或 filesystem cache,承接会重复访问的工作集;
- 策略绑定:表通过
storage_policy绑定到具体介质组合。
所以,ClickHouse 的冷热分层并不是传统意义上强约束的多层存储产品,更像“把容量层和性能层手工拼起来”的可组合架构。
clickhouse-cold-hot-storage 把这组原语落到了更具体的 Kubernetes 实施面:本地 default disk 作为热层,阿里云 OSS 作为 cold_oss,外面再包一层 cache 类型的 s3_cache,最后用 hot_cold_policy 暴露给表。表侧不是“直接知道 OSS”,而是通过 MODIFY SETTING storage_policy = 'hot_cold_policy' 和基于业务时间字段的 TTL move,把旧 part 迁到 cold volume。
这里我会额外记住三个生产细节。第一,OSS S3 兼容 endpoint 要使用 virtual hosted style,让 bucket 出现在域名里;第二,perform_ttl_move_on_insert=false 可以避免命中 TTL 的历史 part 在 INSERT 路径上直接写入慢冷层;第三,是否真的迁移成功要看 system.disks、system.storage_policies 和 system.parts.disk_name,不能只看 DDL 是否执行成功。
这篇实践笔记里的性能测试也给了一个很朴素的判断:冷数据首次从 OSS 读取明显慢于热盘,但二次查询命中本地 cache 后会接近热数据体感。冷热分层的目标不是让冷层无成本地等同热层,而是在容量成本下降的同时,让被再次访问的冷数据有机会回到可接受速度。
多地域复制是网络预算问题
clickhouse-multi-region-replication 给出一个非常实用的边界:跨地域复制是支持的,但地域间时延应保持在两位数毫秒。否则,写路径会因为共识和复制而明显变慢。
这意味着多地域副本更适合:
- 区域级容灾;
- 较近地域之间的高可用冗余;
- 明确接受更高写延迟的部署。
它不适合把全球多地域都纳入同一个低延迟写集群想象中。
分片决策的具体化
clickhouse-sharding-decision 把我之前的判断框架进一步具体化了。它强调了一个关键区分:
- 加副本是配置参数的变化:
remote_servers里加一行,数据自动同步; - 加分片是架构重构:需要分片键设计、数据重分布、查询路径调整。
在冷热分层已经开启的前提下,分片的唯一合理触发条件是单机处理能力成为明确瓶颈。如果热工作集能被单机内存 cache 覆盖、CPU 和内存远未饱和、写入量不大,那么不分片(1 shard × N replicas)配合 Parallel Replicas 是更简洁、更弹性的选择。
这让我把扩容顺序再收紧一层:冷热分层前置时,先验证对象存储冷层、cache disk、热盘容量和节点 I/O;如果 CPU 或内存先吃紧,优先纵向升配;只有当单 shard 数据量、写入吞吐或扫描压力成为明确瓶颈时,再增加 shard。否则 shard 数加得太早,只是在还没有证明瓶颈前提前支付分布式查询和运维成本。
一个实用的部署判断框架
- 先纵向扩容或加副本:当单机资源仍可提升,或核心诉求是高可用、节点故障自动接管、避免单节点损坏导致服务中断。
- 再加分片:当单机已经扛不住容量、I/O 或并行查询压力。
- 考虑存算分离:当冷数据很多、本地盘成本高、需要把容量层转移到对象存储。
- 加冷热分层:当远端对象存储已经引入,但又需要本地缓存保证热工作集性能。
- 慎用多地域:当跨地域需求是真实存在的容灾需求,而不是模糊的“全球部署更高级”。
云厂商验证会改变 shard 判断
clickhouse-production-v4-tencent-cloud-validation 给我补上了一个更贴近生产的提醒:分片数不是只由全量历史数据决定,也由热冷分层后真正留在本地高性能盘上的工作集决定。
在腾讯云 TKE / CBS / COS 的目标形态里,如果最近一个月热数据只有全量历史的一小部分,2 shards x 2 replicas + 3 Keeper 可能比更激进的 6 x 2 更稳。前者减少了 Distributed 查询 fan-out、PVC 数量、副本队列和 Keeper 元数据复杂度;后者只有在全量历史长期留热盘,或者单 shard 写入 / 查询压力被真实观测证明后,才更像合理选择。
这让我把扩容顺序再收紧一层:冷热分层前置时,先验证对象存储冷层、cache disk、热盘容量和节点 I/O;如果 CPU 或内存先吃紧,优先纵向升配;只有当单 shard 数据量、写入吞吐或扫描压力成为明确瓶颈时,再增加 shard。否则 shard 数加得太早,只是在还没有证明瓶颈前提前支付分布式查询和运维成本。
对我的启发
这些文档把 ClickHouse 从“快的列式数据库”扩展成了一个更完整的系统形象:它不仅会算得快,还要求使用者明确理解拓扑、协调、介质、缓存与网络时延之间的连锁关系。
这也让我意识到,分析数据库的部署优化,和 OLTP 世界那种“主从 + 索引 + 连接池”思路并不一样。这里更像是在拼一台面向分析工作负载的分布式机器。
客户端连接策略:连接串应该指向谁?
这是从架构落到实施时最容易被忽略的问题。核心原则是:客户端通常不需要连接所有节点,而是连接一个入口(LB 或任一可达节点),让服务端负责后续路由和并行化。
无分片全副本架构
在这种架构下:
-
客户端连接策略:
- 方式 A(推荐):通过一个负载均衡器(如 K8s Service、HAProxy、Nginx)作为统一入口,LB 把连接分发到各 replica
- 方式 B:客户端直接配置所有 replica 地址,通过
ConnOpenRoundRobin或ConnOpenRandom做连接级负载均衡 - 不需要客户端自己实现”对多个副本并发查询”,那是 Parallel Replicas 的工作
-
查询并行化发生在服务端:
- 请求到达任一节点后,该节点成为 coordinator
- coordinator 通过 Parallel Replicas 把 granules 作为工作单元分发到所有 replica
- 各 replica 处理完后回传 mergeable state,coordinator 合并结果
-
关键设置(在查询 settings 中开启,不需要客户端特殊代码):
SET enable_parallel_replicas = 1; SET cluster_for_parallel_replicas = 'default'; SET max_parallel_replicas = 4;
多分片多副本架构
在这种架构下:
-
客户端连接策略:
- 通常连接 Distributed 表所在的入口节点(或通过 LB 指向该节点)
- 客户端只需要知道一个入口,不需要知道底层有多少 shard 和 replica
- Distributed 表负责把查询路由到各个 shard,并在每个 shard 里选一个 replica 执行
-
查询路由发生在服务端:
Distributed表把查询拆成 M 个子查询(M = shard 数)- 每个子查询被发送到对应 shard
- 每个 shard 只选一个 replica 执行(默认优先本地 replica,可通过
load_balancing调整策略) - 各 shard 返回结果后,
Distributed表做最终聚合
-
写入路由:
- 写入必须走
Distributed表 Distributed表根据分片键(sharding key)计算数据属于哪个 shard- 数据被路由到对应 shard 的一个 replica,再由复制机制同步到同 shard 的其他 replica
- 写入必须走
两种架构的连接策略对比
| 维度 | 无分片(1 shard × N replicas) | 多分片(M shards × N replicas) |
|---|---|---|
| 客户端连接目标 | 所有 replica(或 LB 入口) | Distributed 表入口(或 LB) |
| 客户端职责 | 轻。LB 或客户端做连接分发 | 更轻。只需要知道一个入口 |
| 查询并行化 | Parallel Replicas(granule 级) | Distributed 表跨 shard + 每 shard 内 Parallel Replicas(granule 级) |
| 是否需要 Distributed 表 | 不需要 | 必须 |
| 故障转移 | 客户端/ LB 自动处理 | 入口节点故障需 LB 切换 |
| 扩展性 | 加 replica 时客户端需更新 Addr | 加 shard 时客户端无需变更 |
这让我意识到,“连接串指向谁”不是一个孤立的技术问题,而是服务端拓扑决策在客户端的映射:
- 全副本架构下,客户端需要分散连接(轮询/随机/LB)让查询到达不同节点,再由 Parallel Replicas 在服务端做查询内并行
ClickHouse Cloud 的特殊情况
以上分析主要针对自管集群。ClickHouse Cloud 在客户端连接策略上有本质不同:
Cloud 只需要一个 endpoint:
// Cloud 连接方式:只连一个 Cloud endpoint
Addr: []string{"xxx.asia-northeast1.gcp.clickhouse.cloud:9440"}为什么 Cloud 可以只连一个?
- Cloud 内部有托管负载均衡器:你连接的 endpoint 不是具体节点,而是 Cloud 的 LB 入口,Cloud 自动把连接分发到可用计算节点
- SharedMergeTree 引擎:计算节点是无状态的,数据在共享对象存储,新节点秒级上线即可 serving 查询
- 扩容对客户端完全透明:加/减 replica 时不需要更新客户端配置,Cloud 内部自动处理
这与自管集群的区别:
| 维度 | 自管集群 | ClickHouse Cloud |
|---|---|---|
| 客户端连接 | 多地址(或外部 LB) | 单 endpoint |
| 负载均衡 | 客户端驱动或外部 LB | Cloud 内部托管 |
| 节点发现 | 客户端/ LB 需感知所有节点 | Cloud 自动处理 |
| 扩容影响 | 需更新客户端配置或 LB 后端 | 对客户端完全透明 |
| 节点状态 | 有状态(本地持数据) | 无状态(共享存储) |
核心原则:自管集群需要客户端或外部 LB 来分散连接到多个 replica;Cloud 把这些都内置了,客户端只需要连到 Cloud 提供的入口。
节点故障时的客户端行为
这是一个实际生产中一定会遇到的问题。客户端配置了多个节点地址,如果某个节点挂掉但客户端未及时更新配置,会发生什么?
查询场景:
| 连接策略 | 节点故障影响 | 恢复行为 |
|---|---|---|
ConnOpenInOrder(默认) | 先尝试故障节点,连接超时后 fallback 到下一个节点 | 每次查询增加一次连接超时延迟(默认 30s) |
ConnOpenRoundRobin | 轮询到故障节点时连接失败,查询报错 | 需要应用层重试或连接池自动重连 |
ConnOpenRandom | 随机选中故障节点时连接失败,查询报错 | 需要应用层重试 |
关键细节:
ConnOpenInOrder的故障转移是连接级别的,不是查询级别的。如果连接池里有存活连接,查询会正常执行;只有当需要新建连接时才触发故障转移。ConnMaxLifetime默认 1 小时意味着连接会定期重建,因此故障节点的影响会持续存在直到被移出配置或恢复。- 如果查询已经建立连接后在执行过程中节点挂掉(如 coordinator 在执行 Parallel Replicas 时崩溃),查询会直接失败,需要应用层重试。
- Parallel Replicas 内部有容错:如果某个 worker replica 挂掉,coordinator 会通过 announcement 发现并在其他 replica 上重试;但如果 coordinator 本身挂掉,整个查询失败。
写入场景:
在无分片全副本架构下,写入通常直接打到某个 replica(不走 Distributed 表):
- 如果写入目标节点挂掉:写入失败,需要重试到另一个节点
- 如果写入成功但复制过程中其他 replica 挂掉:数据会在挂掉节点恢复后通过复制队列补齐(
ReplicatedMergeTree的保障)
生产建议:
- 优先用负载均衡器(LB)代替客户端多地址配置:LB 可以做健康检查,自动剔除故障节点,客户端只需要配一个 LB 地址
- 如果必须用客户端多地址:
- 生产环境不要用
ConnOpenInOrder(故障节点排前面会导致每次查询都先超时) - 用
ConnOpenRoundRobin或ConnOpenRandom,配合应用层重试 - 设置合理的
DialTimeout(不要默认 30s,建议 5-10s)
- 生产环境不要用
- 监控
system.replicas:关注absolute_delay、queue_size,及时发现复制延迟或 replica 失联
- 分片架构下,客户端需要收敛连接到一个入口,由 Distributed 表在服务端做跨分片路由
两种架构的负载均衡层次完全不同:全副本是”连接层分散 + 查询层并行”,分片是”连接层收敛 + 服务端路由”。
客户端配置与拓扑决策的耦合
clickhouse-go-configuration 让我意识到,服务端选择了全副本架构后,客户端的连接策略也需要相应调整:
- 全副本架构下,客户端的
Addr应填入所有 replica 地址,通过ConnOpenStrategy(轮询或随机)实现查询负载均衡; ConnMaxLifetime默认值 1 小时:在节点动态扩缩容时可能导致连接分布不均,需要监控;- 协议选择(TCP vs HTTP):不仅影响压缩选项,还影响 session 语义和认证方式。
这让我把”客户端配置”也纳入了部署拓扑判断框架:服务端选择全副本架构后,客户端的连接策略、连接池大小和协议选择都需要相应调整。
来源:clickhouse-manage-and-deploy · clickhouse-replication-and-scaling · clickhouse-separation-storage-compute · clickhouse-external-disks-for-storing-data · clickhouse-cold-hot-storage · clickhouse-multi-region-replication · clickhouse-keeper · clickhouse-operator-introduction · clickhouse-13-mistakes · oneuptime-replicated-replacingmergetree · clickhouse-production-v4-tencent-cloud-validation · clickhouse-cloud-architecture · clickhouse-parallel-replicas
相关页面:clickhouse-keeper-vs-zookeeper · clickhouse-replicated-engines-and-conversion · clickhouse-common-pitfalls · clickhouse-production-migration · clickhouse-sharding-decision · clickhouse · clickhouse-keeper · zookeeper · clickhouse-manage-and-deploy · clickhouse-replication-and-scaling · clickhouse-separation-storage-compute · clickhouse-external-disks-for-storing-data · clickhouse-cold-hot-storage · clickhouse-multi-region-replication · clickhouse-keeper · clickhouse-operator-introduction · clickhouse-13-mistakes · oneuptime-replicated-replacingmergetree · clickhouse-production-v4-tencent-cloud-validation · clickhouse-go-configuration · clickhouse-shared-merge-tree