2、集群架构设计:分布式架构原理、分片与副本机制、MergeTree家族引擎概览
好,咱们进入第二章。说实话,很多刚接触 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 协调数据同步。
副本的核心流程是这样的:
- 数据写入主副本(leader)。
- 主副本把数据写入本地,同时记录到 ZooKeeper 的日志里。
- 其他副本从 ZooKeeper 拉取日志,在本地重放。
这里有个关键点:副本之间是异步同步的。也就是说,写入主副本成功后,其他副本可能还没拿到数据。如果你读的是从副本,可能会读到旧数据。
解决方案:读请求尽量走主副本,或者设置 insert_quorum 参数,等至少一个从副本确认后再返回成功。我一般设 insert_quorum=2,保证至少两个副本有数据。
2.2.3 分片与副本的典型部署
我画了一张图,帮你理解分片和副本怎么组合:
你看,每个分片有 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 用熟,再根据场景选其他变种。