路由与分片分配:自定义 routing、分配感知与热点分片治理

系统讲解 Elasticsearch 的路由与分片分配机制:自定义 routing 控制文档落点、分片分配感知与过滤规则、再平衡阈值调节、热点分片的诊断与治理手段。

索引拆成多少个分片只是第一步,文档究竟落到哪个分片上,才是决定集群是否均衡的真正变量。Elasticsearch 用路由值做哈希把文档映射到分片,默认路由值是文档 _id,看起来随机,实则受业务主键形态强烈影响;再叠加分配感知、分配过滤与再平衡策略,同一个集群既可能被调成完美均摊,也可能被调成一边倒的热点。本文从路由机制讲起,覆盖自定义 routing、分配感知与过滤、再平衡阈值,最后落到热点分片的诊断与治理。

1. 路由机制与文档落点

一句话总结: 文档落到哪个分片由路由值经哈希决定,默认使用 _id,理解这一过程是控制数据分布的前提。

写入一条文档时,协调节点不会把请求广播给所有分片,而是先算出目标分片,再把请求转发到该分片的主分片。这个「算」的过程就是路由。

1.1 默认路由与哈希过程

一句话总结: 默认路由值是文档 _id,经过 murmur3 哈希再对主分片数取模,得到分片编号。

路由公式可以简写为:

shard_num = hash(_routing) % number_of_primary_shards

其中 _routing 在未显式指定时等于文档 _id。哈希函数是 murmur3,取模对象是主分片数而非总分片数,副本只是主分片的拷贝,不参与路由。

这条公式解释了一个经典陷阱:主分片数一旦确定就不能改。因为改了之后 % number_of_primary_shards 的结果全变,所有既有文档的路由位置都会失效,只能重建索引。这也是为什么分片规划必须在建索引时想清楚。

1.2 路由与副本、读写路径的关系

一句话总结: 路由只决定主分片,读写请求先定位分片再在副本间做负载均衡。

写入路径是:协调节点算分片号,把请求发给该分片当前的主分片,主分片写入成功后同步到副本。读取路径是:协调节点算分片号,在该分片的主副本集合中按轮询挑一个(默认自适应副本选择会优先挑负载低的),执行查询。

因此路由值一致的文档必然在同一分片,这既是「同分片聚合」的基础,也是「路由倾斜」的根源。

2. 自定义 routing 的实践

一句话总结: 显式指定 routing 可以把同一业务实体的文档聚到同一分片,显著降低查询扇出,但必须警惕路由倾斜。

2.1 为什么需要自定义 routing

一句话总结: 把同一租户或同一用户的文档路由到同一分片,可以让查询只打一个分片。

典型场景是「一个用户一个分片」。如果按 _id 随机路由,查某用户最近 100 条订单要打到所有分片再合并;如果按 user_id 路由,只需要打一个分片。

# 写入时指定 routing
curl -X POST "localhost:9200/orders/_doc/1001?routing=user_42" -H 'Content-Type: application/json' -d '
{
  "user_id": "user_42",
  "order_no": "SO20261001001",
  "amount": 128.50,
  "created_at": "2026-10-01T09:30:00+08:00"
}'

# 查询时必须带上同样的 routing,否则会扇出到所有分片
curl -X GET "localhost:9200/orders/_search?routing=user_42" -H 'Content-Type: application/json' -d '
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "user_id": "user_42" } }
      ]
    }
  },
  "sort": [ { "created_at": "desc" } ],
  "size": 100
}'

查询时若忘记带 routing,虽然结果依然正确,但协调节点会把请求发到该索引的每个分片,扇出放大,性能优势荡然无存。

2.2 路由倾斜与分片膨胀

一句话总结: 路由值的分布决定分片大小分布,热门租户会把单个分片撑成巨无霸。

如果某个大客户的文档量占全库 40%,而它只路由到一个分片,这个分片就会比其他分片大好几倍,成为写入与查询的双重瓶颈。常见对策:

  • 复合路由:把 tenant_id 与日期或哈希后缀拼接,例如 tenant_42_2026w40,把一个租户的数据摊到多个分片。
  • routing_partition_size:建索引时设置该参数,允许一个路由值映射到多个分片,缓解倾斜。
curl -X PUT "localhost:9200/orders_v2" -H 'Content-Type: application/json' -d '
{
  "settings": {
    "number_of_shards": 12,
    "number_of_replicas": 1,
    "routing_partition_size": 3
  }
}'

routing_partition_size 大于 1 时,路由值会落到一组分片上,写入与查询都要多打几个分片,是用少量扇出换均衡的折中。

2.3 用 routing 优化聚合

一句话总结: 聚合的代价与参与分片数成正比,routing 把同组数据收拢后可以大幅降低协调开销。

对于一个租户一个分片的模型,按租户分组聚合几乎是单分片操作,terms 聚合的全局合并代价接近于零。反过来,如果数据随机分布,每个分片都要产出一份部分聚合结果再由协调节点归并,分片越多、基数越高,代价越大。

3. 分片分配感知

一句话总结: 分配感知让分片副本尽量分散到不同机架、不同可用区,避免单点故障连带损失。

3.1 感知属性与作用

一句话总结: 配置 cluster.routing.allocation.awareness.attributes 后,同一分片的主副本会被强制分到不同感知域。

在 elasticsearch.yml 中为节点打上属性,再声明感知属性:

# 每个节点配置自己的机架与可用区
node.attr.rack: rack-a
node.attr.zone: cn-north-1a

# 主节点配置感知属性(集群级设置,也可用 API 动态设置)
cluster.routing.allocation.awareness.attributes: rack,zone

配置后,同一个分片的主分片与副本不会落在同一个 rack 上。若某 rack 整体掉线,其他 rack 上仍有完整副本。

3.2 强制感知与自动收敛

一句话总结: forced 感知会在感知域数量不足时拒绝分配分片,防止副本被塞回同一域。

cluster.routing.allocation.awareness.force.zone.values: cn-north-1a,cn-north-1b

强制感知声明了「预期存在哪些域」。当 cn-north-1b 整体不可用时,集群不会把副本挤到 cn-north-1a,而是让副本处于未分配状态,避免「看起来有副本、实则同域」的假高可用。这牺牲了部分可用性换取真正的隔离。

4. 分配过滤与节点属性

一句话总结: 分配过滤通过 include、exclude、require 三类规则,把分片精确投放到指定节点组。

4.1 三类过滤规则

一句话总结: include 是「可以放这」,require 是「必须放这」,exclude 是「绝不能放这」。

# 把索引固定在热节点组
curl -X PUT "localhost:9200/logs-hot/_settings" -H 'Content-Type: application/json' -d '
{
  "index.routing.allocation.require.data": "hot",
  "index.routing.allocation.exclude._name": "node-cold-1"
}'

三类规则的语义差异很关键:

  • include:候选节点集合,取交集,多个值之间是「或」。
  • require:强制集合,不满足的节点直接排除。
  • exclude:黑名单,命中即排除。

4.2 冷热分层与 ILM 联动

一句话总结: 用节点属性划分热温冷层,配合 ILM 在生命周期中迁移分片。

典型做法是给节点打 data: hot、data: warm、data: cold 三类属性,索引模板按阶段设置 require.data。ILM 的 allocate 动作在 rollover 后把旧索引迁到温层,再迁到冷层,最终删除。分配过滤是这条流水线的执行机构。

4.3 迁移与磁盘水位

一句话总结: 分配决策还受磁盘水位线约束,超过高水位线的节点会主动迁出分片。

集群默认低水位 85%、高水位 90%、洪水水位 95%。节点超过高水位时,集群会尝试把分片迁走;超过洪水水位则对该节点上的索引置为只读。水位线可以按节点覆盖:

curl -X PUT "localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d '
{
  "transient": {
    "cluster.routing.allocation.disk.watermark.low": "88%",
    "cluster.routing.allocation.disk.watermark.high": "92%",
    "cluster.routing.allocation.disk.watermark.flood_stage": "96%"
  }
}'

5. 分片再平衡

一句话总结: 再平衡在节点增减或负载不均时搬动分片,用阈值控制搬动量,避免抖动。

5.1 再平衡的触发条件

一句话总结: 节点加入、离开、属性变化或分片大小差异超阈值都会触发再平衡。

再平衡由分配决策器周期性评估。它比较各节点上的分片数与磁盘占用,当某个节点明显更重时,选择代价最小的分片搬迁。搬迁本身是「新增副本再删旧副本」,过程中数据可用性不受影响,但会占用网络与磁盘 IO。

5.2 关键阈值参数

一句话总结: 用 cluster.routing.allocation.balance.* 与 cluster.routing.rebalance.enable 控制均衡力度。

curl -X PUT "localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d '
{
  "persistent": {
    "cluster.routing.allocation.balance.shard": 0.45,
    "cluster.routing.allocation.balance.index": 0.55,
    "cluster.routing.allocation.balance.threshold": 1.0,
    "cluster.routing.rebalance.enable": "all"
  }
}'
  • balance.shard:按分片数均衡的权重。
  • balance.index:按索引维度均衡的权重,避免同一索引的分片全挤在少数节点。
  • balance.threshold:触发再平衡的相对差异阈值,调大更稳定、调小更均衡。
  • rebalance.enable:可取 all、primaries、replicas、none。

5.3 抑制再平衡的手段

一句话总结: 维护窗口或大批量导入时,临时关闭再平衡可以避免搬迁与业务抢 IO。

curl -X PUT "localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d '
{
  "transient": {
    "cluster.routing.rebalance.enable": "none",
    "cluster.routing.allocation.enable": "primaries"
  }
}'

cluster.routing.allocation.enable 设为 primaries 时只允许分配主分片,副本原地不动,适合滚动重启期间使用。注意这些是临时设置,重启后失效,长期维护建议用 persistent。

6. 热点分片的诊断

一句话总结: 热点分片表现为单节点 CPU、IO 或分片大小明显偏离均值,需要从路由、分配与查询三方面定位。

6.1 用 cat API 观察分布

一句话总结: _cat/shards 与 _cat/allocation 是观察分片落点与节点负载的第一手工具。

# 按分片大小倒序,找出巨无霸分片
curl -s "localhost:9200/_cat/shards?v&h=index,shard,prirep,state,docs,store,node&s=store:desc" | head -20

# 看每个节点的分片数与磁盘占用
curl -s "localhost:9200/_cat/allocation?v&h=node,shards,disk.used_percent,disk.avail,disk.total"

若某节点分片数远多于其他节点,或某分片 store 是均值数倍,就找到了热点。

6.2 用分配解释定位原因

一句话总结: _cluster/allocation/explain 能告诉你为什么某个分片没被分配到更合适的节点。

curl -X GET "localhost:9200/_cluster/allocation/explain" -H 'Content-Type: application/json' -d '
{
  "index": "orders",
  "shard": 3,
  "primary": false
}'

输出里会列出每个节点的决策原因,例如磁盘水位超限、过滤规则不匹配、感知域冲突等,是排查「分片迟迟不均衡」的利器。

6.3 从查询侧看扇出

一句话总结: 慢日志与 profile 输出里的分片命中数,能反映路由是否失效。

curl -X GET "localhost:9200/orders/_search?profile=true" -H 'Content-Type: application/json' -d '
{
  "size": 0,
  "query": { "term": { "user_id": "user_42" } }
}'

如果一次本应单分片的查询命中了 12 个分片,说明查询没带 routing,或索引的分片设计本身就不支持收拢。

7. 热点分片的治理

一句话总结: 治理热点分片的手段包括重建索引改路由、拆分大分片、迁移分片与限制并发写入。

7.1 重建索引调整路由

一句话总结: 用 _reindex 加 routing 参数把历史数据重新分布到目标分片。

curl -X POST "localhost:9200/_reindex?wait_for_completion=false" -H 'Content-Type: application/json' -d '
{
  "source": {
    "index": "orders",
    "size": 5000,
    "_source": ["user_id", "order_no", "amount", "created_at"]
  },
  "dest": {
    "index": "orders_v2",
    "routing": "user_42"
  },
  "script": {
    "source": "ctx._routing = ctx._source.user_id"
  }
}

用脚本按源文档字段动态设置 routing,可以一次性把随机分布的文档改成按业务键分布。reindex 是耗时操作,务必加 wait_for_completion=false 并配合 _tasks 观察进度。

7.2 拆分与合并分片

一句话总结: _split 把一个分片拆成多个以缓解写热点,_shrink 则相反,用于合并小分片。

# 拆分:目标索引的分片数必须是源索引的整数倍
curl -X POST "localhost:9200/orders_split/_split/orders_v3?copy_settings=true" -H 'Content-Type: application/json' -d '
{
  "settings": {
    "index.number_of_shards": 24
  }
}'

拆分要求源索引只读且所有分片在同一节点上,实践中先 _shrink 到 1 个分片再 _split 是常见路径。

7.3 限制并发与写入打散

一句话总结: 应用侧用批量写入与随机化 routing 后缀,可以削平瞬时写热点。

当热点来自瞬时写入洪峰而非分布不均时,可以从应用层缓解:把大批量 _bulk 拆成多个较小批次并随机打散到不同分片,或用带随机后缀的 routing 让写入均匀铺开。

7.4 监控与持续观察

一句话总结: 把分片大小方差、节点分片数方差纳入监控,才能提前发现倾斜趋势。

curl -s "localhost:9200/_cluster/health?level=indices&pretty" | head -40

建议在监控里对每个索引维护「分片 store 的标准差 / 均值」与「节点分片数极差」两个指标,超出阈值即告警。倾斜往往是渐进的,等业务感知到慢时才处理,代价已经很大。

8. 总结

环节要点
路由公式分片号等于 hash 路由值对主分片数取模,主分片数建索引后不可改
自定义 routing同业务实体聚到同分片降低扇出,查询写入都必须带 routing
路由倾斜用复合路由或 routing_partition_size 摊平大租户
分配感知awareness 属性做机架与可用区隔离,force 防止假高可用
分配过滤require 强制、include 候选、exclude 排除,配合冷热分层与 ILM
再平衡balance 权重与 threshold 控制力度,维护窗口可临时关闭
热点诊断cat shards 看分布,allocation explain 看原因,profile 看扇出
热点治理reindex 改路由、split 拆大分片、限制并发、持续监控方差

路由与分配是集群均衡的两只手:路由决定数据先天怎么分布,分配决定后天怎么摆布。把这两者调顺,集群的吞吐与稳定性就有了地基。下一篇我们转向数据的兜底手段,讲快照与 SLM 如何为集群提供可验证、可恢复的备份能力。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 可搜索快照与冻结层:把冷数据放进对象存储还能查
  2. 分页与深度分页:from/size、search_after、PIT 与 scroll
  3. 嵌套与父子关联查询:nested、join 字段与性能取舍