4. 分布式集群架构
在单机 ClickHouse 中,数据写入本地表、查询本地表,一切都很直接。但行情场景的数据量往往从每天数亿行起步,单机磁盘、CPU 和内存很快成为瓶颈,同时任何一次宕机都可能导致行情数据断档。分布式集群要解决的核心问题只有两个:把数据横向拆开(分片),以及让每份数据有冗余(副本)。理解分片、副本、集群拓扑与协调机制,是设计高可用行情集群的地基。
一、核心原理:分片与副本是两件正交的事
分片(Shard)解决的是容量与吞吐:把一张逻辑上的大表按规则切成若干份,分散到不同节点,每份只保存一部分数据。副本(Replica)解决的是可用性与读扩展:同一份分片数据在多个节点上保存多份,互为备份。二者是正交维度——一个集群可以只有分片没有副本(容量大但无冗余),也可以只有副本没有分片(高可用但容量受限),行情生产集群通常是“多分片 × 每分片多副本”。
ClickHouse 的分布式能力由两层表结构承载:本地表(Local Table)是真正存储数据的表,建在每个分片节点上,通常使用 ReplicatedMergeTree 引擎;分布式表(Distributed Table)本身不存数据,只是一张“路由视图”,它通过集群配置知道有哪些分片、每个分片有哪些副本,写入时按分片键转发,查询时向所有分片下发子查询并汇总结果。可以这样记:本地表是“仓库”,分布式表是“调度台”。
二、集群拓扑与配置:分片、副本如何被描述
ClickHouse 通过配置文件中的 remote_servers 描述集群拓扑。每个 shard 下可以有多个 replica,每个 replica 指向一个节点的 host 与端口。分布式表在创建时引用这个集群名,从而知道数据该往哪里写、查询该往哪里发。
<remote_servers>
<market_cluster>
<shard>
<replica>
<host>ch-shard1-rep1</host>
<port>9000</port>
</replica>
<replica>
<host>ch-shard1-rep2</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>ch-shard2-rep1</host>
<port>9000</port>
</replica>
<replica>
<host>ch-shard2-rep2</host>
<port>9000</port>
</replica>
</shard>
</market_cluster>
</remote_servers>
本地表使用 ReplicatedMergeTree,需要指定 ZooKeeper/Keeper 路径和副本名。同一分片内的两个副本必须使用相同的 {shard} 路径、不同的 {replica} 名,否则会被识别为不同分片而无法复制。
CREATE TABLE market_local ON CLUSTER market_cluster
(
symbol String,
ts DateTime64(3),
price Decimal(18,4),
volume UInt64
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/market_local',
'{replica}'
)
PARTITION BY toYYYYMMDD(ts)
ORDER BY (symbol, ts);
分布式表则基于本地表创建,写入时按分片键路由:
CREATE TABLE market_dist ON CLUSTER market_cluster
AS market_local
ENGINE = Distributed(
market_cluster,
default,
market_local,
rand() -- 或 cityHash64(symbol)
);
三、协调机制:ZooKeeper 与 ClickHouse Keeper
副本之间如何知道对方写入了哪些数据块?靠的是协调服务。每个 ReplicatedMergeTree 表在协调服务上有一个路径,路径下维护着复制日志、副本状态、选主信息等。副本写入时先在协调服务登记一个日志条目,其他副本监听到后拉取并执行,从而实现最终一致。
传统方案使用 ZooKeeper,但它依赖 JVM、运维复杂、资源占用高。ClickHouse Keeper 是官方用 C++ 重写的兼容实现,协议与 ZooKeeper 兼容,但更轻量、延迟更低,且可直接嵌入 ClickHouse 进程或独立部署。新集群建议直接使用 Keeper。
| 对比项 | ZooKeeper | ClickHouse Keeper |
|---|---|---|
| 实现语言 | Java | C++ |
| 部署形态 | 独立 JVM 进程 | 独立进程或内嵌 |
| 资源占用 | 较高 | 较低 |
| 协议兼容 | 原生 | 兼容 ZK 协议 |
| 推荐场景 | 存量集群 | 新建集群首选 |
四、场景与误区复盘
以行情写入为例:采集程序写入分布式表 market_dist,分片键用 cityHash64(symbol),保证同一支股票的数据落在同一分片,便于按 symbol 聚合查询。每个分片两个副本,任一节点宕机,写入自动切到同分片另一副本,查询仍可从存活副本读取。
常见误区有三类:
- 把分布式表当存储表:直接对分布式表做
ALTER或依赖其本地数据,实际上它不存数据,DDL 需用ON CLUSTER下发到本地表。 - 分片键选择随意:用
rand()写入会导致同一 symbol 分散到多个分片,按 symbol 查询时需跨分片聚合,性能下降。 - 忽略协调服务:Keeper 集群故障时,副本表会进入只读状态,写入报
Replica is not ready,必须把 Keeper 纳入监控。
掌握分片与副本的正交关系、本地表与分布式表的分工、以及 Keeper 的协调角色,才能在后面对行情集群做容量规划、故障切换和性能调优时有的放矢。