我对 ClickHouse 部署最强烈的感受是,它逼你放弃“有没有标准架构”这种偷懒问法,转而老老实实回答自己到底想解决哪一类瓶颈。

核心判断

ClickHouse 的部署问题不能只问“单机还是集群”。更准确的问法是:

  • 需要的是扩容还是容灾
  • 需要的是更低单机压力还是更低存储成本
  • 需要的是同地域高可用还是跨地域冗余
  • 需要的是热数据高性能还是冷数据低成本长期保留

官方文档给出的答案不是一套唯一架构,而是一组可组合能力:ReplicatedMergeTreeDistributed、ClickHouse Keeper、storage_policy、对象存储、filesystem cache。我很喜欢这种表达方式,因为它要求使用者自己承担架构判断,而不是期待文档替你选型。

分片与副本不是一回事

clickhouse-replication-and-scaling 让我把这两个常被混说的词真正拆开了。很多部署讨论一开始就糊掉,就是因为扩容和容灾根本不是同一个问题。

  • 分片解决的是单机容量、I/O 和并行度上限;
  • 副本解决的是节点故障下的数据冗余与服务连续性。

因此,一个比较自然的扩展顺序往往是:

  1. 单机先跑到接近容量或并发瓶颈;
  2. 需要横向扩容时增加 shard;
  3. 需要高可用时再给每个 shard 增加 replica。

先纵向扩展,再横向扩展

clickhouse-13-mistakes 又补上了一个我觉得很重要的判断:ClickHouse 新手经常过早水平扩展。因为 Kubernetes 让“多节点部署”看起来很自然,很多人会直接跳到几十个节点、复杂编排和大量 shard。

但 ClickHouse 本来就很擅长吃满单机资源。过滤、排序、聚合这些分析查询阶段通常可以在单机内并行化,优先纵向扩容往往更便宜、运维面更小,join 等操作也少了跨网络搬数据的成本。

这不是说不要 shard,而是不要把 shard 当成默认起点。更稳的顺序是:

  1. 先确认单机资源是否真的成为瓶颈;
  2. 需要高可用时加副本;
  3. 容量、I/O 或并行度超过单机上限时再加 shard;
  4. 引入 shard 后,重新评估跨 shard 聚合、Top-N、写入路由和 Keeper 压力。

Keeper、DistributedON 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_delayqueue_sizeparts_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。

核心差异:从”复制”到”共享”

维度ReplicatedMergeTreeSharedMergeTree
数据存储每个 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 作为最小工作单元

  1. 查询到达的节点成为 coordinator;
  2. coordinator 收集各 replica 的 announcement(当前拥有哪些 parts),避免把任务分配给缺数据的 replica;
  3. 把 granules 拆成任务集,通过 dynamic coordination 让 replica 主动领取而非一次性派发;
  4. hash(part + granules) % replica_count 保证重复查询能命中同一 replica 的本地 cache;
  5. 如果某个 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.diskssystem.storage_policiessystem.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 或任一可达节点),让服务端负责后续路由和并行化。

无分片全副本架构

在这种架构下:

  1. 客户端连接策略

    • 方式 A(推荐):通过一个负载均衡器(如 K8s Service、HAProxy、Nginx)作为统一入口,LB 把连接分发到各 replica
    • 方式 B:客户端直接配置所有 replica 地址,通过 ConnOpenRoundRobinConnOpenRandom 做连接级负载均衡
    • 不需要客户端自己实现”对多个副本并发查询”,那是 Parallel Replicas 的工作
  2. 查询并行化发生在服务端

    • 请求到达任一节点后,该节点成为 coordinator
    • coordinator 通过 Parallel Replicas 把 granules 作为工作单元分发到所有 replica
    • 各 replica 处理完后回传 mergeable state,coordinator 合并结果
  3. 关键设置(在查询 settings 中开启,不需要客户端特殊代码):

    SET enable_parallel_replicas = 1;
    SET cluster_for_parallel_replicas = 'default';
    SET max_parallel_replicas = 4;

多分片多副本架构

在这种架构下:

  1. 客户端连接策略

    • 通常连接 Distributed 表所在的入口节点(或通过 LB 指向该节点)
    • 客户端只需要知道一个入口,不需要知道底层有多少 shard 和 replica
    • Distributed 表负责把查询路由到各个 shard,并在每个 shard 里选一个 replica 执行
  2. 查询路由发生在服务端

    • Distributed 表把查询拆成 M 个子查询(M = shard 数)
    • 每个子查询被发送到对应 shard
    • 每个 shard 只选一个 replica 执行(默认优先本地 replica,可通过 load_balancing 调整策略)
    • 各 shard 返回结果后,Distributed 表做最终聚合
  3. 写入路由

    • 写入必须走 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 可以只连一个?

  1. Cloud 内部有托管负载均衡器:你连接的 endpoint 不是具体节点,而是 Cloud 的 LB 入口,Cloud 自动把连接分发到可用计算节点
  2. SharedMergeTree 引擎:计算节点是无状态的,数据在共享对象存储,新节点秒级上线即可 serving 查询
  3. 扩容对客户端完全透明:加/减 replica 时不需要更新客户端配置,Cloud 内部自动处理

这与自管集群的区别:

维度自管集群ClickHouse Cloud
客户端连接多地址(或外部 LB)单 endpoint
负载均衡客户端驱动或外部 LBCloud 内部托管
节点发现客户端/ 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 的保障)

生产建议:

  1. 优先用负载均衡器(LB)代替客户端多地址配置:LB 可以做健康检查,自动剔除故障节点,客户端只需要配一个 LB 地址
  2. 如果必须用客户端多地址
    • 生产环境不要用 ConnOpenInOrder(故障节点排前面会导致每次查询都先超时)
    • ConnOpenRoundRobinConnOpenRandom,配合应用层重试
    • 设置合理的 DialTimeout(不要默认 30s,建议 5-10s)
  3. 监控 system.replicas:关注 absolute_delayqueue_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