Elasticsearch集群报red状态与分片UNASSIGNED的排查方法(allocation explain定位、磁盘水位线与副本数调整)

集群/_cluster/health返回status是red,unassigned_shards不为0,Kibana的索引管理页里一堆分片显示UNASSIGNED。写入侧跟着报cluster_block_exception,或者index上带了个read-only-allow-delete块,业务方看到的就是写入失败。这种情况先别急着重启节点,搞清楚是哪一片没分配、为什么不分配,比反复重启有用得多。


先看清没分配的是主分片还是副本

red和yellow的分界就在这一条:主分片没落位是red,缺的只是副本则是yellow。查起来一条命令:

curl -s "http://127.0.0.1:9200/_cat/shards?v&h=index,shard,prirep,state,node,unassigned.reason" | grep UNASSIGNED

prirep列里p是主分片、r是副本。unassigned.reason会给出一个方向:NODE_LEFT是分片所在的节点掉过线,INDEX_CREATED是刚建的索引还没落位,ALLOCATION_FAILED是分配失败,磁盘和文件损坏最常见,CLUSTER_RECOVERED是整个集群重启后等着节点回来。


让集群自己说出原因

光看reason还不够细。allocation explain会把每一个决策器(decider)的结论列出来,哪个返回NO,卡点就在哪:

curl -s "http://127.0.0.1:9200/_cluster/allocation/explain?pretty"

要精确查某一个分片,把索引名、分片号和主副标识放进请求体:

curl -s -H "Content-Type:application/json" "http://127.0.0.1:9200/_cluster/allocation/explain?pretty" -d '
{
  "index": "app-log-2026.10.01",
  "shard": 3,
  "primary": false
}'

返回里的allocate_explanation是一句人话结论,deciders数组里disk、node_left、filter、awareness、same_shard、throttling、enable这些项逐个给出YES或NO。见过最典型的一条是the node is above the low watermark cluster setting,一句话就指向磁盘。


磁盘水位线是最常见的一类

Elasticsearch有三个默认阈值:85%是low水位,超过之后不再往该节点分配副本;90%是high水位,超过之后集群开始把分片往外迁;95%是flood stage,该节点上有分片的索引全部被加上只读块。日志里的原文是high disk watermark [90%] exceeded on后面跟节点名,或者flood stage disk watermark [95%] exceeded on,直接搜这两句比翻配置文件快。

救急时可以动态抬高阈值,把low抬到90%、high抬到95%:

curl -X PUT -H "Content-Type:application/json" "http://127.0.0.1:9200/_cluster/settings" -d '
{
  "transient": {
    "cluster.routing.allocation.disk.watermark.low": "90%",
    "cluster.routing.allocation.disk.watermark.high": "95%",
    "cluster.routing.allocation.disk.watermark.flood_stage": "97%"
  }
}'

腾出空间之后还有两件事。被加上只读块的索引不一定自动解锁,手动清一次:

curl -X PUT -H "Content-Type:application/json" "http://127.0.0.1:9200/_all/_settings" -d '
{"index.blocks.read_only_allow_delete": null}'

再让集群把之前放弃的分配重试一遍:

curl -X POST "http://127.0.0.1:9200/_cluster/reroute?retry_failed=true"

实测过几套集群,光清理磁盘到位了,只读块却还挂着,写入照样失败,后两步不能省。


副本数跟节点数不匹配

单节点集群却沿用了默认的number_of_replicas等于1,那些副本永远分不出去,集群会一直是yellow。测试环境图省事可以直接改成0:

curl -X PUT -H "Content-Type:application/json" "http://127.0.0.1:9200/app-log-*/_settings" -d '
{"index.number_of_replicas": 0}'

多节点时按节点数减一来配:3个数据节点配2副本能全部落位,配3副本就必然留一个UNASSIGNED。之前接手过一套从2节点扩到3节点的集群,索引副本数还停在1,扩容带来的余量一点都没用上,就是没算这笔账。


分片总数撞上限

自ES 7.0、7.17起有cluster.max_shards_per_node这个限制,默认1000,按数据节点数算总配额。索引模板里分片数设得太大,或者日志按天建索引又不做生命周期管理,几个月就会撞上,explain里会提示当前分片数已超过每个节点的配额。

curl -s "http://127.0.0.1:9200/_cluster/settings?include_defaults=true&flat_settings=true" | grep max_shards

治本的办法是给日志类索引配ILM,滚到一定天数就收缩或者删除,别让它一直堆着。ILM本身自ES 6.6、6.7引入,只是那会儿分片限制还没这么紧,很多老集群升级到8.x之后才第一次遇到这问题。


节点掉线与延迟分配

NODE_LEFT这一类先确认节点数对不对:

curl -s "http://127.0.0.1:9200/_cat/nodes?v&h=ip,name,heap.percent,ram.percent,disk.used_percent,node.role"

少一个节点就去那台机器看日志,常见是堆内存被打满、磁盘故障或者网络分区。节点回来之后分片会自动恢复,不用手动干预。

这里有个容易误判的点:index.unassigned.node_left.delayed_timeout默认1分钟,节点掉线后集群不会立刻把副本提为主分片,就是为了避免一次短暂重启引发大量数据搬运。急着恢复时临时改成0:

curl -X PUT -H "Content-Type:application/json" "http://127.0.0.1:9200/_all/_settings" -d '
{"index.unassigned.node_left.delayed_timeout": "0"}'

改完记得改回去,否则以后每次节点重启都会立刻触发一轮分片重分配,集群长时间处在恢复状态。


分配开关没拧回来

滚动升级或者做节点迁移时经常把cluster.routing.allocation.enable设成primaries或none,收尾时忘记改回all,之后建的索引分片就一直UNASSIGNED。查一下这个设置,值不是all就改回来:

curl -X PUT -H "Content-Type:application/json" "http://127.0.0.1:9200/_cluster/settings" -d '
{"transient": {"cluster.routing.allocation.enable": "all"}}'

另外两类也在这个位置查:一种是配了allocation awareness按机架或可用区感知,而某个区里的节点全没了,分片找不到合规的节点;另一种是索引级或集群级的include、exclude过滤规则把节点全排除掉了。explain里的filter和awareness两个decider会返回NO,看一眼就知道。


主分片实在找不回来的时候

只有主分片缺失才会red。数据节点磁盘整体损坏、或者分片文件本身损坏而副本也不存在,这时只剩两条路:从快照恢复,或者接受数据丢失后强制分配一个空主分片。

curl -X POST -H "Content-Type:application/json" "http://127.0.0.1:9200/_cluster/reroute?pretty" -d '
{
  "commands": [
    {"allocate_empty_primary": {"index": "app-log-2026.10.01", "shard": 3, "node": "node-1", "accept_data_loss": true}}
  ]
}'

allocate_stale_primary是拿节点上残留的旧分片数据顶上去,数据可能不完整但比空分片强;allocate_empty_primary直接给一个空分片,那一片的数据就彻底没了。执行前把索引名和分片号核对两遍,这个操作不可撤销。日常把快照生命周期管理配好,真出事才不至于只剩这一条路。

本文涉及的默认阈值与接口在Elasticsearch 8.19.14、9.1.10、9.2.8、9.3.4这几个版本上一致,数据截至2026年10月。排查顺序固定下来就是三步:_cat/shards看是哪一片、explain看decider为什么拒绝、再按磁盘、副本数、节点数、分配开关逐项排除。