☰
Elasticsearch 分片分配与再平衡的底层逻辑:从 allocation decider 到集群扩容抖动
2026/10/8 14:30:30 网站建设 项目流程

Elasticsearch 分片分配与再平衡的底层逻辑:从 allocation decider 到集群扩容抖动

1. 先从一次扩容抖动说起

假设你维护一个 6 节点的 Elasticsearch 集群,每个节点 4 核 16GB 内存、2TB 数据盘。某天索引写入变慢,你决定横向扩容到 9 节点。新节点启动、加入集群,_cat/nodes里能看到它们,但过了半小时,_cat/shards里仍有大量分片集中在老节点上。你重启了几个新节点,分片反而开始来回搬,_cluster/health从 green 掉到 yellow,写入延迟飙升。问题出在哪?

这不是“节点加得不对”,而是 Elasticsearch 的分片分配与再平衡没有按你脑中的节奏走。分片不会因为“新机器有空位”就自动平均迁移,它要先过一整套准入判断,再根据权重决定要不要搬、搬多少、什么时候搬。

先记住一个最小模型:分片分配可以理解为“候选节点过滤 + 权重排序”。过滤由 allocation decider 完成,判断某个分片能不能去某个节点;排序由ShardsAllocator完成,在能去的节点里选一个得分最高的。再平衡只是分配的一种触发场景,扩容抖动则通常由过滤被临时阻断、恢复任务并发不足、磁盘水位线卡住共同造成。

下面这张图先给出全局框架,后续章节按图中的顺序展开。

集群状态变更(新节点加入 / 节点离线 / 索引创建) | v +----------------------------+ | AllocationService | 收集待分配分片 | (分配服务) | +-------------+--------------+ | v +----------------------------+ | AllocationDecider 链 | 能不能去? | 路由 / 磁盘水位 / 并发 | | 副本同节点 / 过滤规则 | +-------------+--------------+ | 能去节点列表 | v +----------------------------+ | ShardsAllocator | 去哪个更均衡? | BalancedShardsAllocator | +-------------+--------------+ | v +----------------------------+ | ClusterState 更新 | 分片进入 INIT / RELOCATING | RoutingTable 落盘 | +-------------+--------------+ | v +----------------------------+ | 节点执行 shard 恢复 | 先小后大,副本再平衡 +----------------------------+

这张图能帮助你理解“谁先谁后”,但它不能替代真实源码中的并发状态机。真实集群里,decider 链会在不同阶段被多次调用,分配结果也可能因为节点状态变化而回滚重试。

2. allocation decider 到底是什么

allocation decider 是一组“准入判断器”。每个 decider 只回答一个问题:某个分片能不能分配到某个节点。所有 decider 依次过滤,只要有一个返回NO,这个节点就被排除;如果返回THROTTLE,它不会完全排除节点,而是降低优先级或延后分配。

常见的 decider 包括:

Decider 名称判断内容典型触发场景返回 NO 的后果
SameShardAllocationDecider同一分片的主副本是否已在目标节点单节点上已有该分片副本该节点不可用,强制换节点
DiskThresholdDecider磁盘使用率是否超过 low/high/flood 水位线高写入集群磁盘接近满达到 high 后停止分配新分片,达到 flood 后索引变只读
ShardsLimitAllocationDecider单节点上同一索引分片数是否超限索引分片过多、节点少阻止继续堆积,避免热点
ConcurrentRebalanceAllocationDecider当前并发再平衡分片数是否超限集群正在大量搬迁延迟新再平衡,保护恢复带宽
FilterAllocationDecider节点属性是否匹配index.routing.allocation.*机房、机架、冷热分层节点被排除,分片去其他节点

看起来这些规则很零散,但它们的共同作用是:把“物理上能放”与“业务上该放”分开。物理上能放,包括磁盘、JVM、并发;业务上该放,包括机架感知、冷热分层、租户隔离。

这里最容易误解的是:decider 不是“一次性全部跑完”。在分片进入INIT、RELOCATING、UNASSIGNED等不同状态时,decider 链的输入和结果都会变化。例如节点刚加入时,磁盘水位线判断会把它当作“空闲节点”放行;但当它开始接收分片后,磁盘使用率上升,后续分片就会被重新过滤。

3. 一次分片分配的完整过程

现在让一次“新索引创建”完整走一遍。假设你创建了一个 3 主 1 副本的索引,集群有 3 个数据节点。

第一步,主节点收到create index请求,生成新的索引元数据,并把所有主分片放入UNASSIGNED。

第二步,AllocationService扫描路由表,发现 3 个主分片都未分配,于是对每个分片调用 decider 链。对于主分片,decider 会检查:目标节点是否已有同一分片的副本?磁盘是否超过水位线?节点是否被过滤规则排除?

第三步,能去的节点形成候选列表。BalancedShardsAllocator会计算每个候选节点的权重。权重不是简单的“分片数最少”,它还会考虑索引级均衡、节点容量、分片大小等因素。最终每个主分片选择一个节点。

第四步,主节点更新集群状态,分片进入INITIALIZING,目标节点开始从主分片或快照恢复数据。副本分片不会和主分片同节点,分配到其他节点后进入INITIALIZING,等待主分片恢复完成后复制。

这个过程可以用下面的时序图表示:

客户端 主节点 数据节点 A 数据节点 B | | | | | create index | | | |------------->| | | | | 生成 UNASSIGNED | | | | 调用 decider 链 | | | | 计算节点权重 | | | | 选择 A 作为主分片 | | | |-------------------->| | | | | 分片 INIT | | | 选择 B 作为副本 | | | |------------------------------------->| | | | | 分片 INIT | | 等待主分片恢复完成 | | | | | 恢复完成 | | |<--------------------| | | | 副本开始复制 | | | |------------------------------------->| | | | | 副本恢复完成 |<-------------| | | | 索引可用 | | |

这个顺序说明了:主分片先恢复,副本分片再跟进。如果主分片恢复慢,副本会一直等待,集群健康度会先显示 yellow,直到副本完成。

4. 分片再平衡:不是“越多越好”

再平衡是分配的一种,但目标不同。分配解决“分片有没有地方去”,再平衡解决“分片分布是否足够均匀”。Elasticsearch 默认会在节点之间移动分片,让每个节点的分片数尽量接近,但再平衡受两个约束:

第一,不能破坏主副本分离。副本必须和主分片在不同节点,否则再平衡会把副本搬到主分片所在节点,触发 decider 的NO。

第二,不能压垮恢复带宽。再平衡会搬运实际数据,如果同时搬太多分片,网络、磁盘、CPU 都会被占满,影响正常搜索和写入。因此cluster.routing.allocation.cluster_concurrent_rebalance默认值是 2,表示整个集群同时最多搬 2 个分片。

这里有一个常见的扩容误区:节点加进来后,不一定立刻发生再平衡。因为再平衡的触发条件包括分片分布差异超过阈值、节点被加入分配过滤、索引级别设置了index.routing.allocation.total_shards_per_node等。如果新节点一开始没有满足这些条件,它只会“站着看”,不会自动接盘。

你可以用下面命令观察再平衡情况:

GET _cat/shards?v&h=index,shard,prirep,state,docs,store,node GET _cat/recovery?v&active_only=true GET _cluster/settings?include_defaults=true&flat_settings=true|grep-E"rebalance|concurrent"

第一条看分片分布,第二条看正在恢复的分片,第三条看再平衡相关配置。如果你发现RELOCATING长期不结束,先看_cat/recovery里是否有分片卡在TRANSLOG或FINALIZE阶段,再检查磁盘水位线和网络带宽。

5. 集群扩容为什么会“抖”

扩容抖动不是单一原因,通常是下面四个因素叠加:

第一,并发恢复不足。默认cluster.routing.allocation.node_concurrent_recoveries是 2,意味着每个节点同时最多恢复 2 个分片。新节点加入后,如果它同时被分配大量分片,恢复会排队,集群看起来“没反应”。

第二,副本分片先搬,主分片后搬。再平衡优先搬运副本,因为副本数据可以从主分片复制,风险低。但如果副本很多,搬迁会占用大量网络和磁盘。

第三,磁盘水位线阻断。当节点磁盘使用率超过cluster.routing.allocation.disk.watermark.low(默认 85%),新分片不会分配到该节点;超过high(默认 90%),Elasticsearch 会尝试把分片移走;超过flood(默认 95%),索引变成只读,写入直接失败。

第四,分片恢复和写入竞争。恢复分片时,节点既要读远端数据、写本地磁盘,又要处理正常写入。如果恢复并发太高,写入延迟会上升;如果太低,扩容后均衡太慢。

下面这张表对比了扩容时三种典型现象:

现象可能原因观察命令处理方向
新节点分片数为 0再平衡阈值未触发、磁盘水位高、过滤规则排除_cat/allocation?v、_cluster/allocation/explain检查水位线与过滤规则,必要时手动调整
分片一直 RELOCATING恢复带宽不足、目标节点磁盘慢、并发恢复数低_cat/recovery?v&active_only=true提高并发恢复数,但不要超过磁盘承受能力
集群健康 yellow 转 red主分片恢复失败、磁盘 flood 只读_cluster/health、_cat/shards先解除磁盘只读,再排查主分片

6. shard 恢复:先小后大,先主后副

分片恢复不是简单复制文件。Elasticsearch 的恢复过程大致是:

  1. 目标节点向主分片所在节点发起恢复请求。
  2. 源节点创建快照,把分段文件传输到目标节点。
  3. 目标节点接收文件、校验、写入本地磁盘。
  4. 目标节点重放 translog,补齐恢复期间的新写入。
  5. 分片进入STARTED,开始对外服务。

这个过程决定了两个重要事实:恢复时间与分片大小正相关,但与恢复期间写入量也正相关。一个 50GB 的分片,如果恢复期间还有持续写入,translog 重放会拖长恢复时间。

你可以用_cat/recovery看到每个阶段的耗时:

GET _cat/recovery?v&active_only=true&h=index,shard,time,stage,source_node,target_node,files_percent,translog_ops_percent

字段stage常见值有INIT、INDEX、TRANSLOG、FINALIZE、DONE。如果卡在TRANSLOG,说明文件已传完,但在重放写入;如果卡在FINALIZE,通常是目标节点在做最终校验和状态切换。

这里容易改错的地方是:不要为了加快恢复而无限提高并发。恢复并发受节点磁盘 IO、网络带宽、JVM 堆影响。把node_concurrent_recoveries从 2 提到 8,可能导致正常查询超时,因为恢复线程和搜索线程抢 CPU 和 page cache。

7. 磁盘水位线:从根上决定能不能写

磁盘水位线是分配和写入的共同约束。它有三个级别:

水位线默认值作用触发后的行为
low85%不再向该节点分配新分片新分片去其他节点,可能不均衡
high90%尝试把分片移走触发再平衡搬离,搬不动则一直尝试
flood95%索引变只读写入失败,需要清理磁盘或调高水位线

水位线判断的是“磁盘使用率”,不是“剩余空间”。在数据盘大小不同的集群里,这会导致小盘节点先触发 high,大盘节点还有空间却接不到分片。因此生产上建议让数据节点磁盘规格一致,或者按磁盘大小设置不同的水位线。

下面的命令可以查看各节点磁盘使用率和分片分布:

GET _cat/allocation?v&h=node,shards,disk.used_percent,disk.avail,disk.total GET _cluster/settings?include_defaults=true&flat_settings=true|grep-E"disk.watermark"PUT _cluster/settings{"persistent":{"cluster.routing.allocation.disk.watermark.low":"85%","cluster.routing.allocation.disk.watermark.high":"90%","cluster.routing.allocation.disk.watermark.flood_stage":"95%"}}

注意:调高水位线只是争取时间,不是解决方案。真正要做的是清理旧索引、扩容磁盘、或者把冷数据迁移到对象存储。

8. 用 allocation explain 定位卡点

当分片迟迟不分配时,Elasticsearch 提供了一个非常直接的排查工具:_cluster/allocation/explain。它会告诉你哪个 decider 返回了NO,以及为什么。

假设你有一个未分配的分片,可以执行:

GET _cluster/allocation/explain{"index":"orders-2024.06","shard":0,"primary":false}

返回结果中会包含can_allocate、allocate_explanation、node_allocation_decisions。如果can_allocate是no,往下看deciders数组,找到decision为NO的项。常见输出包括:

  • disk threshold:磁盘超过 high 水位线。
  • same shard:目标节点已有同一分片。
  • filter:节点属性不匹配index.routing.allocation.include/exclude。
  • concurrent rebalance:并发再平衡分片数已满。

这个工具的价值在于:它把“为什么不分配”从猜测变成证据。很多扩容抖动问题,最后都能在 explain 输出里找到一条明确的 decider 拒绝记录。

9. 完整示例一:用 Java 创建索引并观察分配

下面这个最小 Java 示例使用 Elasticsearch Java API Client,创建一个 3 主 1 副本的索引,并打印分片分布。它的目标是让你把“索引创建”和“分片分配”连起来。

前置环境:JDK 17、Maven、Elasticsearch 8.x 本地单节点或三节点集群。

<dependency><groupId>co.elastic.clients</groupId><artifactId>elasticsearch-java</artifactId><version>8.13.0</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.17.0</version></dependency>
importco.elastic.clients.elasticsearch.ElasticsearchClient;importco.elastic.clients.elasticsearch.cluster.HealthResponse;importco.elastic.clients.elasticsearch.indices.CreateIndexResponse;importco.elastic.clients.json.jackson.JacksonJsonpMapper;importco.elastic.clients.transport.rest_client.RestClientTransport;importorg.apache.http.HttpHost;importorg.elasticsearch.client.RestClient;publicclassCreateIndexAndCheckAllocation{publicstaticvoidmain(String[]args)throwsException{RestClientrestClient=RestClient.builder(newHttpHost("localhost",9200,"http")).build();RestClientTransporttransport=newRestClientTransport(restClient,newJacksonJsonpMapper());ElasticsearchClientclient=newElasticsearchClient(transport);try{CreateIndexResponseresponse=client.indices().create(c->c.index("orders-2024.06").settings(s->s.numberOfShards("3").numberOfReplicas("1")).mappings(m->m.properties("orderId",p->p.keyword(k->k)).properties("amount",p->p.double_(d->d))));System.out.println("索引创建结果: "+response.acknowledged());HealthResponsehealth=client.cluster().health(h->h.index("orders-2024.06").waitForStatus(co.elastic.clients.elasticsearch._types.HealthStatus.Yellow));System.out.println("集群健康: "+health.status());System.out.println("活动分片: "+health.activeShards());System.out.println("未分配分片: "+health.unassignedShards());}catch(Exceptione){System.err.println("创建索引失败: "+e.getMessage());}finally{transport.close();restClient.close();}}}

关键步骤是:先建立客户端连接,再创建索引并设置分片和副本,最后等待集群健康到 yellow 并打印分片统计。预期输出会显示acknowledged=true,健康状态为yellow,活动分片数为 3 或 6,未分配分片数取决于集群节点数。

容易改错的地方:如果本地只有一个节点,副本无法分配,waitForStatus(Yellow)可能超时;如果集群是 green,waitForStatus会直接返回。这个例子适合验证“主分片先分配、副本再分配”的顺序。

10. 完整示例二:用 Spring Boot 触发再平衡并记录状态

这个示例更贴近企业实践:Spring Boot 应用启动后,检查集群是否需要再平衡,并打印每个节点的分片数。它的目标是让你看到“应用侧如何观察扩容抖动”。

前置环境:Spring Boot 3.x、Elasticsearch Java API Client 8.x、一个至少两节点的集群。

importco.elastic.clients.elasticsearch.ElasticsearchClient;importco.elastic.clients.elasticsearch.cat.NodesResponse;importco.elastic.clients.elasticsearch.cat.ShardsResponse;importorg.springframework.boot.CommandLineRunner;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.annotation.Bean;@SpringBootApplicationpublicclassEsAllocationProbeApplication{publicstaticvoidmain(String[]args){SpringApplication.run(EsAllocationProbeApplication.class,args);}@BeanCommandLineRunnerprobe(ElasticsearchClientclient){returnargs->{try{NodesResponsenodes=client.cat().nodes(n->n.h("name","node.role","disk.used_percent","heap.percent"));System.out.println("节点状态:\n"+nodes.valueBody());ShardsResponseshards=client.cat().shards(s->s.h("index","shard","prirep","state","node").v(true));System.out.println("分片分布:\n"+shards.valueBody());}catch(Exceptione){System.err.println("探针执行失败: "+e.getMessage());}};}}

关键步骤是:用cat().nodes()查看节点磁盘和堆使用率,用cat().shards()查看分片状态。预期输出中,如果新节点磁盘使用率低,但分片数为 0,说明再平衡还没有触发,或者被过滤规则排除。

容易改错的地方:cat().shards()的v(true)是显示表头,不是“verbose”的意思;h()里的字段名必须和 Elasticsearch 返回字段一致,否则会得到空列。这个示例适合放在运维探针或健康检查接口里,定时记录分片分布,帮助判断扩容后是否均衡。

11. 完整示例三:生产排障案例——磁盘水位线导致写入失败

这个案例不写完整 Java 类,而是给出一个可复现的排障流程。输入是“某个索引写入返回 403 或 cluster_block_exception”,步骤和结果如下。

第一步,查看集群健康说明:

GET _cluster/health?level=indices

如果返回里出现index.blocks.read_only_allow_delete,说明磁盘 flood 水位线已触发。

第二步,确认磁盘使用率:

GET _cat/allocation?v&h=node,shards,disk.used_percent,disk.avail,disk.total

找到disk.used_percent超过 95% 的节点。

第三步,临时解除只读:

PUT _all/_settings{"index.blocks.read_only_allow_delete":null}

第四步,清理旧索引或扩容磁盘,然后把水位线调回安全值。如果你只是临时调高水位线,要记录变更并在清理后恢复。

预期结果是写入恢复,但根因仍在磁盘。这个案例的边界是:解除只读不会自动清理磁盘,如果磁盘仍然超过 flood 水位线,下一次写入还会被阻断。

12. 设计取舍:均衡、恢复速度与写入延迟

分片分配与再平衡的取舍,本质上是三个目标的平衡:

目标倾向配置代价
均衡优先提高并发再平衡、降低水位线恢复占用网络和磁盘,写入延迟上升
恢复速度优先提高node_concurrent_recoveries搜索和写入可能抢不到 CPU
写入稳定优先降低并发恢复、保守水位线扩容后均衡慢,热点可能持续

没有一组参数适合所有集群。判断依据是:你的集群是读多写少还是写多读少?扩容时能否接受短时延迟上升?数据节点磁盘是否一致?把这些条件写清楚,再决定参数。

一个实用原则是:先保证不触发 flood 水位线,再谈均衡。因为 flood 会让写入直接失败,而均衡只是“不够快”。在磁盘紧张的集群里,盲目提高再平衡并发,只会让磁盘更快被写满。

13. 常见误区

第一个误区:以为“加节点就会自动均衡”。实际上,再平衡受 decider 过滤和并发阈值约束,新节点可能因为过滤规则、水位线、分片数限制而接不到分片。

第二个误区:把cluster_concurrent_rebalance和node_concurrent_recoveries混为一谈。前者控制整个集群同时搬多少分片,后者控制单个节点同时恢复多少分片。两者影响的范围不同。

第三个误区:认为副本分片越多越安全。副本越多,写入要同步的节点越多,恢复时搬运的数据也越多。分片和副本数量要结合数据量、节点数和写入吞吐来定。

第四个误区:磁盘水位线只看剩余空间。Elasticsearch 默认按使用率百分比判断,不是按绝对值。小磁盘节点更容易触发 high 和 flood。

14. 生产实践建议

第一,保持数据节点磁盘规格一致。如果必须混用,按最小磁盘设置统一水位线,或者用index.routing.allocation把大索引绑定到大磁盘节点。

第二,扩容前先确认_cat/allocation里没有节点接近 high 水位线。否则新节点加入后,老节点仍在触发搬离,会和新节点接收分片叠加,造成更大抖动。

第三,给恢复留出带宽。在业务低峰期执行扩容,或者临时降低搜索负载。恢复和搜索共享 page cache,磁盘随机读会互相影响。

第四,用_cluster/allocation/explain做第一诊断工具。不要一上来就重启节点,重启会触发更多恢复,可能让问题更糟。

第五,监控分片分布和恢复队列。至少记录每个节点的分片数、磁盘使用率、_cat/recovery中的活动恢复数。

15. 排障清单

当你遇到扩容抖动或分片不均衡时,按下面顺序检查:

  1. GET _cluster/health?level=indices:是否有 red 或 yellow,是否有只读块。
  2. GET _cat/allocation?v:各节点分片数、磁盘使用率是否异常。
  3. GET _cat/shards?v&h=index,shard,prirep,state,node:是否有长期RELOCATING或UNASSIGNED。
  4. GET _cluster/allocation/explain:具体是哪个 decider 拒绝分配。
  5. GET _cluster/settings?include_defaults=true&flat_settings=true:再平衡、恢复、水位线配置是否被改过。
  6. GET _cat/recovery?v&active_only=true:恢复卡在哪个阶段。
  7. 检查节点日志中是否有disk watermark、allocation decider、failed to relocate关键字。

这张清单的顺序是“先看结果,再看原因,最后看配置和日志”。不要跳过第 4 步,它通常能直接给出答案。

16. 面试与复盘问题

  1. allocation decider 的返回值NO、THROTTLE、YES分别意味着什么?
  2. 为什么新节点加入后,分片不会立刻平均迁移?
  3. cluster_concurrent_rebalance和node_concurrent_recoveries的区别是什么?
  4. 磁盘水位线的 low、high、flood 三个阶段分别会触发什么行为?
  5. 一个分片长期处于RELOCATING,你会按什么顺序排查?
  6. 主分片恢复和副本分片恢复的先后关系是什么?
  7. 如何用_cluster/allocation/explain判断是磁盘问题还是过滤规则问题?
  8. 扩容时如何避免写入延迟飙升?

这些问题可以用于团队复盘,也可以用于面试中考察候选人对 Elasticsearch 集群行为的理解。

17. 总结

把整篇文章收回到一张框架图:

触发事件 | v AllocationService 收集待分配分片 | v AllocationDecider 链过滤节点 | 路由 / 磁盘水位 / 并发 / 过滤规则 v ShardsAllocator 权重排序 | v ClusterState 更新,分片进入 INIT / RELOCATING | v 节点执行 shard 恢复:先主后副,先小后大 | v 再平衡持续调整,直到分布接近均衡

你需要记住三件事:第一,分片分配是“过滤 + 排序”,不是简单的轮询;第二,扩容抖动通常来自并发恢复、磁盘水位线和过滤规则,而不是节点数量本身;第三,_cluster/allocation/explain是定位分配问题的第一入口。

当你能把“谁在什么条件下做了什么、结果怎样”说清楚,Elasticsearch 的分片分配与再平衡就不再是黑盒,而是一套可以观测、可以调参、可以复盘的工程机制。

18. 参考资料

  1. Elasticsearch 官方文档:Cluster-level shard allocation and routing settings
  2. Elasticsearch 官方文档:Shard allocation filtering
  3. Elasticsearch 官方文档:Disk-based shard allocation
  4. Elasticsearch 官方文档:Cluster allocation explain API
  5. Elasticsearch 官方文档:Index recovery
  6. Elasticsearch 官方文档:CAT shards API、CAT allocation API、CAT recovery API
  7. Elasticsearch 官方文档:Java API Client
  8. 《Elasticsearch: The Definitive Guide》,Clinton Gormley、Zachary Tong 著

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询