流烟默头像

ClickHouse 集群实战:分片、副本、本地表与分布式表的完整梳理

ClickHouse 集群实战:分片、副本、本地表与分布式表的完整梳理

一次把 ClickHouse 集群里最让人晕的几个概念讲清楚:分片到底是逻辑概念还是物理存在?分片键什么时候生效?写本地表和写分布式表到底选哪个?"全分片聚合"的查询真的会慢吗?


一、从一次困惑说起

项目里用到 ClickHouse 集群,架构约定是:

  • 写入:应用直连本地表(经 LB 端口)
  • 读取:应用查分布式表
  • 规范:INSERT 全部打本地表,SELECT 全部走 dis_ 前缀的分布式表

这套规范本身没问题,但围绕着它,冒出了一连串让人绕不清的问题:

  • 我写入本地表时,落到了哪个分片?分片数量是多少?
  • 分布式表 DDL 里那个"分片键"(rand() / cityHash64(product_id))是什么?是不是在"创建 64 个分片"?
  • 既然分片键不参与写入,写本地表会不会导致数据倾斜?
  • 查询走分布式表"全分片聚合",3 个分片岂不是要扫 3 倍数据,不会慢吗?

这篇博客把这些问题一次梳理清楚,也把踩过的认知坑记下来。


二、概念基础:节点、分片、副本

这三个词最容易混,先把关系钉死:

概念是什么关系
节点(Node)一台物理机 / 一个 clickhouse-server 进程集群的物理组成
分片(Shard)数据的逻辑分片单位分片数 ≤ 节点数
副本(Replica)同一分片数据的完整拷贝,驻留在不同节点分片数 × 副本数 = 节点数

核心公式:

节点数 = 分片数 × 每分片副本数

一个具体的例子——3 分片 × 2 副本 = 6 节点:

            集群 (cluster_xxx)
    ┌──────────────┬──────────────┬──────────────┐
    │   分片 1      │   分片 2      │   分片 3      │
    │              │              │              │
    │ 节点 A(副1)   │ 节点 C(副1)   │ 节点 E(副1)   │  ← 分片主副本
    │ 节点 B(副2)   │ 节点 D(副2)   │ 节点 F(副2)   │  ← 分片备副本
    │              │              │              │
    │ 数据 0-1/3   │ 数据 1/3-2/3 │ 数据 2/3-1   │
    └──────────────┴──────────────┴──────────────┘

关键理解:

  • 分片是逻辑切分:数据按某种规则分成 3 份,每份进一个分片
  • 副本是物理冗余:同一个分片的数据,在不同节点上各存一份(通过 ReplicatedMergeTree 自动同步)
  • 分片之间的数据不互通(除非经分布式表),副本之间的数据完全一致

三、一条 SQL 看清集群全貌

不用登服务器、不用翻配置文件,DBeaver 直连任意节点就能查到完整拓扑:

SELECT shard_num,
       replica_num,
       host_address,
       port,
       host_name
FROM system.clusters
WHERE cluster = 'cluster_xxx';

返回示例:

shard_numreplica_numhost_addresshost_name
1110.x.x.2710.x.x.27
1210.x.x.1310.x.x.13
2110.x.x.2910.x.x.29
2210.x.x.1510.x.x.15
3110.x.x.2310.x.x.23
3210.x.x.2410.x.x.24

怎么读:

  • shard_num 的最大值 = 分片总数(这里是 3)
  • 同一个 shard_num 下的多行 = 该分片的多个副本(每行一个节点)
  • replica_num 区分同一分片内的不同副本

这个查询返回的是"集群配置",跟任何数据写入无关——它是运维在部署时配好的静态拓扑,全集群同步,任何节点查到的结果都一样。


四、分片键:被误解最深的参数

分片键是分布式表 DDL 的第四个参数:

ENGINE = Distributed(cluster_xxx, gateway, yi_chen_report_flow, cityHash64(product_id))
                                                                    ↑
                                                              这就是分片键

4.1 分片键不是"分片数量"

看到 cityHash64(product_id) 有人会问:“是不是要建 64 个分片?” —— 完全不是。

  • cityHash64 是一个哈希函数:输入字符串,输出一个 64 位整数
  • 名字里的 “64” 指的是输出位数,跟分片数无关
  • 分片数是 system.clusters 里查出来的(比如 3),由运维配置固定

4.2 分片键真正做的事:哈希取模

当有人向分布式表插入数据时,CK 拿分片键计算结果对分片数取模:

分片号 = hash(分片键值) % 分片数

例(3 分片):
  cityHash64("AAA") % 3 = 0  → 分片 1
  cityHash64("BBB") % 3 = 1  → 分片 2
  cityHash64("CCC") % 3 = 2  → 分片 3
  cityHash64("AAA") % 3 = 0  → 分片 1(同一个值每次都落同一分片)

取模的直觉:

  • 3 个分片 → 结果落在 [0, 1, 2] 三个桶里
  • 同一个 key 每次算出来都一样 → 稳定落同一分片

4.3 分片键只在一条路径上生效

这是最容易被忽略的一点:

写入路径分片键是否生效
INSERT INTO dis_xxx(分布式表)✅ 生效,CK 按分片键路由每行
INSERT INTO xxx_local(本地表)❌ 完全不参与

为什么:本地表 DDL 里压根没有"分片键"这个参数——分片键是分布式表的属性。你写本地表时,这行的落点由当前连接挂在哪个节点决定,跟分片键毫无关系。

4.4 rand() vs cityHash64(key) 的真实差异

分片键经 dis_ 表写入时经 dis_ 表查询时
rand()每行随机撒到各分片WHERE 没法推分片 → 全分片扇出
cityHash64(product_id)同 key 固定落同一分片配合 optimize_skip_unused_shards 可只打 1 个分片

但:在"本地写、分布式读"架构下,两者日常都不生效——写入不走 dis_ 表,分片键就像个摆设。真正的影响只体现在将来需要数据本地性时的可选优化。


五、写入路径对比:本地表 vs 分布式表

业务侧写入有两种方式,各有取舍:

5.1 方式 A:写本地表(本地写 + 连接层均衡)

INSERT INTO gateway.yi_chen_report_flow VALUES (...);
--              ↑ 本地表(无 dis_ 前缀)

路由机制:不经过 CK 的分片逻辑;连接建立时由 LB / 驱动决定落哪台节点,该连接后续所有 INSERT 都写到那个节点的分片。

优点:

  • ✅ 写入路径最短:直写磁盘,无转发
  • ✅ 不产生转发碎 part,后台 merge 压力小
  • ✅ 应用只需知道 LB 地址,不知道拓扑

缺点:

  • ⚠️ 均衡精度取决于 LB 策略 + 连接池大小,不精确
  • ⚠️ 同一个逻辑 key 的数据不保证同分片
  • ⚠️ 依赖 LB(LB 挂了写入全挂)

5.2 方式 B:写分布式表

INSERT INTO gateway.dis_yi_chen_report_flow VALUES (...);
--              ↑ 分布式表

路由机制:CK 按分片键算每行的落点,从接收节点异步转发到目标分片。

优点:

  • ✅ 数据均匀分布(哈希算法保证)
  • ✅ 同 key 固定同分片(如果分片键选 key)
  • ✅ 应用不需要知道拓扑

缺点:

  • ⚠️ 多一跳转发:数据先落在接收节点,再异步转发到目标分片
  • ⚠️ 转发产生碎 part,merge 压力大
  • ⚠️ 转发失败会重试(Replacing 幂等所以安全,但会增加抖动)
  • ⚠️ 接收节点有转发压力(写高峰时可能变瓶颈)

5.3 决策框架

场景推荐
中小规模、追求开发效率写分布式表
大规模、写入吞吐瓶颈写本地表 + 连接层路由
ETL 批量导入写本地表(直写各分片)
有 LB 做连接级均衡写本地表(避免冗余转发)
需要同 key 同分片写分布式表(分片键生效)

判断原则:如果写入 QPS × 单条大小达到几百 MB/秒的量级,分布式表的转发开销会成瓶颈,就得写本地表让连接层分担路由。


六、负载均衡:LB 的"隐式路由"

"写本地表"看起来像没做路由,但如果 LB 端口插在中间,路由其实已经发生了——只是发生在连接层而非应用层。

6.1 架构与数据流

应用 (HikariCP 连接池,比如 10 条连接)
   │  每条连接建立时被 LB 分发到某个后端节点
   ▼
LB (10.x.x.172:9000)   ← native 端口做了负载均衡
   ├──► 节点 A:分片 1 副本 1 ── 写本地表
   ├──► 节点 B:分片 1 副本 2 ──(副本同步,不直接写)
   ├──► 节点 C:分片 2 副本 1 ── 写本地表
   └──► ...

连接池里多条连接被 LB 摊到各节点,每条连接的 INSERT 落在它绑定的节点 = 连接级随机分片,数据是分散的。

6.2 三个关键细节

  1. 路由粒度是连接级不是语句级。Hikari 池内的连接建立后固定绑定某个后端节点,期间该连接所有 INSERT 都走同一节点。均衡精度取决于连接数 × LB 策略,不会精确到行。

  2. 节点宕机时自愈。池内坏连接报错、重建后 LB 重新分发——不需要人工干预。

  3. 写入不区分副本。LB 分发到某节点的连接,写入该节点上驻留的分片副本;如果是备副本,数据会通过 ReplicatedMergeTree + ZK 同步到主副本,反之亦然。应用写入不需要关心是主还是备。

6.3 为什么这是"更优"的一条路

有 LB 做连接级均衡时,写本地表反而绕开了分布式表的转发开销:

  • 无转发 → 无碎 part → 无接收节点压力
  • 数据仍均匀(LB 摊开连接池)
  • 应用零改动

这就是"本地写 + LB"在你们架构里比"写分布式表"更合适的原因——两边都对,取决于基础设施。


七、分布式读取:为什么不会慢

最后一个核心疑问:查 dis_ 表"全分片聚合",不会慢吗?

7.1 不是串行扫全量,而是并行分治

应用查询 dis_yi_chen_report_flow (product_id=? AND report_date=?)
   │
   ▼
发起节点自动成为"协调者"(initiator),把子查询同时发给全部分片
   ├──► 分片 1:本地表执行(排序键命中 + 分区裁剪,只扫几个 granule)──┐
   ├──► 分片 2:本地表执行(并行,同样毫秒级)──────────────────────┤→ 汇聚 → 返回
   └──► 分片 3:本地表执行(并行)──────────────────────────────────┘

耗时 = max(各分片本地耗时) + 一次内网汇聚,不是三个分片时间相加。

7.2 用数字理解

假设 3 个分片、共 30 亿行数据:

方式每个分片扫多少相对耗时
不分片(单机扛 30 亿行)30 亿100%
3 分片并行(各 10 亿行)10 亿≈ 33%

总扫描量没变,但时间是并行的。

7.3 真正的开销与优化时机

开销项量级什么时候需要优化
扇出(分发子查询到各分片)内网一次 RTT,几 ms分片数 ≥ 几十、或 QPS 极高时
汇聚中间结果点查场景几乎为 0大范围聚合查询时
分片本地执行主成本,本来必须有永远

优化手段:

  • 分片键改 cityHash64(product_id) + 开启 optimize_skip_unused_shards
  • 让点查(WHERE product_id = ?)只打 1 个分片
  • 分片数少(个位数)时,扇出的几 ms 完全无感,不用管

八、实战:如何查询分片数量、写入落点、数据分布

应用侧不需要也不能感知落点——真相只能问 CK 集群本身。三组 SQL 由粗到细:

8.1 分片数量与拓扑

SELECT shard_num, replica_num, host_address, port, host_name
FROM system.clusters
WHERE cluster = 'cluster_xxx';

8.2 各分片实际数据量与写入活跃度

SELECT hostName() AS host,
       partition AS pt,
       sum(rows) AS rows,
       max(modification_time) AS last_write
FROM clusterAllReplicas('cluster_xxx', system.parts)
WHERE database = 'gateway'
  AND table = 'yi_chen_report_flow'
  AND active
GROUP BY host, pt
ORDER BY host, pt;

看两点:

  • 各分片行数:同一量级 → LB 均衡性正常
  • 最近写入时间:持续刷新 → 该分片在正常接收写入

8.3 某条数据的实际分布

SELECT hostName() AS host, count() AS cnt
FROM clusterAllReplicas('cluster_xxx', gateway.yi_chen_report_flow)
WHERE product_id = '实际值'
GROUP BY host;

跑几组不同的 product_id 会发现:同一个家庭的数据出现在多个节点——这正是"连接级路由"的直接证据(数据在不同连接的生存期内被写入,落到了不同节点)。

也再次说明:查询必须走 dis_ 表全分片聚合,不能假设数据在本地。


九、决策框架

把所有讨论收敛成一张决策表:

决策点你的场景选择
写入路径有 LB、数据量中等本地表
读取路径需要全集群数据分布式表(无例外)
分片键本地写架构下不生效rand() 够用,不用改
优化时机分片少、QPS 可控无需优化,留证据即可
未来触发条件分片扩到 10+ / QPS 翻倍 / 需同 key 同分片再评估切分布式表 + cityHash64

核心结论:

你们的场景(数据量中等 + 有 LB + 查询走分布式表)下,"本地写 + LB 均衡 + 分布式读"是最优解。

分片键 rand() 是摆设,不影响任何功能;等将来分片数或 QPS 上一个量级、或出现数据本地性需求时,再切分布式写 + cityHash64 分片键。


十、常见误区纠正

误区事实
分片是写入时创建的分片是运维预先配置的拓扑,写入不会改变它
cityHash64 = 建 64 个分片cityHash64 是哈希函数,“64” 指输出位数;分片数由 system.clusters 决定
分片键随便选无所谓本地写架构下无所谓;写分布式表时,选低基数列会导致严重倾斜
写本地表 = 没做路由有 LB 时,路由在连接层发生(连接级随机分片)
写本地表要应用做 hash 路由应用按行 hash 需要维护分片映射,业界几乎没人这么做;LB 或客户端多 host 轮询才是主流
分布式查询会慢是并行分治,不是串行全扫;分片让查询更快,不是更慢
rand() 分片键是缺陷在本地写架构下它不参与路由;将来需要数据本地性时再改

十一、总结

把整篇博客压缩成三句话:

  1. 拓扑是静态的:分片数、副本数由运维预先配置,任何时候查 system.clusters 都能看到;写入不会创建分片。

  2. 分片键只在特定路径生效:只作用于"经分布式表写入"这一条路径;本地写架构下它是摆设,不影响功能和性能。

  3. 读写策略看基础设施:

    • 无 LB、应用直连单节点 → 写分布式表(让 CK 帮你路由)
    • 有 LB、数据量中等 → 写本地表(避免转发开销)
    • 读取无论如何都走分布式表(需要全集群数据)

最终架构判断:

数据量不大 + 有 LB + 查询走分布式表
  → 本地写 + 分布式读 = 标准解

分片数扩张 / QPS 上量 / 需同 key 同分片
  → 切分布式写 + cityHash64 分片键

十二、后记

这套概念不是靠背文档能真正搞清的——它需要先接受"三个层次分开看":

  • 层次 1:集群拓扑(运维配置,静态)
  • 层次 2:本地表 / 分布式表(DDL 层,定义语义)
  • 层次 3:写入 / 读取路径(运行时行为,看基础设施)

把这三层混在一起看,就会觉得"分片数量"和"分片键"矛盾、"写本地表没路由"等等一堆困惑。一旦分开,所有问题都自洽。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/J080624/article/details/168140462

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--