主题
ES 生产踩坑
一句话:ES 的生产故障大多集中在五处:master 稳定性、mapping 爆炸、磁盘水位、深分页和 bulk 队列;它们的共同点是「配置/查询模式在数据量上来后才暴露」,靠加机器通常治标不治本。
适用版本:Elasticsearch 8.x。6.x 的 minimum_master_nodes、type 等写法已不适用。
为什么需要
ES 的默认值适合入门,不适合所有生产形态:动态 mapping 会悄悄长出上千字段,from+size 在深页时会把 coordinating node 打 OOM,磁盘到 95% 会直接只读,bulk 队列满了会引发超时重试的恶性循环。下面按「现象 → 证据 → 根因 → 处置」列出五类高频问题。
核心机制
这五类故障的共性不是某个参数写错,而是默认值按「单机、浅页、小字段、低并发」设计,数据量上来后同时撞上资源边界:master 选举要多数派,候选少于 3 个挂一个就选不出主;每个字段都有索引和 doc_values,动态 mapping 会把字段数推到 1000 上限;磁盘 85% 停止分配、90% 尝试迁移、95% 只读;深分页让每个 shard 先返回 from+size 条再归并且挤爆 coordinating node 内存;write 线程池队列有界,bulk 过大或并发过高就返回 429/rejected。
最小上手
bash
# ES 8.x 单节点,10 分钟内可复现下面的检查命令
docker run -d --name es -p 9200:9200 \
-e discovery.type=single-node -e xpack.security.enabled=false \
docker.elastic.co/elasticsearch/elasticsearch:8.15.0
curl "localhost:9200/_cluster/health?pretty"
curl "localhost:9200/_cat/nodes?v&h=name,node.role,master,heap.percent"
curl "localhost:9200/_nodes/stats/thread_pool?filter_path=**.bulk"预期:_cluster/health 的 status 为 green 或 yellow,_cat/nodes 只有一个 * master;thread_pool.bulk.queue/rejected 能看出写入是否排队。磁盘水位、mapping 字段数和深分页上限分别用 _cat/allocation、_mapping、from+size 查询验证。
一、master 不稳定 / 误以为脑裂
现象:集群状态频繁变更,分片反复分配、查询间歇性失败;日志里出现 master 选举、节点离开/加入。
根因(8.x):
- master 候选节点少于 3 个,挂一个就没有多数派;
cluster.initial_master_nodes在集群形成后没有移除,重启时可能引导出第二个独立集群;- master 与 data 混部,重写入/重聚合把 master 拖慢;
- 网络分区导致节点反复进出。
注意:7.x+ 用多数派投票,少数派分区选不出 master,「两边各选一个 master」的经典脑裂基本不会发生;真正的问题是多数派丢失或 bootstrap 配置错误。
处置:至少 3 个专用 master 候选,跨机架;cluster.initial_master_nodes 只在首次启动配置;用 _cat/nodes?v&h=name,node.role,master,heap.percent 和 _cluster/health 确认状态;不要在 master 上跑重聚合。
二、Mapping 爆炸
现象:集群变慢、堆上涨、GC 频繁,写入 QPS 断崖式下跌;_cluster/state 或索引 mapping 里有上千字段。
根因:动态 mapping 把日志/JSON 的每个键都建成字段;每个字段都有索引和 doc_values 开销,字段数近似线性放大。
关键默认值:index.mapping.total_fields.limit = 1000(8.x,以官方文档为准)。
处置:
- 关闭动态 mapping,或只对白名单字段开
dynamic:true。 - 日志类 JSON 用
dynamic_templates统一映射;结构不固定的对象用flat_object(7.3+)而不是无限展开。 - 监控每个索引的字段数;接近 1000 时先治理,不要等写入报错。
三、磁盘满 → 索引只读
现象:写入报 blocked by: FORBIDDEN/12/index read-only / allow delete (api)。
根因:磁盘水位保护按三档递进,最后一档直接把索引锁成只读:
| 水位 | 默认阈值 | 动作 |
|---|---|---|
| low | 85% | 不再向该节点分配新分片 |
| high | 90% | 尝试迁走分片 |
| flood_stage | 95% | 索引变为只读 |
处置:75% 就告警;到只读时先删/关闭旧索引或扩盘,再 PUT /index/_settings {"index.blocks.read_only_allow_delete": null} 解除;不要关闭水位保护来「先写进去」。长期靠 ILM、forcemerge 旧索引、冷热分层。
四、深分页把集群拖垮
现象:某个查询触发 GC overhead limit exceeded,coordinating node OOM,其他查询一起失败。
根因:from=50000, size=100 时每个 shard 都要取 50100 条候选,多分片归并时大量 _source 同时驻留内存;index.max_result_window 默认 10000,超过直接报错。
处置:
- 列表页用
search_after+ PIT,排序要确定,用唯一字段或_shard_doc做 tiebreaker。 - 全量导出用 PIT/scroll,但不要放在用户请求链路里。
- 不要为了「支持深页」把
max_result_window调大;那只是把 OOM 推迟。
五、bulk 队列打满 → 写入雪崩
现象:写入延迟暴涨,bulk 返回 timeout,客户端重试,队列更深,最后大量 rejected。
根因:单次 bulk 太大(10MB+)或并发太高(几十个线程同时 bulk),write 线程池队列满,请求排队后超时。
处置:
- bulk 按 5–15MB 控制,不按条数;客户端按响应动态调整。
- 写入线程数不超过 CPU 核心数;对 429/rejected 做退避重试,不要立即重发。
- 监控
_nodes/stats/thread_pool的bulk.queue、rejected,以及_nodes/stats/indices/indexing的延迟。
生产动作
批量导入:
json
PUT /my_index/_settings
{ "index": { "refresh_interval": "-1", "number_of_replicas": 0 } }
// 导入完成后恢复
PUT /my_index/_settings
{ "index": { "refresh_interval": "1s", "number_of_replicas": 1 } }
POST /my_index/_forcemerge?max_num_segments=1 // 只对不再写入的索引refresh_interval=-1关闭自动 refresh,减少小 Segment;不是 0。- 导入期
number_of_replicas=0省掉副本同步,导完必须恢复。 index.translog.flush_threshold_size默认 512MB;调大减少 flush 次数,但会延长恢复重放时间。forcemerge只对只读索引做,写入型索引频繁 forcemerge 会和正常 merge 抢 IO。
查询侧:
- 能用
filter就不用must;filter不算分、可缓存。 - 只返回需要的字段:
_source: ["title","price"]。 - 排序/聚合用
keyword或数值字段;text 排序会走 fielddata,吃堆。 - 堆内存 ≤32GB(保留压缩指针),且不超过物理内存 50%,其余留给 PageCache。
一个容易写反的结论:让 ES 自动生成 _id 通常比显式指定 _id 更快,因为显式 ID 需要检查文档是否已存在、读取版本信息;但需要幂等重试、外部系统对账时,显式 ID 仍然必要。不要为了「性能」盲目改成自增 ID。
边界
- 这些坑不是靠加节点能根治:mapping 爆炸、深分页、bulk 队列首先是配置和查询模式问题。
forcemerge、reroute、关副本都是高成本操作,必须在维护窗口做。- 副本不是备份;磁盘只读时先止血,再补 snapshot。自动 ID 更快,但不等于所有场景都该用;幂等和更新需要显式 ID。
讲清楚
ES 生产问题集中在五处:master 候选少于 3 个或 bootstrap 配置错误导致选主不稳定;动态 mapping 把字段数推到 1000 上限;磁盘 85/90/95 三档水位最后变成只读;
from+size深分页让 coordinating node OOM;bulk 太大或并发太高把 write 队列打满。处置顺序都是先看证据再动手:master 看
_cat/nodes和_cluster/health,mapping 看字段数,磁盘看水位,深分页换search_after+ PIT,bulk 按 5–15MB 和 CPU 核心数控制。导入期关 refresh、关副本,导完恢复,forcemerge 只对只读索引做。还有一个常被写反的点:自动生成_id通常更快,显式 ID 是为了幂等,不是为了性能。