4、表引擎选择:MergeTree家族引擎对比,针对高频写入场景选择最优引擎(ReplicatedMergeTree vs Distributed)
好,咱们进入第四讲。前面聊了写入链路和分区策略,但有个最基础的问题还没解决——到底该用哪个表引擎?
MergeTree 家族里,大家最纠结的就是 ReplicatedMergeTree 和 Distributed。我见过不少新手,上来就建 Distributed 表,觉得它能自动分片、自动负载均衡,多省心。结果呢?写入一上去,数据丢了、节点挂了、查询还慢。嗯,这里面的坑,我今天一次性给你讲透。
4.1 先搞清楚:它们根本不是同一层的东西
很多人把这两个引擎放在一起比较,其实不太对。我打个比方:
- ReplicatedMergeTree 是「存储引擎」,它管的是数据怎么存、怎么备份、怎么恢复。
- Distributed 是「查询引擎」,它管的是请求怎么路由、怎么把多个分片的结果拼起来。
说白了,Distributed 本身不存数据,它只是个「调度员」。你建一个 Distributed 表,背后必须挂一个本地表(通常是 MergeTree 或 ReplicatedMergeTree)。
核心结论:Distributed 不是 MergeTree 的替代品,而是它的「上层封装」。高频写入场景下,你真正要选的是底层那个本地表引擎。
4.2 ReplicatedMergeTree:高频写入的「定海神针」
我个人习惯,只要集群超过 1 个节点,本地表一律用 ReplicatedMergeTree。为什么?
- 自动副本同步:写入一个节点,ZooKeeper 协调,其他副本自动拉取。你不需要写任何同步代码。
- 故障自动切换:某个副本挂了,写入自动转到其他副本。查询也不受影响。
- 数据零丢失:只要副本数 ≥ 2,单节点宕机不会丢数据。
我在项目中遇到过一件事:有个团队用普通 MergeTree,某天一台机器磁盘坏了,数据全丢。恢复花了两天。从那以后,我所有生产环境都强制要求 ReplicatedMergeTree。
小技巧:建表时指定 zookeeper_path 和 replica_name,可以让你在 ZooKeeper 里一眼看出哪个表、哪个副本。比如:
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/my_table', '{replica}')
这样集群扩容时,新节点自动注册,不用改表结构。
4.3 Distributed:别把它当「写入加速器」
很多人觉得 Distributed 能提高写入速度,其实这是个误解。
Distributed 表写入时,默认是「异步写入」——数据先写到本地队列,后台线程再分发到各个分片。你想想看,如果某个分片挂了,队列里的数据就丢了。而且异步写入的延迟不稳定,有时候几毫秒,有时候几秒。
那什么时候用 Distributed?
- 查询场景:你想查全量数据,但数据分布在多个节点上。Distributed 表帮你自动路由、合并结果。
- 写入负载均衡:你不想手动指定写入哪个分片,让 Distributed 帮你随机或按权重分发。
但注意:高频写入场景下,我建议直接写本地表。为什么?
- 减少一次网络跳转(写 Distributed → 写本地表)
- 避免异步写入的数据丢失风险
- 写入延迟更可控
避坑指南:我曾经有个项目,用 Distributed 表做高频写入,结果某个分片磁盘满了,数据全部积压在队列里,最后队列溢出,丢了几百万条数据。从那以后,我所有高频写入都直接写本地 ReplicatedMergeTree,再用 Distributed 表做查询。
4.4 高频写入场景的「黄金组合」
说了这么多,到底怎么选?我直接给结论:
| 场景 | 本地表引擎 | 查询表引擎 | 说明 |
|---|---|---|---|
| 单节点、不关心高可用 | MergeTree | 直接查本地表 | 开发测试环境可用 |
| 多节点、需要高可用 | ReplicatedMergeTree | Distributed | 生产环境标准配置 |
| 多节点、超高写入吞吐 | ReplicatedMergeTree | Distributed(只读) | 写入直连本地表,查询走 Distributed |
| 多节点、需要自动分片 | ReplicatedMergeTree | Distributed(带 sharding_key) | 注意分片键要均匀 |
说白了,高频写入场景下,ReplicatedMergeTree 是必选项,Distributed 是可选项。你可以在写入端直接连本地表,查询端用 Distributed 表统一访问。
4.5 一张图看懂引擎选择逻辑
我画了张流程图,帮你快速决策:
4.6 实战配置示例
最后,给你一个可以直接用的配置模板。假设你有 3 个节点,每个节点 2 个副本:
-- 在每个节点上创建本地表(ReplicatedMergeTree)
CREATE TABLE my_table_local ON CLUSTER my_cluster (
event_time DateTime,
user_id UInt64,
event_type String,
payload String
) ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/my_table_local',
'{replica}'
)
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_time, user_id);
-- 在任意一个节点上创建分布式表(Distributed)
CREATE TABLE my_table_distributed ON CLUSTER my_cluster
AS my_table_local
ENGINE = Distributed(
'my_cluster',
'default',
'my_table_local',
rand() -- 随机分片,保证数据均匀
);
写入时,我建议你直接写本地表:
-- 写入本地表(推荐)
INSERT INTO my_table_local VALUES (...);
-- 查询时用分布式表
SELECT * FROM my_table_distributed WHERE event_time > '2024-01-01';
个人经验:如果你用 Kafka 或 Flink 写入,可以在写入端做分片逻辑,直接写对应节点的本地表。这样既避免了 Distributed 的异步风险,又实现了负载均衡。我在一个日写入 50 亿条的项目里就是这么干的,稳得很。
好了,这一讲的核心就这些。记住一句话:高频写入,ReplicatedMergeTree 是底线,Distributed 是锦上添花。别搞反了。