2. 集群架构设计

好,咱们进入第二章。说实话,很多刚接触 ClickHouse 的朋友,第一反应是“这玩意儿单机跑得挺快啊,为什么要搞集群?” 嗯,等你数据量到了几十 TB,或者查询并发一上来,单机那点资源根本扛不住。我见过不少团队,单机跑得好好的,一上生产就崩,就是因为没提前想清楚架构。

这一章,咱们就把集群的底裤扒干净。从分布式架构怎么拆,到分片和副本到底怎么配,再到 MergeTree 家族那些引擎各自该用在什么场景。你把这些搞明白了,后面调优才有方向。

2.1 分布式架构的核心逻辑

ClickHouse 的分布式架构,说白了就是“分而治之”。它不像有些数据库那样搞主从复制那一套,而是把数据水平切碎,分散到多台机器上。每台机器只管自己那一亩三分地,查询的时候再合并结果。

我个人习惯把这种架构拆成三个角色来看:

  • 协调节点:负责接收 SQL,解析后分发到各个分片。它本身不存数据。
  • 数据节点:真正存数据的机器,每个节点上可以跑多个分片。
  • 分布式表:一个逻辑上的“壳”,通过 Distributed 引擎把多个本地表映射成一张表。

关键点:分布式表本身不存数据,它只是一个路由层。你往分布式表写数据,它根据分片键把数据打到对应的本地表上。

举个例子,你建一张分布式表:

CREATE TABLE my_table_distributed AS my_table_local
ENGINE = Distributed('cluster_name', 'default', 'my_table_local', rand());

这里的 rand() 就是分片键。每次写入,ClickHouse 对 rand() 取模,决定数据落到哪个分片。当然,你也可以用 cityHash64(id) 这种更均匀的算法。

我的经验:分片键别选那种有明显热点的字段。比如按用户 ID 分片,如果某个大客户的数据量是别人的 100 倍,那这个分片就会成为瓶颈。我一般用 cityHash64(user_id) % shard_count,或者干脆用 rand()。

2.2 分片与副本机制

分片解决的是“数据太多,一台机器装不下”的问题。副本解决的是“机器挂了,数据不丢”的问题。两者是正交的,可以组合使用。

2.2.1 分片策略

ClickHouse 支持两种分片方式:

分片方式 说明 适用场景
静态分片 在配置文件中写死分片数量和数据分布 集群规模稳定,不经常扩缩容
动态分片 通过 Distributed 引擎的 sharding_key 实时计算 需要灵活调整分片,或者数据量波动大

静态分片的好处是性能好,因为路由规则是固定的。坏处是加机器麻烦,得重新分布数据。动态分片灵活,但每次写入都要计算一次哈希,性能略低。

避坑指南:我曾经遇到一个案例,团队用了 rand() 做分片键,结果因为写入量太大,某些分片的数据倾斜严重。后来改成 cityHash64(event_id),才把数据分布均匀。记住,分片键的均匀性直接影响集群的整体吞吐。

2.2.2 副本机制

副本通过 ReplicatedMergeTree 引擎实现。每个分片可以有多个副本,副本之间通过 ZooKeeper 协调数据同步。

副本的核心流程是这样的:

  1. 数据写入主副本(leader)。
  2. 主副本把数据写入本地,同时记录到 ZooKeeper 的日志里。
  3. 其他副本从 ZooKeeper 拉取日志,在本地重放。

这里有个关键点:副本之间是异步同步的。也就是说,写入主副本成功后,其他副本可能还没拿到数据。如果你读的是从副本,可能会读到旧数据。

解决方案:读请求尽量走主副本,或者设置 insert_quorum 参数,等至少一个从副本确认后再返回成功。我一般设 insert_quorum=2,保证至少两个副本有数据。

2.2.3 分片与副本的典型部署

我画了一张图,帮你理解分片和副本怎么组合:

分片与副本组合架构 分片1 (Shard 1) 副本 A 副本 B 分片2 (Shard 2) 副本 A 副本 B 分片3 (Shard 3) 副本 A 副本 B ZooKeeper 集群 说明: 每个分片有 2 个副本,副本之间通过 ZooKeeper 同步数据。 写入时,数据根据分片键路由到对应分片的主副本,再异步同步到从副本。

你看,每个分片有 2 个副本,副本之间通过 ZooKeeper 协调。这样即使一台机器挂了,其他副本还能提供服务。

2.3 MergeTree 家族引擎概览

MergeTree 是 ClickHouse 的“心脏”。几乎所有高性能查询都离不开它。但 MergeTree 不是只有一个引擎,而是一个家族。我按使用频率给你排个序:

引擎名称 核心特点 典型场景
MergeTree 基础版,支持主键排序、分区、TTL 日志、监控、时序数据
ReplicatedMergeTree 带副本的 MergeTree,依赖 ZooKeeper 生产环境,需要高可用
SummingMergeTree 自动合并相同主键的数值列,做预聚合 报表、统计、汇总查询
AggregatingMergeTree 存储聚合函数的状态,支持增量聚合 复杂聚合、物化视图
CollapsingMergeTree 通过 sign 字段标记删除/更新,实现“软删除” 需要更新/删除的场景
VersionedCollapsingMergeTree 带版本号的 CollapsingMergeTree,解决乱序问题 实时更新、数据修正
ReplacingMergeTree 按主键去重,保留最后一条 去重、数据清洗

2.3.1 MergeTree 的核心机制

不管哪个变种,底层都依赖 MergeTree 的三大机制:

  • 分区:按日期或其他字段把数据切分成目录,查询时只扫描相关分区。
  • 排序键:数据在分区内按指定列排序,加速范围查询。
  • 合并:后台线程定期把小数据块合并成大块,减少文件数量。

我举个例子,你建一张表:

CREATE TABLE events (
    event_date Date,
    event_id UInt64,
    event_type String,
    value Float64
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, event_id);

这里 PARTITION BY toYYYYMM(event_date) 表示按月分区。ORDER BY (event_date, event_id) 是排序键。查询时如果带上 event_date 范围,ClickHouse 能直接跳过不相关的分区。

我的习惯:排序键不要选太多列,一般 2-3 列就够了。列多了,排序的开销会变大,而且索引文件也会膨胀。我通常把最常用的过滤条件放在排序键的第一位。

2.3.2 选型建议

很多新手会问:“我到底该用哪个 MergeTree 引擎?” 我的建议很简单:

  • 如果只是存原始数据,不做任何预聚合,用 MergeTree 或 ReplicatedMergeTree。
  • 如果需要做报表,数据量又大,用 SummingMergeTree 提前聚合。
  • 如果数据有重复,需要去重,用 ReplacingMergeTree。
  • 如果业务有更新/删除需求,用 CollapsingMergeTree 或 VersionedCollapsingMergeTree。

避坑指南:我曾经在一个项目里用了 SummingMergeTree,结果发现聚合结果不对。后来排查才发现,SummingMergeTree 只在合并时做聚合,而合并是异步的。如果你需要实时聚合,得配合 AggregatingMergeTree 和物化视图一起用。

2.4 本章小结

这一章咱们把集群架构的底子打好了。分布式架构的核心是“分而治之”,分片解决容量问题,副本解决高可用问题。MergeTree 家族提供了丰富的引擎选择,但别贪多,先把 MergeTree 和 ReplicatedMergeTree 用熟,再根据场景选其他变种。