路由的基本规则
默认情况下,决定一条文档去哪个分片的,是它的 _id。
哈希
对 routing 值做哈希
默认 routing 值就是文档的 _id。Elasticsearch 对它算一个哈希值。
取模
哈希值 mod 分片数
把哈希结果对分片数量取模,得到一个 0 到"分片数-1"之间的编号,这就是目标分片。
分片数固定
创建索引时就定死了
因为分片数是公式的一部分,索引创建之后没法随便改分片数——改了,同一个 ID 算出来的分片编号就变了。
真实验证:同一个 ID,分片数一变,落点就变
建一个 5 分片的索引,插入 10 条文档,用 _search_shards API 直接问 Elasticsearch"这个 ID 该去哪个分片"。
真实实测
PUT /orders { "settings": { "number_of_shards": 5 }}
GET /orders/_search_shards?routing=1
→ doc id=1 → shard 4
doc id=2 → shard 3
doc id=3 → shard 0
doc id=4 → shard 1
doc id=5 → shard 0
doc id=6 → shard 3
doc id=7 → shard 2
doc id=8 → shard 2
doc id=9 → shard 0
doc id=10 → shard 2
反复查同一个 ID 得到的分片编号完全一致(确认过 id=1 连续查两次都是 shard 4)——这是一个纯函数,不依赖任何运行时状态。现在把分片数从 5 改成 3,同样的 ID 会怎样:
真实实测
PUT /orders3 { "settings": { "number_of_shards": 3 }}
GET /orders3/_search_shards?routing=1
→ doc id=1 → shard 2 (5 分片时是 shard 4)
doc id=2 → shard 1 (5 分片时是 shard 3)
doc id=3 → shard 1 (5 分片时是 shard 0)
doc id=4 → shard 1 (5 分片时是 shard 1,巧合一致)
doc id=5 → shard 0 (5 分片时是 shard 0,巧合一致)
这就是"索引创建后不能改分片数"这条规则的根源——不是人为限制,是数学上必然的:分片数是取模运算的除数,除数一变,几乎所有 ID 的取模结果都会跟着变。真的需要扩容,Elasticsearch 提供的是 _split(把每个分片再拆细,分片数必须是原来的整数倍)和 reindex(整个重新写一份到新索引),都是绕开"就地改除数"这件事,而不是直接改。
1000 条文档,分布均不均匀
10 条数据的样本太小看不出规律,批量插入 1000 条,看真实分布。
| 分片 | 文档数 |
| shard 0 | 221 |
| shard 1 | 202 |
| shard 2 | 185 |
| shard 3 | 193 |
| shard 4 | 199 |
1000 条平均下来每个分片该有 200 条,实测在 185~221 之间浮动——哈希函数保证了"差不多均匀",不是"绝对均匀"。这也是为什么分片数不能设得太大:分片数越多,每个分片装的数据量越少,当文档总数不够多的时候,分布不均匀的相对影响会更明显。
自定义路由:把相关的数据摁在同一个分片
默认按 _id 路由是为了让文档尽量均匀撒开。但如果业务上经常要"查某个用户的所有订单",数据分散在 5 个分片里反而更慢——每次都要问全部 5 个分片。Elasticsearch 允许手动指定 routing 值,把相关文档摁在同一个分片。
真实实测
POST /orders/_doc/2001?routing=user42 { "order_id": 2001, "user": "user42" }
POST /orders/_doc/2002?routing=user42 { "order_id": 2002, "user": "user42" }
POST /orders/_doc/2003?routing=user42 { "order_id": 2003, "user": "user42" }
→ 三条不同 ID 的文档,全部落进了 shard 0
-- 带上同样的 routing 去查,只需要碰 1 个分片;不带 routing,得碰全部 5 个
GET /orders/_search_shards?routing=user42 → 1 个分片
GET /orders/_search_shards → 5 个分片
这是一个真实的权衡:自定义 routing 能让"按某个维度查询"变快(少碰几个分片),代价是如果这个维度本身分布不均匀(比如某个大客户订单量远超其他人),数据会在分片间产生热点,均匀性反而变差了——用哪种路由策略,取决于你的查询模式更看重"均匀"还是"局部性"。
演示:文档怎么落进分片
输入一个 ID(或路由值),看它落进哪个分片(用简化哈希还原真实规则的形状)
演示用的哈希函数是一个简化的字符串哈希(不是 Elasticsearch 真实使用的 Murmur3),只用来直观展示"同一个值 hash 后 mod 分片数 = 固定分片"这个规律的形状——换分片数时结果会大幅重排,这一点和真实规则一致;具体每个 ID 落在哪个编号,不代表真实 Elasticsearch 的结果(本文"真实验证"一节的数字才是真实结果)。
参考与说明
- 本文所有分片分配结果均为本机真实运行的 Elasticsearch 8.15.0(单节点)通过 _search_shards API 真实查询得到,1000 条文档的分布统计来自真实的 _cat/shards 输出,未做任何删改。
- 真实的路由公式是 shard = hash(_routing) % routing_num_shards / routing_factor(Murmur3 哈希,routing_num_shards 是为将来支持 _split 预留的一个通常大于分片数的内部值,不直接等于分片数)。本文验证了这个公式最核心的可观察后果——同一个值哈希结果确定不变、换分片数结果大幅重排——但没有在 Python 里逐位复刻 Murmur3 + 精确的 routing_num_shards 推导过程,演示动画用的是形状相似的简化哈希,已在演示区明确说明。
- 没有涉及:副本分片(replica)如何参与读请求的负载均衡(下一篇话题的一部分)、_split/_shrink API 具体怎么在不停机的情况下改变分片数、分片过多/过少各自的运维代价。
→