上一篇解决了聚合分析框架——Bucket、Metric、Pipeline 三层聚合在搜索结果上提供实时分析。这一篇进入数据怎样分布到多个 shard 上。

ES 把一个 index 的数据分成多个 shard,分布到不同的节点上。分片的核心是路由公式——决定一条文档应该落在哪个 shard 上。这个公式看起来简单,但它的分母不可变这一约束影响了 ES 的整个容量规划思路。

本文只抓一个问题:分片路由公式怎样工作,以及分片数不可变的约束从何而来。

路由公式

1
shard_num = hash(_routing) % number_of_primary_shards

默认情况下 _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
2
3
4
5
6
7
# 写入时指定 routing
POST /orders/_doc?routing=user_123
{ "user_id": "user_123", "product": "laptop", "price": 999 }

# 搜索时指定相同的 routing,只查一个 shard
GET /orders/_search?routing=user_123
{ "query": { "term": { "user_id": "user_123" } } }

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
2
3
4
5
6
7
8
9
10
11
12
13
14
PUT /shard-demo
{ "settings": { "number_of_shards": 3, "number_of_replicas": 0 } }

POST /shard-demo/_bulk
{"index": {"_id": "1"}}
{"title": "doc one"}
{"index": {"_id": "2"}}
{"title": "doc two"}
{"index": {"_id": "3"}}
{"title": "doc three"}
{"index": {"_id": "4"}}
{"title": "doc four"}
{"index": {"_id": "5"}}
{"title": "doc five"}

查看 shard 分布:

1
GET _cat/shards/shard-demo?v&h=index,shard,prirep,state,docs,store,node

_search_shards 查看特定 routing 值会命中哪个 shard:

1
GET /shard-demo/_search_shards?routing=user_123

模式提炼:固定分母哈希

1
2
item → hash(key) % N → bucket
N 不可变 → 数据定位不依赖外部查找
系统 路由方式 分母可变 重新分布
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 值分布不均匀,会导致严重的数据倾斜。

练习

  1. 创建一个 5 shard 的 index,写入 20 条文档,用 _cat/shards 观察每个 shard 的文档数是否大致均匀。

  2. _search_shards?routing=xxx 验证不同 routing 值会路由到不同的 shard。

  3. 用 custom routing 写入同一个 routing 值的 10 条文档,验证它们都在同一个 shard 上。

系列导航

上一篇 下一篇
Aggregation 框架:搜索之上的实时分析 副本与高可用:故障恢复与读写模型

参考资料