深入 Elasticsearch(10):数据分布的核心机制
上一篇解决了聚合分析框架——Bucket、Metric、Pipeline 三层聚合在搜索结果上提供实时分析。这一篇进入数据怎样分布到多个 shard 上。
ES 把一个 index 的数据分成多个 shard,分布到不同的节点上。分片的核心是路由公式——决定一条文档应该落在哪个 shard 上。这个公式看起来简单,但它的分母不可变这一约束影响了 ES 的整个容量规划思路。
本文只抓一个问题:分片路由公式怎样工作,以及分片数不可变的约束从何而来。
路由公式
1 | |
默认情况下 _routing 等于文档的 _id。这个公式把文档 ID 的哈希值对 primary shard 数取模,得到目标 shard 编号。
5 条文档经过这个公式分配到 3 个 shard 的过程:
graph LR
D1["doc _id=1"] --> H["hash(_id) % 3"]
D2["doc _id=2"] --> H
D3["doc _id=3"] --> H
D4["doc _id=4"] --> H
D5["doc _id=5"] --> H
H --> S0["Shard 0"]
H --> S1["Shard 1"]
H --> S2["Shard 2"]
style H fill:#4a90d9,color:#fff
style S0 fill:#7bc47f
style S1 fill:#7bc47f
style S2 fill:#7bc47f
哈希函数对同一个 _id 总是产生相同的结果,所以读取时不需要查询元数据——直接算一次就知道文档在哪个 shard 上。
这个公式意味着:
- 给定相同的
_routing和相同的number_of_primary_shards,结果总是确定的。不需要查元数据就能知道文档在哪个 shard 上。 number_of_primary_shards一旦确定就不能改变。如果把分母从 5 改成 6,所有文档的路由结果都会变,已有数据不再能正确定位。- 分片数在 index 创建后不可变。这是 ES 容量规划中最重要的约束之一。
Custom Routing
默认按 _id 路由让数据均匀分布。但某些场景需要让相关文档落在同一个 shard 上——比如按用户 ID 路由,让同一个用户的所有数据在同一个 shard 上,搜索时只需要查一个 shard:
1 | |
Custom routing 的风险是数据倾斜。如果某些 routing 值对应的文档数远多于其他值,会导致部分 shard 数据量远大于其他 shard(热点 shard)。
routing_partition_size 可以缓解这个问题——让一个 routing 值的数据分散到多个 shard(而不是只一个),在 custom routing 和数据均匀之间取平衡。
Shard 分配
路由公式决定文档落在哪个 shard 上,shard allocation 决定 shard 落在哪个 node 上。
master 节点负责 shard 分配,考虑的因素包括:
| 策略 | 作用 |
|---|---|
| 均衡分配 | 每个节点上的 shard 数尽量均衡 |
| 同 index 分散 | 同一个 index 的 primary 和 replica 不放在同一个节点 |
| Allocation awareness | 按 rack/zone 属性分散,保证跨机架/可用区高可用 |
| Disk watermark | 磁盘使用率超过阈值时停止分配新 shard |
| Allocation filtering | 通过 index.routing.allocation.include/exclude 控制 shard 只分配到特定节点 |
路由公式决定了文档到 shard 的映射,shard allocation 决定了 shard 到 node 的映射。一个 3-shard、1-replica 的 index 在 3 节点集群上的分布:
graph TB
subgraph Node_1["Node 1"]
P0["P0<br/>Primary Shard 0"]
R2["R2<br/>Replica Shard 2"]
end
subgraph Node_2["Node 2"]
P1["P1<br/>Primary Shard 1"]
R0["R0<br/>Replica Shard 0"]
end
subgraph Node_3["Node 3"]
P2["P2<br/>Primary Shard 2"]
R1["R1<br/>Replica Shard 1"]
end
P0 -.->|同步| R0
P1 -.->|同步| R1
P2 -.->|同步| R2
style P0 fill:#4a90d9,color:#fff
style P1 fill:#4a90d9,color:#fff
style P2 fill:#4a90d9,color:#fff
style R0 fill:#f5a623
style R1 fill:#f5a623
style R2 fill:#f5a623
蓝色是 primary shard,橙色是对应的 replica shard。master 节点保证同一个 shard 的 primary 和 replica 不会落在同一个节点上——任何单节点故障最多丢失一份副本,另一份仍然可用。
实验:观察分片分布
创建一个 3 shard 的 index 并写入文档:
1 | |
查看 shard 分布:
1 | |
用 _search_shards 查看特定 routing 值会命中哪个 shard:
1 | |
模式提炼:固定分母哈希
1 | |
| 系统 | 路由方式 | 分母可变 | 重新分布 |
|---|---|---|---|
| ES shard routing | hash % N(N 固定) | 否(需 reindex) | 手动 reindex |
| Kafka partition | hash % N(N 固定) | 否(新分区不迁移旧数据) | 手动 reassign |
| MySQL 分库分表 | hash % N 或 range | 通常固定 | 手动迁移 |
| Redis Cluster | 16384 hash slot | slot 可迁移 | 自动 resharding |
| 一致性哈希 | hash ring | 动态增减节点 | 只迁移相邻数据 |
ES 的路由方式是一致性哈希的简化形式。一致性哈希在增减节点时只需要迁移部分数据,ES 的固定分母哈希在改变 shard 数时需要完全重建。简化的好处是路由计算极快(一次哈希 + 一次取模),代价是分母不可变。
工程迁移表
| 概念 | Elasticsearch | Kafka | MySQL 分库分表 | Redis Cluster |
|---|---|---|---|---|
| 数据分布单元 | Shard | Partition | 分库/分表 | Hash slot |
| 路由公式 | hash(_routing) % N | hash(key) % N | hash(key) % N 或 range | CRC16(key) % 16384 |
| 分区数可变 | 否(需 reindex) | 可增不可减 | 困难 | Slot 可迁移 |
| Custom 路由 | routing 参数 | 自定义 partitioner | 分片键 | Hash tag |
| 数据倾斜处理 | routing_partition_size | 无内置 | 无内置 | 无内置 |
常见误解
误解一:可以随时增加 shard 数。 Primary shard 数在 index 创建后不可修改。需要更多 shard 时,必须创建新 index(更多 shard)并 reindex 数据。_split API 可以把一个 index 分裂为 shard 数更多的新 index,但也是创建新 index。
误解二:shard 数越多越好。 每个 shard 是一个 Lucene index,有固定开销(内存、文件句柄、cluster state 元数据)。过多的 shard 会增加 cluster state 大小、增加搜索的 scatter-gather 开销。官方建议每个 shard 的大小在 10GB-50GB 之间。
误解三:custom routing 总是好主意。 Custom routing 让相关数据集中到同一个 shard,搜索时可以只查一个 shard。但如果 routing 值分布不均匀,会导致严重的数据倾斜。
练习
-
创建一个 5 shard 的 index,写入 20 条文档,用
_cat/shards观察每个 shard 的文档数是否大致均匀。 -
用
_search_shards?routing=xxx验证不同 routing 值会路由到不同的 shard。 -
用 custom routing 写入同一个 routing 值的 10 条文档,验证它们都在同一个 shard 上。
系列导航
| 上一篇 | 下一篇 |
|---|---|
| Aggregation 框架:搜索之上的实时分析 | 副本与高可用:故障恢复与读写模型 |
