4. 分布式集群架构

在单机 ClickHouse 中,数据写入本地表、查询本地表,一切都很直接。但行情场景的数据量往往从每天数亿行起步,单机磁盘、CPU 和内存很快成为瓶颈,同时任何一次宕机都可能导致行情数据断档。分布式集群要解决的核心问题只有两个:把数据横向拆开(分片),以及让每份数据有冗余(副本)。理解分片、副本、集群拓扑与协调机制,是设计高可用行情集群的地基。

一、核心原理:分片与副本是两件正交的事

分片(Shard)解决的是容量与吞吐:把一张逻辑上的大表按规则切成若干份,分散到不同节点,每份只保存一部分数据。副本(Replica)解决的是可用性与读扩展:同一份分片数据在多个节点上保存多份,互为备份。二者是正交维度——一个集群可以只有分片没有副本(容量大但无冗余),也可以只有副本没有分片(高可用但容量受限),行情生产集群通常是“多分片 × 每分片多副本”。

ClickHouse 的分布式能力由两层表结构承载:本地表(Local Table)是真正存储数据的表,建在每个分片节点上,通常使用 ReplicatedMergeTree 引擎;分布式表(Distributed Table)本身不存数据,只是一张“路由视图”,它通过集群配置知道有哪些分片、每个分片有哪些副本,写入时按分片键转发,查询时向所有分片下发子查询并汇总结果。可以这样记:本地表是“仓库”,分布式表是“调度台”。

行情集群:2 分片 × 2 副本拓扑 分布式表 market_dist 只做路由,不存数据 Shard 1(shard_key 取模=0) 分片键:symbol Shard 2(shard_key 取模=1) 分片键:symbol 副本 1-1 ReplicatedMergeTree 副本 1-2 ReplicatedMergeTree 副本 2-1 ReplicatedMergeTree 副本 2-2 ReplicatedMergeTree ClickHouse Keeper / ZooKeeper 存副本元数据、选主、协调复制 /clickhouse/tables/{shard}/market

二、集群拓扑与配置:分片、副本如何被描述

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。

对比项ZooKeeperClickHouse Keeper
实现语言JavaC++
部署形态独立 JVM 进程独立进程或内嵌
资源占用较高较低
协议兼容原生兼容 ZK 协议
推荐场景存量集群新建集群首选
协调服务本身也要高可用:通常部署 3 个 Keeper 节点组成仲裁集群,避免单点导致所有副本表无法写入。

四、场景与误区复盘

以行情写入为例:采集程序写入分布式表 market_dist,分片键用 cityHash64(symbol),保证同一支股票的数据落在同一分片,便于按 symbol 聚合查询。每个分片两个副本,任一节点宕机,写入自动切到同分片另一副本,查询仍可从存活副本读取。

常见误区有三类:

  • 把分布式表当存储表:直接对分布式表做 ALTER 或依赖其本地数据,实际上它不存数据,DDL 需用 ON CLUSTER 下发到本地表。
  • 分片键选择随意:用 rand() 写入会导致同一 symbol 分散到多个分片,按 symbol 查询时需跨分片聚合,性能下降。
  • 忽略协调服务:Keeper 集群故障时,副本表会进入只读状态,写入报 Replica is not ready,必须把 Keeper 纳入监控。
副本数不是越多越好。每增加一个副本,写入需同步的日志和网络开销都会上升。行情集群通常每分片 2 副本即可满足高可用,追求读扩展时可增至 3 副本。

掌握分片与副本的正交关系、本地表与分布式表的分工、以及 Keeper 的协调角色,才能在后面对行情集群做容量规划、故障切换和性能调优时有的放矢。