跳到正文

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)。

根因:磁盘水位保护按三档递进,最后一档直接把索引锁成只读:

水位默认阈值动作
low85%不再向该节点分配新分片
high90%尝试迁走分片
flood_stage95%索引变为只读

处置: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 是为了幂等,不是为了性能。

把一门技术讲透到既懂也会用