4、表引擎选择:MergeTree家族引擎对比,针对高频写入场景选择最优引擎(ReplicatedMergeTree vs Distributed)

好,咱们进入第四讲。前面聊了写入链路和分区策略,但有个最基础的问题还没解决——到底该用哪个表引擎?

MergeTree 家族里,大家最纠结的就是 ReplicatedMergeTreeDistributed。我见过不少新手,上来就建 Distributed 表,觉得它能自动分片、自动负载均衡,多省心。结果呢?写入一上去,数据丢了、节点挂了、查询还慢。嗯,这里面的坑,我今天一次性给你讲透。

4.1 先搞清楚:它们根本不是同一层的东西

很多人把这两个引擎放在一起比较,其实不太对。我打个比方:

  • ReplicatedMergeTree 是「存储引擎」,它管的是数据怎么存、怎么备份、怎么恢复。
  • Distributed 是「查询引擎」,它管的是请求怎么路由、怎么把多个分片的结果拼起来。

说白了,Distributed 本身不存数据,它只是个「调度员」。你建一个 Distributed 表,背后必须挂一个本地表(通常是 MergeTree 或 ReplicatedMergeTree)。

核心结论:Distributed 不是 MergeTree 的替代品,而是它的「上层封装」。高频写入场景下,你真正要选的是底层那个本地表引擎。

4.2 ReplicatedMergeTree:高频写入的「定海神针」

我个人习惯,只要集群超过 1 个节点,本地表一律用 ReplicatedMergeTree。为什么?

  • 自动副本同步:写入一个节点,ZooKeeper 协调,其他副本自动拉取。你不需要写任何同步代码。
  • 故障自动切换:某个副本挂了,写入自动转到其他副本。查询也不受影响。
  • 数据零丢失:只要副本数 ≥ 2,单节点宕机不会丢数据。

我在项目中遇到过一件事:有个团队用普通 MergeTree,某天一台机器磁盘坏了,数据全丢。恢复花了两天。从那以后,我所有生产环境都强制要求 ReplicatedMergeTree。

小技巧:建表时指定 zookeeper_pathreplica_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 一张图看懂引擎选择逻辑

我画了张流程图,帮你快速决策:

高频写入场景:表引擎选择决策树 开始:高频写入场景 单节点? MergeTree 开发/测试环境 需要高可用? ReplicatedMergeTree 生产环境标准配置 超高写入吞吐? 写入直连本地表 查询走 Distributed ReplicatedMergeTree + Distributed(读写分离) 核心原则:ReplicatedMergeTree 保证数据可靠,Distributed 简化查询 高频写入场景,优先保证写入路径最短、最可靠 推荐方案 进阶方案 决策节点

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 是锦上添花。别搞反了。