集群/_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为什么拒绝、再按磁盘、副本数、节点数、分配开关逐项排除。