跳到主要内容

故障排查与最佳实践

排查的基本顺序​

ES 的故障看起来五花八门,排查路径其实很固定,按这个顺序走,大部分问题五分钟内能定位到方向:

  1. 看三色:_cluster/health,green / yellow / red 决定了严重程度。
  2. 看未分配分片:_cat/shards?v 里筛 UNASSIGNED,确认是主分片还是副本。
  3. 问 ES 为什么:_cluster/allocation/explain,它会直接告诉你分片卡在哪个决策环节。
  4. 看节点资源:_cat/nodes 看堆、磁盘、负载,_cat/allocation 看磁盘余量。
  5. 翻日志:/var/log/elasticsearch/集群名.log,以及 gc.log、deprecation.log。
先止损,再定位

线上故障的第一优先级是恢复服务,不是搞清楚根因。读请求报错就先切流量、写请求 429 就先降并发,等业务恢复了再慢慢查。反过来,一边查一边让业务继续挨打,是最糟的处理方式。

集群 yellow:副本没地方放​

yellow 的含义很精确:所有主分片都分配了,但有副本分片没分配。数据是全的、读写都正常,缺的是冗余。

先判断是不是「正常 yellow」​

最常见的 yellow 来自单节点集群:索引默认 1 个副本,但只有一个数据节点,副本无处安放。这种情况不是故障,把副本设为 0 就变 green:

PUT /logs-2025.10.25/_settings
{
"index": {
"number_of_replicas": 0
}
}

如果集群有 3 个数据节点、副本数是 1,仍然 yellow,那就是真的有问题,继续往下查。

用 allocation explain 定位​

这个接口是分片分配问题的「X 光机」,一定要会用:

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

不传参数时,它默认解释第一个未分配的分片。返回里重点看三段:

{
"index": "logs-2025.10.25",
"shard": 0,
"primary": false,
"current_state": "unassigned",
"unassigned_info": {
"reason": "NODE_LEFT",
"at": "2025-10-25T02:13:41.221Z",
"details": "node_left [AbCdEf123]",
"last_allocation_status": "no_valid_shard_copy"
},
"can_allocate": "no",
"allocate_explanation": "cannot allocate because allocation is not permitted to any of the nodes",
"node_allocation_decisions": [
{
"node_name": "es-node-1",
"deciders": [
{ "decider": "disk_threshold", "decision": "NO", "explanation": "the node is above the high watermark cluster setting" }
]
}
]
}
  • unassigned_info.reason:分片为什么变成未分配。NODE_LEFT(节点离开)、INDEX_CREATED(新建索引还没分配)、ALLOCATION_FAILED(分配失败)、REALLOCATED_REPLICA(副本被重新分配)等。
  • can_allocate:yes 能分配、no 不能、throttled 被限流。no 时看 allocate_explanation。
  • node_allocation_decisions:每个候选节点的 deciders 数组里,每个决策器为什么投反对票。上面这个例子里 disk_threshold 说节点超过高水位,答案就很明确了。

常见原因与处理​

reason / 决策器含义处理
INDEX_CREATED + can_allocate: yes刚创建,正在分配等几秒,通常会自己好
NODE_LEFT节点下线,副本随之丢失节点恢复上线;或补节点、减副本
disk_threshold节点磁盘超过水位清理数据或扩容,见下一节
same_shard主副本要落在同一节点加数据节点,或减少副本数
filter(决策器)分配过滤规则排除了所有节点检查 index.routing.allocation.* 和节点属性
replica_after_primary_active主分片还没就绪等主分片恢复
max_retry(决策器)分配重试次数用完修复根因后 POST /_cluster/reroute?retry_failed=true
no_valid_shard_copy找不到可用的分片副本数据通常只能从快照恢复,或接受数据丢失

重试分片分配(修完根因之后):

curl -s -X POST "http://localhost:9200/_cluster/reroute?retry_failed=true&pretty"
重试不是万能的

retry_failed=true 只是把失败计数清零、让 ES 重新尝试分配。如果磁盘还是满的、节点还是不满足条件,重试只会再次失败。先看 explain 里的根因,再决定要不要重试。

集群 red:主分片丢了​

red 比 yellow 严重一个量级:至少有一个主分片没分配,这块分片上的数据读不到,相关查询会返回 unavailable_shards_exception。red 必须当成故障处理。

定位是哪个索引、哪个分片​

# 找出所有未分配的分片,重点看 pri 列为 p 的
curl -s "http://localhost:9200/_cat/shards?v" | grep UNASSIGNED

输出里 pri 是 p 的就是主分片。拿到索引名和分片号后,用 explain 问原因:

GET /_cluster/allocation/explain
{
"index": "orders",
"shard": 2,
"primary": true
}

按原因处理​

原因一:节点临时离线,数据还在磁盘上。 重启节点、恢复网络,等它重新加入集群,分片会自动恢复。如果节点再也回不来,但磁盘还在,可以把磁盘挂到新节点上启动,ES 会识别旧数据。

原因二:磁盘满导致分片损坏或无法分配。 先清理磁盘(见下一节),再重启节点让它重新加入。分片恢复时会从 translog 回放,时间可能比较长,用 _cat/recovery?v&active_only=true 观察进度。

原因三:分片数据损坏。 日志里会出现 corrupt、checksum、failed to read 之类的关键字。这种情况通常只能从快照恢复。如果确认有完好的副本,可以尝试让 ES 用副本提升为主:

POST /_cluster/reroute
{
"commands": [
{ "allocate_stale_primary": {
"index": "orders", "shard": 2,
"node": "es-node-2", "accept_data_loss": true
} }
]
}

allocate_stale_primary 表示「我知道这个分片的数据可能不是最新的,仍然用它当主分片」。这是丢数据的操作,只在确认其他副本都不可用时使用,执行前务必先做快照(如果还能做的话)。

原因四:索引元数据损坏。 极端情况下 _cluster/state 里的索引元数据坏了,只能从快照恢复,或者用 elasticsearch-shard 离线工具修复。这类操作风险高,建议先备份整个数据目录再动手。

red 状态下先别急着删索引

red 时最忌讳的操作是「删了重建」——数据就真的没了。先确认快照是否可用、副本是否还有救,再决定是恢复还是接受损失。删索引是最后手段。

磁盘水位与只读索引​

磁盘写满是 ES 最高频的故障,没有之一。表现为:写入突然全部失败,报 index [xxx] blocked by: [FORBIDDEN/12/index read-only / allow delete (api)],同时集群可能 yellow 或 red。

确认是水位问题​

# 各节点磁盘余量
curl -s "http://localhost:9200/_cat/allocation?v"

# 看哪些索引被设成了只读
curl -s "http://localhost:9200/_cat/indices?v&health=yellow" | head

被水位置为只读的索引,_settings 里会多出 index.blocks.read_only_allow_delete: true。确认一下:

curl -s "http://localhost:9200/logs-2025.10.25/_settings?pretty" | grep read_only

解除只读​

先清磁盘,再解只读——顺序反了马上会再次只读。

# 1. 扩容磁盘,或删除/归档旧索引(先做快照)
curl -s -X DELETE "http://localhost:9200/logs-2025.08.01"

# 2. 确认磁盘降到水位线以下(至少低于 85%)
curl -s "http://localhost:9200/_cat/allocation?v"

然后解除只读。可以单个索引解,也可以全集群批量解:

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

把值设为 null 表示删除这个设置,索引恢复正常可写。ES 在磁盘水位回落到阈值以下时也会自动解除,但有时需要等一个检查周期(默认 30 秒),手工解除更干脆。

不要靠调高水位消警​

确实腾不出磁盘时,有人会临时把洪水位从 95% 调到 97% 换几个小时。这只是把「写不进去」往后推,磁盘真满之后 ES 连日志都写不了,故障更难恢复。调完必须立刻清理或扩容,处理完改回默认。治本的办法还是那三样:ILM 自动删除过期索引、快照后归档冷数据、扩容磁盘。

JVM 堆压力与 GC​

堆打满的表现很有迷惑性:查询变慢、偶发超时、批量写入被拒,严重时节点「假死」——进程还在,但响应极慢,master 判定它失联,开始重新分配分片,恢复又带来新一轮 IO 压力,雪崩就这么来的。

确认堆的状态​

# 堆使用率、GC 次数和耗时
curl -s "http://localhost:9200/_nodes/stats/jvm?pretty"

# 哪个线程在吃 CPU(排查卡顿时最有用的一条命令)
curl -s "http://localhost:9200/_nodes/hot_threads"

hot_threads 会输出各节点当前最忙的线程栈。如果看到大量线程卡在 merge、Lucene 的查询路径、或者 bulk 处理上,方向就清楚了。GC 相关的指标里,重点看 old 收集器的 collection_time_in_millis 增速——老年代 GC 频繁且耗时长,基本就是堆不够或者有大对象。

常见成因​

成因表现处理
分片数过多堆里全是分片元数据,基线堆就很高减少分片、合并索引、扩节点
大聚合terms 的 size 设成几万,一次请求吃光堆限制 size、用 composite 分页聚合
深分页from + size 很大,协调节点归并吃内存改 search_after
fielddatatext 字段被排序/聚合,整份词项进堆改 keyword,见下一节
批量太大单次 bulk 几百 MB,请求缓冲撑爆堆控制在 5-15MB
堆设置不当堆太小扛不住,太大失去指针压缩物理内存 50%,不超过 31GB

应急手段​

# 取消正在跑的长任务(比如失控的大聚合)
curl -s "http://localhost:9200/_tasks?pretty"
curl -s -X POST "http://localhost:9200/_tasks/节点ID:任务ID/_cancel"
PUT /_cluster/settings
{ "persistent": { "search.max_buckets": 20000 } }

search.max_buckets 限制单次聚合能建多少个桶(默认 65536),调小可以防止单个查询把堆打爆,但会让大聚合直接报错——这是有意的取舍:宁可让一个查询失败,也不能让整个节点挂掉。

如果堆压力是持续性的,别指望调参数解决,老老实实扩容或者减少分片。堆 85% 以上还硬扛的集群,出事只是时间问题。

fielddata 内存爆炸​

fielddata 是 ES 里最经典的内存炸弹。它的触发条件很简单:对 text 字段做排序或聚合。ES 为了能排序,会把分词后的所有词项加载进堆内存,字段基数越高、文档越多,占用越夸张。

症状​

  • 堆使用率只涨不降,重启后慢慢又涨回去
  • _cat/fielddata 显示某个字段占用几个 GB
  • 日志里出现 CircuitBreakingException,提示 fielddata 超过限制
# 按索引看 fielddata 占用
curl -s "http://localhost:9200/_cat/fielddata?v&bytes=gb"

# 细到字段
curl -s "http://localhost:9200/_nodes/stats/indices/fielddata?level=indices&pretty"

处理​

根治:改 Mapping。 需要排序/聚合的字段用 keyword,或者给 text 字段加 .keyword 子字段,然后 reindex 把数据迁过去。这是唯一干净的办法。

止损:清缓存。 先把已经加载的 fielddata 清掉,能立刻回收一部分堆:

POST /logs-2025.10.25/_cache/clear?fielddata=true

如果暂时改不了 Mapping,只能先清缓存扛一阵、把大聚合停掉,并尽快排期 reindex——fielddata: true 不是解药,只是把炸弹的引线放长。

别顺手开 fielddata: true

网上很多「解决 text 字段不能排序」的答案就是加 fielddata: true,这在测试环境能跑通,上生产就是定时炸弹。正确的解法永远是改字段类型,而不是打开这个开关。

bulk 写入被拒:429 与队列​

批量写入报错,最常见的是 HTTP 429,响应体里是 es_rejected_execution_exception。这不是网络问题,是 ES 在明确告诉你:写入队列满了,我处理不过来。

先看队列状态​

curl -s "http://localhost:9200/_cat/thread_pool/write?v&h=node_name,active,queue,rejected,completed"

write 线程池的默认配置是:线程数等于 CPU 核数,队列长度 10000。queue 持续接近 10000、rejected 在涨,就是过载。

原因分析​

原因判断依据处理
写入量突增QPS 曲线陡增,queue 涨得快客户端限流、扩容数据节点
单批太大请求体几百 MB,处理一条堵一批降到 5-15MB
merge 抢 IO_cat/segments 段多,磁盘 util 高降低写入速率,错峰 forcemerge
副本同步慢主分片写完等副本确认检查副本所在节点负载
refresh 太频繁refresh_interval 是 1s,段生成快写入密集时临时调到 30s
磁盘慢iostat 看到 await 高换 SSD,或减少并发

正确姿势:客户端退避重试​

429 是可重试错误,但绝不能立刻重试——那只会让队列更满。标准做法是指数退避 + 随机抖动:

第 1 次失败:等 200ms + 随机 0-100ms
第 2 次失败:等 400ms + 随机 0-100ms
第 3 次失败:等 800ms + 随机 0-100ms
...
最多重试 5 次,仍失败则落盘/进死信队列,事后补偿

同时降低客户端并发数。很多写入雪崩的根因不是 ES 扛不住,而是客户端看到失败就疯狂重试,把队列彻底压死。

别无脑调大队列

把 thread_pool.write.queue_size 从 10000 调到 100000,看起来「能缓冲更多请求」,实际是把内存压力往后推:请求在队列里排队,延迟从毫秒涨到几十秒,客户端超时重试,队列更满。队列是缓冲,不是容量。

其他高频故障速查​

现象可能原因排查入口
节点启动失败vm.max_map_count 不够、目录权限、堆太大内存不足启动日志、sysctl vm.max_map_count
节点频繁掉线又回来GC 停顿过长、网络抖动、时钟不同步hot_threads、GC 日志、chrony
master 选不出来master 候选节点不足多数派、initial_master_nodes 残留_cat/master、各节点日志
分片数超限单节点分片数超过 cluster.max_shards_per_node(默认 1000)创建索引时报 validation_exception
查询报 too_many_clausesbool 查询子句过多拆分查询、用过滤器合并条件
写入报 mapper_parsing_exception字段类型和 Mapping 不符看报错里的字段名,改数据或 Mapping
建索引报 invalid_index_name_exception名字带大写或非法字符索引名必须小写,不能用空格和特殊符号
9200 连不上进程未启动、network.host 绑定错、防火墙ss -lntp、_nodes、安全组

最佳实践清单​

最后收一份可以直接拿去对照的清单,按「部署 → 索引 → 查询 → 运维」四块整理。

部署​

  • ✅ master 节点 3 个且为奇数,生产环境 master 与 data 角色分离
  • ✅ 堆内存 -Xms 等于 -Xmx,不超过物理内存 50%,不超过 31GB
  • ✅ vm.max_map_count=262144、关闭 swap、文件描述符给足
  • ✅ 生产开启 xpack.security 和 TLS,9200 只监听内网
  • ✅ 数据盘、日志盘分开,监控磁盘水位
  • ✅ 至少 3 个数据节点,副本数至少 1

索引​

  • ✅ Mapping 显式声明,业务索引 dynamic: strict
  • ✅ 精确匹配/排序/聚合用 keyword,全文检索用 text + 合适的分词器
  • ✅ 单分片 20-50GB,单节点分片数不超过「堆 GB × 20」
  • ✅ 主分片数一次定对,用「索引模板 + 别名」管理,别手工建索引
  • ✅ 时间序列数据配 ILM,自动 rollover / 降冷 / 删除
  • ✅ 快照仓库提前配好并验证,SLM 自动备份,定期做恢复演练

查询​

  • ✅ 精确过滤条件放 bool.filter,利用缓存
  • ✅ 分页用 search_after,别深翻 from + size
  • ✅ 聚合加 size: 0,terms 的 size 别开太大
  • ✅ 避免 wildcard 前缀通配、避免脚本聚合,这些是 CPU 杀手
  • ✅ 大查询设置 timeout,别让一个请求拖垮协调节点

运维​

  • ✅ 每天看三色、堆、磁盘、线程池拒绝数
  • ✅ 变更前先做快照,升级前读 Breaking Changes
  • ✅ forcemerge 只对只读历史索引做,安排在低峰期
  • ✅ 监控接 Prometheus + Grafana,告警覆盖非 green、堆高、拒绝、未分配分片
  • ✅ 保留一份「故障处理手册」,把本次踩过的坑写进去

小结​

  • 排查固定顺序:三色 → 未分配分片 → allocation explain → 节点资源 → 日志。_cluster/allocation/explain 是定位分片问题的核心工具,它会直接告诉你哪个决策器投了反对票。
  • yellow 是缺副本,单节点下属于正常;red 是缺主分片,必须当故障处理,先确认快照和副本,再决定恢复还是接受损失,别急着删索引。
  • 磁盘水位三级:85% 停止分配、90% 主动迁移、95% 索引只读。解除只读前必须先清磁盘,顺序反了会立刻复发。
  • 堆压力的根因通常是分片过多、大聚合、深分页、fielddata;hot_threads 和 _nodes/stats/jvm 是两把好用的手术刀。
  • fielddata 是内存炸弹,唯一根治办法是把字段改成 keyword,别用 fielddata: true 糊弄。
  • bulk 429 是过载信号,正确应对是客户端指数退避 + 降并发 + 控制批量大小,而不是调大队列。
  • 最后,把上面的最佳实践清单当成例行检查表:大部分故障都不是突然发生的,而是长期偏离最佳实践的结果。