Elasticsearch 系列 · 第四篇

一个索引拆成好几个分片,一条文档怎么知道自己该进哪一个?

一个 Elasticsearch 索引通常会被切成多个分片(shard),分散存到不同节点上。这带来一个问题:写入一条文档的时候,它凭什么就知道该落进哪个分片?查询的时候,又是怎么知道该去哪几个分片找?这篇在本机真实的 5 分片索引上,用真实写入和真实查询把这套路由机制摸清楚。

221 / 202 / 185 / 193 / 199
1000 条文档真实落到 5 个分片的分布(本机实测)
1 个分片
带上正确的路由参数,一次查询真实只需要碰这么多分片

路由的基本规则

默认情况下,决定一条文档去哪个分片的,是它的 _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 0221
shard 1202
shard 2185
shard 3193
shard 4199

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 具体怎么在不停机的情况下改变分片数、分片过多/过少各自的运维代价。
☕ 如果这篇文章帮到你,可以请作者喝杯咖啡 · 爱发电