索引拆成多少个分片只是第一步,文档究竟落到哪个分片上,才是决定集群是否均衡的真正变量。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 如何为集群提供可验证、可恢复的备份能力。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。