故障排查与最佳实践
排查的基本顺序
ES 的故障看起来五花八门,排查路径其实很固定,按这个顺序走,大部分问题五分钟内能定位到方向:
- 看三色:
_cluster/health,green / yellow / red 决定了严重程度。 - 看未分配分片:
_cat/shards?v里筛UNASSIGNED,确认是主分片还是副本。 - 问 ES 为什么:
_cluster/allocation/explain,它会直接告诉你分片卡在哪个决策环节。 - 看节点资源:
_cat/nodes看堆、磁盘、负载,_cat/allocation看磁盘余量。 - 翻日志:
/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 时最忌讳的操作是「删了重建」——数据就真的没了。先确认快照是否可用、副本是否还有救,再决定是恢复还是接受损失。删索引是最后手段。
磁盘水位与只读索引
磁盘写满是 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 |
| fielddata | text 字段被排序/聚合,整份词项进堆 | 改 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 不是解药,只是把炸弹的引线放长。
网上很多「解决 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_clauses | bool 查询子句过多 | 拆分查询、用过滤器合并条件 |
写入报 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 是过载信号,正确应对是客户端指数退避 + 降并发 + 控制批量大小,而不是调大队列。
- 最后,把上面的最佳实践清单当成例行检查表:大部分故障都不是突然发生的,而是长期偏离最佳实践的结果。