ES 反模式与排查宝典

6 阅读1小时+

概述

衔接前文段落
本系列从 ES 特性全景出发,逐层深入到倒排索引、写入持久化、映射建模、查询聚合、中文分词、集群分布式、安全、Spring 整合和运维监控。然而,知晓原理不等于能在生产环境正确运用。本文作为系列反模式排查的核心篇章,将前面的所有核心技术点投射到真实的故障场景中,通过“反模式→排查→修复”的闭环,帮助读者将分散的知识点内化为系统化的排障直觉。

总结性引言
“集群 RED 无法写入”“搜索 P99 突然飙到 5 秒”“聚合查询导致节点 OOM”“分词更新后搜索召回率暴跌”——这些 ES 相关的线上故障,根因往往不是某个技术本身有问题,而是使用方式违背了它的设计本意。本文将 ES 生态中最常见的反模式归纳为五大领域,以 24 个真实案例为载体,严格遵循“错误示例→现象描述→排查思路→根因分析→修正方案→最佳实践”的六步诊断法,并整合全系列诊断工具与标准化排查决策树,为读者提供一套可复用的、逻辑严密的排障体系。

核心要点

  • 五大反模式领域(设计+运行时):索引映射、查询聚合、写入性能、集群架构、安全,共 24 个案例。
  • 六步诊断法:从错误代码到修复的标准化流程,每个案例根因显式关联前文原理,并引入源码级剖析。
  • 诊断工具集与决策树:覆盖 ES 端全工具链与四大典型故障的精确决策路径。

文章组织架构图

flowchart LR
    Start[反模式与排查宝典]
    Start --> M1[1. 索引与映射反模式]
    M1 --> M1D[设计反模式]
    M1D --> M1D1[dynamic=true 字段爆炸]
    M1D --> M1D2[text 误用于聚合字段]
    M1D --> M1D3[nested 过度使用]
    M1D --> M1D4[分片数过大]
    M1D --> M1D5[分片数过小]
    M1 --> M1R[运行时反模式]
    M1R --> M1R1[refresh_interval 失衡]
    M1R --> M1R2[段合并限速过低]
    
    M1 --> M2[2. 查询与聚合反模式]
    M2 --> M2D[设计反模式]
    M2D --> M2D1[深分页 from+size]
    M2D --> M2D2[前缀通配全量扫描]
    M2 --> M2R[运行时反模式]
    M2R --> M2R1[terms 聚合 size 过大]
    M2R --> M2R2[未使用 filter 上下文]
    M2R --> M2R3[script_score 开销过大]
    
    M2 --> M3[3. 写入与性能反模式]
    M3 --> M3D[设计反模式]
    M3D --> M3D1[refresh_interval=1s 日志场景]
    M3 --> M3R[运行时反模式]
    M3R --> M3R1[force_merge 在线索引]
    M3R --> M3R2[Bulk 批大小不当]
    M3R --> M3R3[Translog 异步刷盘]
    
    M3 --> M4[4. 集群与架构反模式]
    M4 --> M4D[设计反模式]
    M4D --> M4D1[Master/Data 混合部署]
    M4D --> M4D2[分配感知未配置]
    M4D --> M4D3[磁盘水位线不当]
    M4 --> M4R[运行时反模式]
    M4R --> M4R1[集群 RED 未及时处理]
    
    M4 --> M5[5. 安全反模式]
    M5 --> M5D[设计反模式]
    M5D --> M5D1[未启用 TLS]
    M5D --> M5D2[匿名访问未禁用]
    M5D --> M5D3[默认密码未修改]
    M5 --> M5R[运行时反模式]
    M5R --> M5R1[API Key 权限过大]
    
    M5 --> M6[6. 诊断工具集与映射表]
    M6 --> M7[7. 标准化排查决策树]
    M7 --> M8[8. 面试高频故障排查专题]

架构图说明

  1. 总览说明:全文 8 个模块以前 5 个反模式领域的案例分析为主体,每个案例采用设计/运行时双视角剖析,后接诊断工具集和决策树,最后以面试故障排查专题收束。
  2. 逐模块说明:模块 1-5 覆盖 ES 全生命周期中的典型错误,每个案例根因直接引用前文原理并深入源码;模块 6 提供可打印的速查工具箱;模块 7 绘制故障决策路径图;模块 8 面试巩固。
  3. 关键结论:ES 反模式的根因往往可追溯到前 10 篇的核心原理。掌握六步诊断法、设计/运行时双视角分析范式、标准化决策树,是将理论转化为排障能力的关键。

1. 索引与映射反模式

1.1 设计反模式:dynamic=true 导致字段爆炸与 mapping 膨胀

1.1.1 错误示例

PUT /logs
{
  "mappings": {
    "dynamic": true,
    "properties": {
      "timestamp": {"type": "date"},
      "message": {"type": "text"}
    }
  }
}

写入端不断添加任意嵌套字段,如 user.agent.details.versionerror.stacktrace.line 等,未做规范化处理。

1.1.2 现象描述

  • 索引字段总数迅速超过 index.mapping.total_fields.limit(默认 1000),新写入被拒绝,返回错误:
{
  "error": {
    "type": "illegal_argument_exception",
    "reason": "Limit of total fields [1000] in index [logs] has been exceeded"
  }
}
  • 集群状态更新缓慢,主节点 CPU 和内存使用率升高。通过 GET _cat/nodes?v 观察到 master 节点 heap.percent 持续增长。
  • 索引元数据体积膨胀,GET _cluster/state 返回的数据量巨大,导致主节点发布集群状态的延迟显著增加,日志中出现 publish cluster state took [3s] 等警告。

1.1.3 排查思路

  1. 使用 GET /logs/_mapping 统计字段数量,或执行 GET /logs/_field_caps?fields=* 获取全局字段信息。
  2. 查看索引设置:GET /logs/_settings,确认 index.mapping.total_fields.limit 是否维持默认或已被手动调高。
  3. 检查写入端代码,确认是否将未受控的 JSON 直接写入 ES,可通过抓取写入日志或使用 _cat/indices?v&h=docs.count 对比文档数与预期是否相符。
  4. 使用 GET _cluster/state 并观察元数据大小,或使用 _cat/pending_tasks 查看是否有长时间未完成的 put-mapping 任务。

1.1.4 根因分析 ES 的动态映射(dynamic: true)会为每个新出现的 JSON 字段自动推断类型并在 mapping 中创建定义。每个字段都需要在 Lucene 索引中构建对应的倒排索引、doc_values 等数据结构,这直接导致:

  • 内存开销:索引 mapping 作为集群状态的一部分驻留在主节点堆内存中,大量字段会导致集群状态体积剧增。主节点在发布状态更新时需要序列化和广播,串行处理,可能堵塞其他任务。
  • 磁盘与 I/O 开销:字段越多,写入时需更新的数据结构越多,段合并时处理的数据也越多,容易引起 I/O 竞争。
  • 稳定性风险:当字段数达到 total_fields.limit 后,索引阻塞,写入完全中断。

根因详见第 4 篇映射与文档建模:动态映射模板和 dynamic 策略的原理。同时,与第 2 篇倒排索引结构相关联:每个字段都会建立倒排索引,字段爆炸意味着倒排索引数量爆炸。

1.1.5 修正方案

  • 禁用动态映射:将 dynamic 设置为 falsestrict
PUT /logs_v2
{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "timestamp": {"type": "date"},
      "message": {"type": "text"},
      "level": {"type": "keyword"},
      "service": {"type": "keyword"}
    }
  }
}
  • 若仍需保留部分灵活性,使用 dynamic_templates 将未知字符串映射为 keyword 并截断:
{
  "dynamic_templates": [
    {
      "strings_as_keyword": {
        "match_mapping_type": "string",
        "mapping": {
          "type": "keyword",
          "ignore_above": 256
        }
      }
    }
  ]
}
  • 通过 ILM 滚动索引,使旧索引只读,新索引规划好 mapping。
  • 若字段数已超标,可临时增加 total_fields.limit 以恢复写入,然后尽快 _reindex 到新索引。

1.1.6 最佳实践

  • 对日志、事件等半结构化数据,优先采用 dynamic: false,并将原始 JSON 存入 _source,核心字段显式定义。
  • 开启字段数量监控报警,设置告警阈值(如 800 fields)。
  • 在索引模板中统一控制 mapping,避免各索引各自为政。

1.2 设计反模式:text 误用于聚合字段

1.2.1 错误示例

PUT /products
{
  "mappings": {
    "properties": {
      "category": { "type": "text" },
      "price": { "type": "float" }
    }
  }
}

执行按类别统计的 terms 聚合:

POST /products/_search
{
  "size": 0,
  "aggs": {
    "categories": {
      "terms": { "field": "category" }
    }
  }
}

1.2.2 现象描述

  • 聚合结果中出现多个含义相同但拼写或大小写不同的桶,例如 “Electronics”、“electronics”、“Electronics ”。
  • 节点 fielddata 内存使用飙升,通过 GET _nodes/stats/indices/fielddata 可看到 memory_size_in_bytes 达到数 GB。
  • 查询变慢,尤其当聚合字段基数较高时,CPU 消耗巨大。
  • 日志中出现 CircuitBreakingException,提示 [fielddata] Data too large

1.2.3 排查思路

  1. GET /products/_mapping 确认 categorytext 类型。
  2. GET _nodes/stats/indices/fielddata?fields=category 观察 memory_size
  3. 使用 _profile 查看聚合阶段,build_ordinalscollect 时间占比极高。
  4. 检查集群堆内存使用 GET _nodes/stats/jvm,确认 old gen 使用接近上限。

1.2.4 根因分析 text 类型在索引时会经过分析器(如 standard analyzer)分词,例如 “Electronics” 可能被分成 “electron” 等多个 token,原始字符串并未直接存储为可用于聚合的文档值。为了支持聚合、排序等操作,ES 必须将倒排索引中的 term 反向构建为 fielddata 结构,并全部加载到 JVM 堆内存。这一过程:

  • 内存消耗巨大fielddata 在内存中常驻,直到段被合并或手动清理。
  • 性能差fielddata 的构建和读取都比基于磁盘的 doc_values 慢得多。
  • 结果错误:聚合使用分词后的 token,导致用户期望的原始值被拆散,产生大量无效桶。

根因详见第 2 篇倒排索引结构与 text/keyword 存储差异,以及第 5 篇 doc_valuesfielddata 的内存模型keyword 类型直接使用 doc_values(列式存储),聚合时顺序扫描磁盘,不占用堆内存。

1.2.5 修正方案

  • 重建索引,将 category 改为 keyword 类型:
PUT /products_v2
{
  "mappings": {
    "properties": {
      "category": { "type": "keyword" },
      "price": { "type": "float" }
    }
  }
}
  • 使用 _reindex 迁移数据:
POST _reindex
{
  "source": {"index": "products"},
  "dest": {"index": "products_v2"}
}
  • 若同一字段需要分词搜索和精确聚合,可使用 fields 子字段:
"category": {
  "type": "text",
  "fields": {
    "keyword": {"type": "keyword"}
  }
}

查询时聚合使用 category.keyword

1.2.6 最佳实践

  • 所有用于排序、聚合、精确匹配的字符串字段,一律使用 keywordtextkeyword 子字段。
  • 定期检查 fielddata 使用情况,设置 indices.fielddata.cache.size 限制。
  • 使用 Kibana 监控 fielddata 内存占用并设置告警。

1.3 设计反模式:nested 过度使用导致写入性能下降

1.3.1 错误示例 电商产品索引,每个文档包含数百个变体(SKU),全部定义为 nested

PUT /products
{
  "mappings": {
    "properties": {
      "name": { "type": "text" },
      "variants": {
        "type": "nested",
        "properties": {
          "sku_id": { "type": "keyword" },
          "price": { "type": "float" },
          "stock": { "type": "integer" },
          "attrs": {
            "type": "nested",
            "properties": {
              "key": {"type": "keyword"},
              "value": {"type": "keyword"}
            }
          }
        }
      }
    }
  }
}

每个产品文档的 variants 数组包含 200 个元素,每个变体又包含 10 个嵌套属性对象。

1.3.2 现象描述

  • 写入吞吐量远低于理论值,Bulk 写入延迟高,CPU 使用率居高不下。
  • 索引速率(Indexing Rate)遇到瓶颈,_cat/indicesdocs.count 增长速度与预期不符。
  • 磁盘空间消耗远大于同等数据量的普通对象结构索引。
  • 使用 _nodes/hot_threads 观察,看到大量线程执行 NestedDocumentParser 相关操作。

1.3.3 排查思路

  1. GET _cat/indices/products?v&h=index,pri,rep,docs.count,store.size 对比文档数和存储量。
  2. POST _reindex 预演:创建一个测试索引将部分文档索引为非 nested 类型,比较写入速度和存储大小。
  3. _nodes/hot_threads 检查热点线程,确认 NestedDocumentWriterLucene80DocValuesProducer 等耗时。
  4. 查看段合并线程状态,_cat/thread_pool/force_mergeGET _nodes/stats/indices/merge 观察合并压力。

1.3.4 根因分析 nested 类型在 Lucene 层将数组中的每个对象索引为独立的子文档,并在父文档和子文档之间通过内部 ID 进行关联。当写入一个包含 200 个变体的文档时:

  • 实际产生的 Lucene 文档数为 1(父)+ 200(一级嵌套)+ 200*10(二级嵌套)= 2201 个文档。
  • 每个子文档都需要建立单独的倒排索引和 doc_values,写入放大极其严重。
  • 查询时,即使是简单的嵌套条件也需要执行父-子文档关联操作,性能开销大。

根因详见第 4 篇映射建模中 nested 内部实现,以及第 3 篇写入流程中多文档的处理。

1.3.5 修正方案

  • 反规范化:将变体中的关键属性提升为产品文档的顶层数组,并使用扁平化对象存储。例如,将价格、库存扁平化为 variant_prices: [19.99, 29.99],配合 term 查询过滤。
  • 应用层拼接:将变体数据拆分为独立索引 variants,通过父 ID 关联,在应用层进行两次查询。
  • 若必须保留嵌套,限制嵌套层数,并大幅减少嵌套内的字段数量。

1.3.6 最佳实践

  • 评估数据关系,优先使用反规范化或父子文档(join),但注意 join 的性能代价。
  • 严格控制嵌套深度和数组大小,单个嵌套数组元素建议不超过 100。
  • 对于写入密集型应用,慎用 nested

1.4 设计反模式:分片数过大导致 segment 数膨胀

1.4.1 错误示例 一个每日 3 GB 的日志索引,分配了 10 个主分片,每分片 1 副本:

PUT /logs-2024.01.01
{
  "settings": {
    "number_of_shards": 10,
    "number_of_replicas": 1
  }
}

1.4.2 现象描述

  • 集群内存持续增长,_nodes/stats/jvm 显示 heap used 接近上限,频繁触发 minor GC,偶尔 Full GC。
  • 文件描述符数量持续升高,接近 file descriptors 上限,_cat/nodesfd.used 很高。
  • 查询延迟 P50 和 P99 均升高,尤其对 match_all 等需要扫描多个分片的查询。
  • _cat/segments 统计显示整个索引的 segment 总数超过 5000,而平均每个分片 segment 数约 500+。

1.4.3 排查思路

  1. GET _cat/shards/logs-2024.01.01?v 查看分片分布和大小。
  2. GET _cat/segments/logs-2024.01.01?v&h=index,shard,segment,docs.count,size 统计 segment 数量。
  3. GET _nodes/stats/indices/segments 获取 segment memory 占用。
  4. 计算每个分片平均 segment 数,远超过 50~100 的健康范围。
  5. 检查索引设置,确认分片数是否过高。

1.4.4 根因分析 每个分片是一个独立的 Lucene 索引实例,内部包含多个 segment。ES 的段合并(Merge)策略默认会在各分片内独立运行。当分片数过多时:

  • 所有分片的 segment 元数据都需要在节点堆内存中维护,导致 heap 压力大。
  • 每个 segment 会打开文件句柄,分片数 * segment 数决定了文件句柄数量,可能耗尽 OS 限制。
  • 查询需要访问所有相关分片,分片过多增加了协调节点的扇出开销,也使得缓存效率降低。

根因详见第 7 篇集群分布式原理与分片划分,以及第 3 篇段合并机制

1.4.5 修正方案

  • 对历史数据,通过 _reindex 到分片数更合理的新索引,如 2 个主分片。
  • 新索引结合 ILM 和 Rollover,每个索引大小控制在 2050 GB,分配 13 个主分片。
  • 对现有大量段的情况,可在只读状态下执行 _forcemerge?max_num_segments=5 来减少段数,但必须在非高峰期执行。

1.4.6 最佳实践

  • 单个分片大小控制在 1050 GB,分片总数不宜超过节点数的 2030 倍。
  • 使用 _cat/segments 监控段数,设置告警阈值(如每分片 >100 segments)。

1.5 设计反模式:分片数过小导致单分片数据量超大

1.5.1 错误示例 一个预计存储一年订单数据的索引,仅分配 1 个主分片和 1 个副本:

PUT /orders
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  }
}

一年后数据量达到 600 GB。

1.5.2 现象描述

  • 写入出现热点:所有写入压力集中在单分片所在节点,_cat/nodes 显示该节点 CPU 和磁盘 I/O 远高于其他节点。
  • 搜索和聚合耗时极长,单个分片无法利用多节点并行计算。
  • 分片迁移(如节点失联后分片重分配)耗时数小时,期间主分片不可用则为 RED。
  • _cat/shards 显示 store.size 达到 600 GB,远超推荐值。

1.5.3 排查思路

  1. GET _cat/shards/orders?v 查看分片大小。
  2. _cat/nodes 检查各节点负载是否均衡。
  3. _cluster/health 确认无未分配分片,但查询延迟高。
  4. 使用 _nodes/stats 查看该节点索引线程和 IO 统计,确认单点瓶颈。

1.5.4 根因分析 ES 的分布式写入是基于分片路由的,每个文档根据 _id 路由到一个主分片。单个分片无法水平扩展写入能力,所有写入请求都汇聚到一个节点的一个线程池队列中。同样,查询和聚合也只能在这个单分片上执行,无法利用集群的并行计算能力。超大分片还会导致段合并时 I/O 巨幅波动,影响在线服务。

根因详见第 7 篇集群分布式协调与分片路由机制

1.5.5 修正方案

  • 重新设计索引,增加分片数,例如 600 GB 可使用 10~15 个主分片。
  • 通过别名滚动索引(Rollover),让新索引使用合理分片数,旧索引保留只读或通过 _reindex 迁移。
POST _reindex
{
  "source": {"index": "orders"},
  "dest": {"index": "orders_v2"}
}

1.5.6 最佳实践

  • 索引设计时预估数据量,按 30~50 GB/分片规划。
  • 结合 ILM 自动切割索引,而不是手动创建大索引。

1.6 运行时反模式:refresh_interval 配置不当导致写入/搜索性能失衡

1.6.1 错误示例 日志索引使用默认 index.refresh_interval=1s,而每日写入速度高达 10 万 docs/sec。

1.6.2 现象描述

  • 写入吞吐量持续波动,_cat/thread_pool/write 队列频繁积压,rejected 计数增长。
  • CPU 使用率中 refresh 线程占比高,_nodes/hot_threads 可见 RefreshTask 相关堆栈。
  • 日志写入延迟增加,但搜索可见性仍然很高(非必要)。

1.6.3 排查思路

  1. GET /logs/_settings 获取 refresh_interval
  2. GET _cat/thread_pool/write?v 检查队列和拒绝数。
  3. GET _nodes/stats/indices/refresh 统计 refresh 总次数和耗时,发现 refresh 频率极高。
  4. 评估业务对日志可见性的要求,确认是否可以容忍 30s~60s 延迟。

1.6.4 根因分析 每次 refresh 都会将内存 buffer 中的数据生成新的 segment 并打开,使新写入的文档可被搜索。这涉及:

  • 创建新的 segment 元数据和文件句柄。
  • 清空内存 buffer,并触发后续可能的段合并。
  • 频繁的 refresh 会导致大量小 segment 产生,增加合并压力和 I/O。

根因详见第 3 篇写入刷新(refresh)机制与性能影响

1.6.5 修正方案

  • 对于日志索引,将 refresh_interval 调整为 30s60s
PUT /logs/_settings
{
  "index": {
    "refresh_interval": "30s"
  }
}
  • 如果是纯粹的离线分析索引,可以设置为 -1,在批量写入完成后手动 POST /logs/_refresh

1.6.6 最佳实践

  • 写入密集型场景(日志、指标)务必调大 refresh_interval
  • 在线搜索场景维持 1s 但需监控 refresh 负载。

1.7 运行时反模式:段合并限速过低导致 segment 数膨胀与查询性能下降

1.7.1 错误示例 集群配置了极低的合并限速以保护磁盘 I/O:

indices.store.throttle.max_bytes_per_sec: 20mb

而写入速率持续很高。

1.7.2 现象描述

  • _cat/segments 显示 segment 总数持续攀升,达到 3000+,每个分片平均 segment 数超过 200。
  • 查询延迟逐渐升高,_profile 显示 next_doc 时间增加。
  • 文件句柄使用量接近 ulimit 上限。

1.7.3 排查思路

  1. GET _cat/segments/index?v&h=index,shard,segment,docs.count 统计段数。
  2. GET _nodes/stats/indices/merge 查看合并速率和节流时间。
  3. 检查集群设置:GET _cluster/settings?include_defaults=true 过滤 throttle

1.7.4 根因分析 TieredMergePolicy 根据段的大小和数量选择合并目标。当限速过低时,合并线程被 IO 限速卡住,无法及时合并小段,导致段数量累积。每个段都会占用内存和文件句柄,搜索时需要遍历所有段,导致性能下降。

根因详见第 3 篇段合并与 I/O 限速原理,以及 TieredMergePolicy 的参数调节

1.7.5 修正方案

  • 适当提高限速至 100mb 或更高(取决于 SSD 能力):
PUT /_cluster/settings
{
  "persistent": {
    "indices.store.throttle.max_bytes_per_sec": "100mb"
  }
}
  • 对不再写入的历史索引,在非高峰期执行 POST /logs-2024.01.01/_forcemerge?max_num_segments=5
  • 调整 index.merge.policy.segments_per_tier 等参数以更积极地合并。

1.7.6 最佳实践

  • 监控段数和合并统计,设置合理的限速,避免“过保护”导致性能衰退。
  • 写入密集时使用 SSD,可支持更高合并限速。

2. 查询与聚合反模式

2.1 设计反模式:深分页 from+size 导致协调节点 OOM

2.1.1 错误示例

POST /orders/_search
{
  "from": 50000,
  "size": 20,
  "query": { "match_all": {} }
}

2.1.2 现象描述

  • 查询超时或抛出 CircuitBreakingException,堆栈显示 parent circuit breakerrequest breaker 限制。
  • 协调节点堆内存 spike,Minor GC 频繁,甚至 Full GC。
  • 慢查询日志记录此查询耗时 >10s。

2.1.3 排查思路

  1. 使用 _profile 查看查询,发现协调节点排序阶段耗时巨大,且内存占用高。
  2. GET _nodes/stats/jvm 查看协调节点的 heap 使用,old_gen 接近满。
  3. 检查查询参数,发现 from 值过大。
  4. 查看 index.max_result_window 设置,可能已被调大,默认为 10000。

2.1.4 根因分析 分布式搜索时,协调节点向每个分片请求 from+size 条文档(此处为 50020 条),然后在内存中进行全局排序并丢弃前 50000 条。分片数越多,拉取到协调节点的文档总数越大,内存消耗越高。Lucene 的 TopDocs 收集器会持有这些文档 ID 和评分,导致堆内存压力。

根因详见第 5 篇高级查询 DSL 中深度分页问题与 search_after 原理,以及分布式搜索两阶段协调过程。

2.1.5 修正方案

  • 使用 search_after,基于排序值翻页,每次只取 size 条:
GET /orders/_search
{
  "size": 20,
  "query": { "match_all": {} },
  "sort": [{"order_id": "asc"}],
  "search_after": [50000]
}
  • 若需导出大量数据,使用 scroll API,注意其维护的搜索上下文会占用资源,用后需清理。
  • 限制 from 参数,在前端使用“无限滚动”替代跳页。

2.1.6 最佳实践

  • 业务分页必须使用 search_afterscroll(非实时)。
  • 严格限制 index.max_result_window,防止深度分页。

2.2 设计反模式:前缀通配 *keyword 导致全量扫描

2.2.1 错误示例

GET /articles/_search
{
  "query": {
    "wildcard": {
      "title": { "value": "*elasticsearch" }
    }
  }
}

2.2.2 现象描述

  • 查询延迟极高,CPU 飙升至近 100%。
  • 慢查询日志记录查询耗时 30 秒,即使索引数据量仅 100 万。
  • _profile 输出显示 WildcardQuerynext_doc 耗时占比极大,且扫描文档数等于总文档数。

2.2.3 排查思路

  1. _profile 详细输出显示 matched_terms 为大量甚至全部 term。
  2. 检查通配符位置,发现以 * 开头。
  3. 通过 _explain 查看任意文档为何匹配,理解其需遍历 term 字典。

2.2.4 根因分析 Lucene 的倒排索引 term 字典使用 FST 结构,能够高效支持前缀搜索(如 prefix 查询)。但前导通配符 *keyword 相当于“任意前缀”,FST 无法提供加速,必须遍历整个 term 字典,对每个 term 获取倒排列表进行匹配。这等效于全索引扫描,性能极差。

根因详见第 2 篇倒排索引与 FST 数据结构,以及 WildcardQuery 在 Lucene 中的执行流程。

2.2.5 修正方案

  • 禁止使用前导通配符。如果业务需要后缀搜索,可在索引时对字段做 reverse 分词,然后使用 prefix 查询。
  • 使用 edge_ngram token filter 生成后缀 token,代价是索引体积膨胀。
  • 改用全文搜索、模糊匹配或正则(注意正则也可能性能差)。

2.2.6 最佳实践

  • 搜索交互设计避免后缀模糊匹配,引导用户使用前缀或全文搜索。
  • 若必须支持后缀搜索,必须配合专用索引结构和硬件评估。

2.3 运行时反模式:terms 聚合 size 过大导致内存溢出

2.3.1 错误示例

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "top_users": {
      "terms": {
        "field": "user_id",
        "size": 5000000
      }
    }
  }
}

2.3.2 现象描述

  • 查询被 CircuitBreakingException 中断,提示 [fielddata] Data too large[aggregation] ...
  • 节点内存 spike,可能引发 Full GC 或 OOM。
  • 日志中错误为 ElasticsearchException[failed to execute query] ... caused by CircuitBreakingException

2.3.3 排查思路

  1. GET _nodes/stats/indices/fielddata?fields=user_id 查看 fielddata 占用,发现异常大。
  2. GET _nodes/stats/jvm 查看堆使用,heap_used_percent 接近 100%。
  3. _profile 分析聚合查询,collector 阶段内存分配巨大。
  4. 定位到具体的 terms 聚合 size 参数超大。

2.3.4 根因分析 terms 聚合需要构建全局序数(Global Ordinals)并在内存中维护每个桶的计数。size 决定了需要维护的最大桶数,设置过大(百万级)会导致内存占用急剧膨胀。即使使用 doc_values,构建全局序数和收集桶的过程依然需要分配大量内存,很容易超出 Circuit Breaker 的限制。

根因详见第 5 篇聚合分析引擎中 terms 聚合与全局序数机制,以及 fielddata 的内存模型

2.3.5 修正方案

  • 限制 size 为合理的 Top N,如 1000。
  • 若需要全量聚合,使用 composite 聚合进行分页:
{
  "aggs": {
    "users": {
      "composite": {
        "size": 1000,
        "sources": [ { "uid": { "terms": { "field": "user_id" } } } ]
      }
    }
  }
}
  • 使用 cardinality 聚合获取去重近似值。

2.3.6 最佳实践

  • 绝不在无分页的情况下设置超大 size
  • 定期清理 fielddata 缓存,限制 indices.fielddata.cache.size

2.4 运行时反模式:未使用 filter 上下文导致重复评分计算

2.4.1 错误示例

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "term": { "status": "active" } },
        { "match": { "description": "wireless headphones" } }
      ]
    }
  }
}

2.4.2 现象描述

  • 相同查询重复执行,_profile 显示 TermQueryscore 时间占比较多。
  • Query Cache 命中率极低,GET _nodes/stats/indices/query_cache 显示 hit_count 远小于 miss_count
  • CPU 使用率偏高,但查询逻辑简单。

2.4.3 排查思路

  1. _profile 分析查询,TermQuerybreakdownscore 不为零。
  2. 检查 bool 结构,term 放在了 must 而不是 filter
  3. _stats/query_cache 查看缓存利用情况。

2.4.4 根因分析 query 上下文(mustshould)会计算文档的相关度评分(_score),即使对于 term 这种确定性查询,每次也需要进行评分计算,无法利用 Query Cache。filter 上下文不会计算评分,只进行文档匹配,并且匹配结果可被自动缓存,后续相同过滤条件直接复用缓存结果,跳过磁盘读取和评分。

根因详见第 5 篇高级查询 DSL 中 filterquery 上下文的区别,以及 Query Cache 机制

2.4.5 修正方案

  • 将确定性过滤移入 filter 子句:
{
  "query": {
    "bool": {
      "must": [ { "match": { "description": "wireless headphones" } } ],
      "filter": [ { "term": { "status": "active" } } ]
    }
  }
}

2.4.6 最佳实践

  • 编写查询时,明确区分:需要影响评分的条件放 must/should,纯过滤条件放 filter
  • 监控 query cache 效率,及时优化。

2.5 运行时反模式:script_score 脚本性能开销过大

2.5.1 错误示例

GET /articles/_search
{
  "query": {
    "script_score": {
      "query": { "match": { "title": "elasticsearch" } },
      "script": {
        "source": "double clicks = doc['clicks'].value; double weight = doc['weight'].value; return _score * Math.log(2 + clicks * weight);"
      }
    }
  }
}

2.5.2 现象描述

  • 查询延迟非常高,P99 达到秒级。
  • _nodes/hot_threads 显示大量 CPU 时间消耗在 ScriptScoreQueryscore 方法上。
  • GET _nodes/stats/indices/script_cache 显示编译缓存正常,但执行开销巨大。

2.5.3 排查思路

  1. _profile 定位到 script_score 阶段耗时占比超过 80%。
  2. 查看脚本逻辑,涉及 Math.log 等计算。
  3. 测试移除 script_score 后的基准查询时间,发现差异巨大。

2.5.4 根因分析 脚本在 Lucene 层对每个匹配的文档逐个执行,无法利用索引加速。即使脚本编译后缓存在 ScriptCache 中,执行阶段仍需为每个文档调用 Painless 引擎进行计算,CPU 开销随文档数量线性增长。复杂的数学运算和多字段访问会显著放大延迟。

根因详见第 5 篇脚本查询与性能考量,以及 Painless 脚本引擎的执行模型

2.5.5 修正方案

  • 写入时预计算:增加一个 score_factor 字段,在写入时计算好 Math.log(2 + clicks * weight),查询时使用 function_scorefield_value_factor 直接引用:
{
  "query": {
    "function_score": {
      "query": { "match": { "title": "elasticsearch" } },
      "field_value_factor": {
        "field": "score_factor"
      }
    }
  }
}
  • 若必须使用脚本,尽量简化逻辑,避免 Math 函数和字符串操作。

2.5.6 最佳实践

  • 避免在线查询中进行复杂计算,优先写入时完成。
  • 使用内置函数替代自定义脚本。

查询慢的排查流程图

flowchart TD
    A[用户/监控发现查询P99突增] --> B[查看慢查询日志]
    B --> C{是否有明显慢查询?}
    C -- 否 --> D[检查集群负载, _cat/nodes, _cat/thread_pool]
    D --> E[可能写入压力或资源不足]
    C -- 是 --> F[复制慢查询语句, 使用_profile API分析]
    F --> G[分析 breakdown 各阶段耗时]
    G --> H[查询阶段: term lookup, score耗时异常?]
    H -- 是 --> I[检查是否未使用filter, 是否前缀通配, 是否脚本开销大]
    I --> J[优化查询结构或索引映射]
    H -- 否 --> K[聚合阶段: global_ordinals构建慢?]
    K -- 是 --> L[检查terms size, 聚合字段类型, fielddata使用]
    L --> M[调整聚合参数或映射]
    G --> N[检查query cache命中率: _nodes/stats/indices/query_cache]
    N --> O[命中率低则增加filter缓存使用]
    J --> P[修复后压测验证]
    M --> P
    O --> P

图下四层说明

  1. 图例说明:该流程图展示了从发现查询变慢到定位根因的标准排查路径,覆盖 _profile 分析、缓存诊断和查询结构调整。
  2. 诊断路径解读:首选慢查询日志收集具体语句;再利用 _profilebreakdown 拆解各阶段耗时,快速区分是查询阶段还是聚合阶段的问题;最后根据问题类型采取优化。
  3. 关键命令解析GET _nodes/stats/indices/query_cache 获取缓存统计;POST /_profile 注入查询进行性能剖析;GET _cat/thread_pool 判断是否因排队导致延迟。
  4. 常见误区纠正:不要一遇到慢查询就加硬件;避免忽略 filter 缓存的巨大收益;_profile 结果应关注 next_docadvance 时间而不是总时间。

3. 写入与性能反模式

3.1 设计反模式:refresh_interval=1s 在日志场景下导致写入吞吐下降

本案例与 1.6 类似,但更强调设计层面:一开始设计索引模板时采用默认 1s。修正方案同样为增大 refresh_interval,最佳实践为日志场景设置 30s~60s。此处不再重复六步,而是统一引用 1.6 的详细分析并强调设计时的考虑。

3.2 运行时反模式:force_merge 误用于在线索引导致 I/O 尖刺

3.2.1 错误示例 定时任务在高流量时段对正在写入的索引执行:

POST /logs-2024.01.01/_forcemerge?max_num_segments=1

3.2.2 现象描述

  • 执行期间磁盘 I/O 利用率达到 100%,iostat 显示 await 飙升。
  • 搜索和写入延迟瞬间升高,_cat/thread_pool/searchwrite 队列积压。
  • 集群负载急速攀升,甚至导致节点假死。

3.2.3 排查思路

  1. _cat/tasks?detailed&actions=indices:admin/forcemerge* 查看当前正在执行的强制合并任务。
  2. iostat -x 1 或通过监控查看磁盘读写 IOPS 和吞吐。
  3. _nodes/hot_threads 显示大量线程阻塞于 write_bytesfsync
  4. 确认 force_merge 是否在业务高峰期执行。

3.2.4 根因分析 force_merge 会强制合并所有段到指定数量(甚至 1 个),需要读取多个段并写入一个新的段,涉及大量磁盘 I/O 和 CPU 计算。对于在线索引,合并过程与正常的写入、搜索争抢资源,导致 I/O 尖刺。此外,强制合并会将已删除文档物理清除,写入新的段时会产生大量临时文件,容易填满磁盘。

根因详见第 3 篇段合并(Merge)机制与 force_merge API 的实现细节

3.2.5 修正方案

  • 仅对只读索引执行,并在业务低峰期进行。
  • 添加 only_expunge_deletes=true 参数,仅合并已删除文档的段,减少 I/O。
  • 限制 max_num_segments 为 5~10,而不是 1。
  • 利用 ILM 的 force merge action 在索引进入 cold 阶段自动执行,此时索引已只读。

3.2.6 最佳实践

  • force_merge 永远是重型操作,务必在只读索引和非高峰期执行。
  • 监控合并任务,设置 indices.store.throttle.max_bytes_per_sec 合理限速。

3.3 运行时反模式:Bulk 批大小过大或过小

3.3.1 错误示例

  • 客户端每个 Bulk 仅包含 10 个文档(每个约 1KB)。
  • 或客户端一次 Bulk 塞入 100 MB 数据。

3.3.2 现象描述

  • 批次过小:吞吐量极低(<1000 docs/s),_cat/thread_pool/writeactive 线程数少,但延迟尚可。
  • 批次过大:写入延迟抖动大,节点 GC 频繁,_cat/thread_pool/write 出现 rejected_nodes/stats/jvm 显示 heap 突然飙升。

3.3.3 排查思路

  1. GET _cat/thread_pool/write?v 查看活跃、队列、拒绝。
  2. 调整客户端批量大小进行压测,观察吞吐量和延迟变化。
  3. 监控节点 GC 日志和堆使用情况。

3.3.4 根因分析 Bulk API 将多个索引操作合并为一个请求,减少网络往返和内部事务开销。但批量过小无法摊薄开销;批量过大则单个请求占用过多内存和 CPU,并可能导致 JVM 内存压力,甚至触发 Circuit Breaker。此外,一个 Bulk 请求中的某个文档写入慢会拖慢整个批次。

根因详见第 3 篇 Bulk 写入与资源消耗

3.3.5 修正方案

  • 通过压测找到最佳点,一般建议 5–15 MB 请求体大小。
  • 客户端使用异步批量处理器,设置 bulkSize 为 10 MB,bulkActions 为 1000,flushInterval 为 5s。
  • 动态调整,监控 rejected 和延迟。

3.3.6 最佳实践

  • 根据硬件和索引模型压测确定最佳 Bulk 大小。
  • 启用 _bulkpipeline 来处理批次失败重试。

3.4 运行时反模式:Translog 异步刷盘导致宕机数据丢失

3.4.1 错误示例 索引设置:

PUT /logs
{
  "settings": {
    "index": {
      "translog": {
        "durability": "async",
        "sync_interval": "120s"
      }
    }
  }
}

3.4.2 现象描述

  • 节点意外重启后,最近 120 秒已确认写入的文档丢失。
  • 应用日志显示写入返回 200 OK,但搜索不到。

3.4.3 排查思路

  • 对比写入确认数量与重启后可查文档数。
  • 检查 _settings 中 translog 配置。

3.4.4 根因分析 translog.durability: async 模式下,写入请求在 translog 写入后即返回成功,但 translog 的 fsync 是由后台线程周期性调用的。若节点在两次同步之间宕机,尚未 fsync 的 translog 数据会丢失,导致已确认的文档无法恢复。

根因详见第 3 篇写入持久化与 Translog 机制

3.4.5 修正方案

  • 对于关键业务数据,设置 index.translog.durability: request(默认),确保每次写入请求后 translog 同步到磁盘。
  • 若需兼顾性能且可接受少量丢失,可保留 async 但缩短 sync_interval 至 5s。

3.4.6 最佳实践

  • 根据数据重要性选择策略,默认推荐同步模式。
  • 异步模式仅用于大批量日志且可容忍少量丢失场景。

4. 集群与架构反模式

4.1 设计反模式:Master 节点与 Data 节点混合部署

4.1.1 错误示例 所有节点配置:

node.master: true
node.data: true

4.1.2 现象描述

  • 集群状态更新延迟高,_cat/pending_tasks 中出现 put-mappingcreate-index 任务长时间未完成。
  • 节点 Full GC 时,可能触发主节点选举风暴,日志中出现 master leftelected as master 频繁交替。
  • 查询和写入响应时断时续。

4.1.3 排查思路

  • GET _cat/nodes?v&h=name,node.role,master,heap.percent 查看角色和堆使用。
  • 主节点 GC 日志分析,观察到频繁的长时间 GC 停顿。
  • GET _cluster/health 频繁变化。

4.1.4 根因分析 主节点负责维护集群状态、分片分配、索引创建等元数据管理任务,需要稳定和快速的响应。数据节点上密集的读写、段合并、搜索等操作会消耗大量 CPU 和堆内存,若与主节点混部,当数据节点负载高时,主节点的响应变慢,导致心跳超时、集群不稳定。

根因详见第 7 篇集群分布式原理与角色分离建议

4.1.5 修正方案

  • 分离角色,配置至少 3 个专用 master 节点:
node.master: true
node.data: false

其他节点:

node.master: false
node.data: true

4.1.6 最佳实践

  • 专用主节点内存一般 4-8 GB,CPU 无需过高。
  • 确保 discovery.seed_hosts 正确配置所有 master 候选节点。

4.2 设计反模式:分片分配感知未配置导致机架级故障时数据不可用

4.2.1 错误示例 所有节点位于同一机架或数据中心,未设置 rack awareness。

4.2.2 现象描述

  • 一个机架断电,所有副本集中在该机架,主分片虽有分配但部分副本丢失,集群变为 YELLOW;若主分片也在失电机架,则 RED。
  • 数据恢复需从单点拉取,耗时极长。

4.2.3 排查思路

  • GET _cluster/settingscluster.routing.allocation.awareness.attributes
  • GET _cat/shards 结合 _cat/nodeattrs 查看分片分布,发现主副分片聚集。

4.2.4 根因分析 ES 通过分片分配感知(Shard allocation awareness)可以根据节点属性(如 rack_id)将主副本分片分配到不同机架,避免单点故障。未配置时,ES 只根据磁盘和负载分布,可能将所有副本放在同一机架。

根因详见第 7 篇分片分配感知与 rack awareness 机制

4.2.5 修正方案

  • 为节点添加属性 node.attr.rack_id: rack1rack2
  • 开启强制感知:
PUT /_cluster/settings
{
  "persistent": {
    "cluster.routing.allocation.awareness.attributes": "rack_id",
    "cluster.routing.allocation.awareness.force.rack_id.values": "rack1,rack2"
  }
}

4.2.6 最佳实践

  • 物理部署时必须配置 rack awareness,确保主副分片不在相同故障域。

4.3 设计反模式:磁盘水位线未合理设置

4.3.1 错误示例 采用默认水位线,磁盘总容量 2TB,当使用率达到 95%(约 1.9TB)触发 flood_stage,索引被锁定只读。

4.3.2 现象描述

  • 索引突然只读,写入报错:cluster_block_exception [FORBIDDEN/12/index read-only / allow delete (api)]
  • GET _cat/allocation 显示 disk.avail 为 100GB,但 disk.percent 为 95%。

4.3.3 排查思路

  • GET _cat/allocation?v 查看各节点磁盘使用。
  • GET _cluster/settings?include_defaults=true 查看 disk.watermark 设置。
  • 日志中可能出现 high disk watermark exceeded 等警告。

4.3.4 根因分析 默认水位线 low=85%, high=90%, flood_stage=95%,达到 95% 时 ES 强制设置该节点上所有索引为 read_only_allow_delete,阻止写入以避免磁盘写满导致节点崩溃。对于大容量磁盘,5% 剩余空间可能有上百 GB,但仍被锁定。

根因详见第 7 篇磁盘水位线与分片分配控制

4.3.5 修正方案

  • 根据磁盘实际容量调整水位线百分比:
PUT /_cluster/settings
{
  "transient": {
    "cluster.routing.allocation.disk.watermark.low": "90%",
    "cluster.routing.allocation.disk.watermark.high": "95%",
    "cluster.routing.allocation.disk.watermark.flood_stage": "98%"
  }
}
  • 当触发只读锁定时,手动解除:PUT /logs/_settings {"index.blocks.read_only_allow_delete": null},并快速清理或扩容。

4.3.6 最佳实践

  • 结合磁盘监控,提前规划扩容或清理。
  • 对于 SSDs,可适当提高水位线,但必须预留合并所需临时空间。

4.4 运行时反模式:集群 RED 未及时处理导致数据丢失风险

4.4.1 错误示例 集群出现 RED,运维未及时处理,延迟数小时。期间持有唯一主分片的节点硬盘损坏,数据永久丢失。

4.4.2 现象描述

  • 部分索引读写失败,_cluster/health status = red。
  • _cat/shards 显示主分片 UNASSIGNED,原因 NODE_LEFTALLOCATION_FAILED
  • 错误日志 unassigned shard, reason=NODE_LEFT, node_left=[node1]

4.4.3 排查思路

  1. GET _cluster/health?level=shards,找出未分配分片。
  2. GET _cat/shards?h=index,shard,prirep,state,unassigned.reason 查看具体原因。
  3. GET _cluster/allocation/explain 请求体指定 indexshard,获取详细决策信息,如 node_allocation_decisions 中每个节点的 deciders 解释。
  4. 若原因 NODE_LEFT 且节点已永久丢失,立即检查是否有可用快照。

4.4.4 根因分析 当主分片由于节点离开或磁盘故障导致无法分配时,集群会变为 RED。若该主分片的唯一副本不可用(或未分配),则数据丢失风险极高。分配解释 API 可展示具体原因:例如 disk_watermark_low_failedshard_state_not_allocated 等。

根因详见第 7 篇集群健康、分片分配过程,以及 AllocationDecider 机制

4.4.5 修正方案

  • 磁盘空间不足:清理或扩容,然后 POST _cluster/reroute?retry_failed
  • 节点临时离线:等待节点重新加入。
  • 节点永久丢失:若副本存在,ES 会提升副本为主分片;若无副本,可从快照恢复。
  • 紧急时可接受数据丢失,手动分配空主分片:POST _cluster/reroute { "commands": [{ "allocate_empty_primary": { "index": "x", "shard": 0, "node": "node2", "accept_data_loss": true } }] }

4.4.6 最佳实践

  • RED 状态立即响应,设置告警。
  • 始终配置至少 1 个副本,并定期快照备份。

集群 RED 排查序列图

sequenceDiagram
    participant U as 运维/监控
    participant C as Cluster API
    participant S as Shards API
    participant A as Allocation Explain API
    U->>C: GET _cluster/health
    C-->>U: status: red, unassigned_shards: 2
    U->>S: GET _cat/shards?h=index,shard,prirep,state,unassigned.reason
    S-->>U: indexA 1 p UNASSIGNED NODE_LEFT
    U->>A: GET _cluster/allocation/explain { "index": "indexA", "shard": 1, "primary": true }
    A-->>U: 详细解释:节点未加入,磁盘空间不足等
    U->>U: 根据原因修复,如释放磁盘,恢复节点
    U->>C: 再次检查 health,变为 yellow/green

图下四层说明

  1. 图例说明:序列图展示了运维人员通过三个核心 API 逐步定位 RED 原因的流程。
  2. 诊断路径解读:第一步用 _cluster/health 确认整体状态和未分配分片数;第二步用 _cat/shards 查看分片状态和未分配原因字段;第三步用 _cluster/allocation/explain 获取详细分配失败解释。
  3. 关键命令解析_cluster/allocation/explain 可指定 indexshard,返回 node_allocation_decisions,包含每个节点的判断结果,是诊断分配问题的利器。
  4. 常见误区纠正:不要盲目重启节点;RED 时优先诊断而不是修改配置;unassigned.reasonNODE_LEFT 不一定是节点故障,可能是临时网络分区。

5. 安全反模式

5.1 设计反模式:生产环境未启用 TLS

5.1.1 错误示例 elasticsearch.yml 中未配置 SSL/TLS,HTTP 和 transport 均明文传输。

5.1.2 现象描述

  • 网络抓包可见索引数据、认证凭证明文。
  • 未授权访问风险高。

5.1.3 排查思路

  • 使用 curl -v http://es:9200 确认未加密。
  • 检查配置文件中 xpack.security.transport.ssl.enabled 等项。

5.1.4 根因分析 无 TLS 时,节点间通信和客户端通信均未加密,违背数据安全传输原则,易被中间人攻击。

根因详见第 8 篇安全体系中的 TLS 配置与证书管理

5.1.5 修正方案

  • 生成证书,配置 elasticsearch.yml:
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.keystore.path: certs/elastic-certificates.p12
xpack.security.transport.ssl.truststore.path: certs/elastic-certificates.p12
xpack.security.http.ssl.enabled: true
xpack.security.http.ssl.keystore.path: certs/elastic-certificates.p12

5.1.6 最佳实践

  • 强制开启 TLS,使用自签名或 CA 证书。

5.2 设计反模式:匿名访问未禁用

5.2.1 错误示例 配置中保留匿名用户并赋予 superuser 角色。

5.2.2 现象描述

  • 任何人无需认证即可执行任意操作。

5.2.3 排查思路

  • 尝试 curl http://es:9200/_cluster/health 无凭据访问。

5.2.4 根因分析 匿名访问绑定角色后,未认证的请求自动获得这些角色权限,成为安全后门。

根因详见第 8 篇安全体系认证与授权

5.2.5 修正方案

  • 禁用匿名访问:xpack.security.authc.anonymous.enabled: false

5.2.6 最佳实践

  • 生产环境必须禁用匿名访问。

5.3 设计反模式:elastic 超级用户未修改默认密码

5.3.1 错误示例 使用密码 changeme 或保留初始化时的密码。

5.3.2 现象描述

  • 攻击者获取管理员权限,随意删除索引。

5.3.3 排查思路

  • 使用默认密码尝试登录。

5.3.4 根因分析 内置 elastic 账户拥有全部权限,密码泄露即完全失控。

根因详见第 8 篇安全体系内置用户管理

5.3.5 修正方案

  • 执行 bin/elasticsearch-reset-password -u elastic 重置强密码。

5.3.6 最佳实践

  • 所有内置用户设置强密码,并启用密码策略。

5.4 运行时反模式:API Key 权限过大且未设置过期时间

5.4.1 错误示例

POST /_security/api_key
{
  "name": "my-app",
  "role_descriptors": {
    "super_role": { "cluster": ["all"], "indices": [{"names": ["*"], "privileges": ["all"]}] }
  }
}

5.4.2 现象描述

  • API Key 泄露后,攻击者可执行任何操作,且 Key 永久有效。

5.4.3 排查思路

  • GET /_security/api_key 查看权限和过期时间,发现无 expiration

5.4.4 根因分析 最小权限原则要求每个 API Key 仅授予必要的权限。权限过大且无过期时间,泄露后危害极大。

根因详见第 8 篇 API Key 管理与最小权限原则

5.4.5 修正方案

  • 细化权限并设置过期:
POST /_security/api_key
{
  "name": "my-app-readonly",
  "expiration": "30d",
  "role_descriptors": {
    "reader": {
      "cluster": ["monitor"],
      "indices": [{"names": ["logs-*"], "privileges": ["read", "view_index_metadata"]}]
    }
  }
}

5.4.6 最佳实践

  • 按应用分配 Key,遵循最小权限,定期轮换。

6. 诊断工具集与工具→现象映射表

6.1 ES 端全工具链速查

  • _cluster/health:集群健康状态,红/黄/绿,分片概况。
  • _cat/nodes:节点 CPU、内存、角色、负载。
  • _cat/shards:分片分布、状态、未分配原因。
  • _cat/thread_pool:线程池活跃、队列、拒绝数。
  • _nodes/hot_threads:节点当前最繁忙的线程堆栈。
  • _nodes/stats/jvm:JVM 堆使用、GC 次数和时间。
  • Kibana Profiler:图形化查询剖析,定位查询耗时阶段。
  • _profile API:类似 Profiler,可编程分析。
  • _explain:单文档匹配评分细节。
  • _cluster/allocation/explain:分片未分配详细原因。
  • _cat/segments:段数量与大小。
  • _cat/pending_tasks:集群挂起任务。
  • _cat/recovery:分片恢复状态。
  • _security/api_key:API Key 管理。

6.2 工具→反模式现象映射表

典型现象推荐工具关键检查命令常见根因
集群状态 RED_cluster/health_cat/shards_cluster/allocation/explainGET _cluster/health?level=shards主分片未分配(磁盘满、节点离线)
查询 P99 突增Kibana Profiler、_profilePOST _profile 分析 breakdown未使用 filter、深分页、脚本开销
节点 OOM_nodes/stats/jvm_cat/nodesGET _nodes/stats/jvm?filter_path=**.heap_used_percentterms 聚合 size 过大、fielddata 膨胀
写入拒绝_cat/thread_pool_cat/healthGET _cat/thread_pool/write?v写入线程池队列满、磁盘水位线
索引存储膨胀_cat/indices_cat/segmentsGET _cat/segments?v&h=index,shard,docs.count,size字段爆炸、segment 数过多
查询缓慢但 CPU 不高_profile_explainGET _explain 查看评分细节磁盘 I/O 瓶颈、segment 数量大
CPU 突然飙升_nodes/hot_threads_nodes/statsGET _nodes/hot_threads复杂脚本、大量通配查询、段合并
集群 YELLOW 持续_cat/shards_cluster/allocation/explainGET _cat/shards?h=index,shard,prirep,state副本未分配,节点数不足或磁盘空间
节点掉线频繁_cat/nodes_cluster/health日志查看主节点选举主数据混合、GC 停顿、网络分区
安全事件_xpack/security/_authenticate_security/api_keyGET _security/api_key默认密码、匿名访问、权限过大
写入延迟高_cat/thread_pool_nodes/stats/indices观察 refresh/merge 统计refresh_interval 过小、Bulk 大小不当
数据丢失_cat/shards_cluster/allocation/explain检查 UNASSIGNED 原因和 translog 设置Translog 异步、副本缺失

OOM 排查序列图

sequenceDiagram
    participant M as 监控
    participant N as _nodes/stats/jvm
    participant F as _nodes/stats/indices/fielddata
    participant A as _profile/聚合分析
    participant S as _cat/segments
    M->>N: GET _nodes/stats/jvm -> heap used 95%
    M->>F: 查看 fielddata 内存使用量
    F-->>M: fielddata 占用 4GB (异常)
    M->>A: 对最近的查询执行 _profile,检查 terms 聚合 size
    A-->>M: size=1000000, global_ordinals 构建消耗大量内存
    M->>S: 查看 segment 数量,segment 多也会增加内存
    S-->>M: segments: 3200
    M->>M: 确定根因:terms size 过大 + segment 膨胀

图下四层说明

  1. 图例说明:序列图演示了从发现节点 OOM 到定位根因的多工具联合排查流程。
  2. 诊断路径解读:首先检查 JVM 堆内存使用,确认 OOM 现象;接着查看 fielddata 内存和 segment 数量等内存占用大户;最后通过 _profile 分析罪魁祸首查询的聚合细节。
  3. 关键命令解析_nodes/stats/jvm 可查看堆内存各代使用;_nodes/stats/indices/fielddata 得到 fielddata 占用;_profile 显示聚合的 build_ordinals 等阶段消耗。
  4. 常见误区纠正:不能仅通过堆总大小判断,需细分 fielddata、segment memory、query cache 等;OOM 不一定由单个大查询造成,可能是 segment 数量和 fielddata 积累。

7. 标准化排查决策树

7.0 四大典型故障决策树总图

flowchart TD
    START[故障告警] --> Q1{故障类型?}
    Q1 -- 查询变慢 --> P1[慢查询日志/_profile]
    P1 --> P1A{breakdown 中哪个阶段慢?}
    P1A -- term/score --> P1A1[检查filter使用, 通配查询, 脚本]
    P1A -- 聚合 --> P1A2[检查terms size, fielddata, global_ordinals]
    P1A1 --> P1SOLVE[调整查询, 加filter, 预计算]
    P1A2 --> P2SOLVE[限制size, 改用composite]
    
    Q1 -- 集群RED --> R1[GET _cluster/health & _cat/shards]
    R1 --> R2[未分配主分片?]
    R2 -- 是 --> R3[_cluster/allocation/explain]
    R3 --> R3A{原因?}
    R3A -- 磁盘满 --> R3A1[释放磁盘或调水位线]
    R3A -- 节点离线 --> R3A2[恢复节点或手动分配]
    R3A -- 分片损坏 --> R3A3[从快照恢复]
    
    Q1 -- 节点OOM --> O1[_nodes/stats/jvm heap 使用率]
    O1 --> O2[检查 fielddata, segment 数, terms 聚合]
    O2 --> O3{主要占用?}
    O3 -- fielddata --> O3A[优化聚合字段映射为keyword, 清除fielddata cache]
    O3 -- segments --> O3B[执行force_merge, 调整限速]
    
    Q1 -- 写入拒绝 --> W1[_cat/thread_pool write rejected]
    W1 --> W2{队列满?}
    W2 -- 是 --> W2A[检查 refresh_interval, 磁盘水位, Bulk 大小]
    W2A --> W2B[增大 refresh_interval, 清理磁盘, 调整 Bulk]
    W2 -- 否 --> W2C[检查网络或单节点压力]

图下四层说明

  1. 图例说明:总图汇总了四种常见故障的决策路径,每个节点包含判断逻辑和下一步操作。
  2. 诊断路径解读:从故障现象出发,进入对应分支。查询变慢侧重剖析执行阶段;RED 侧重分片分配诊断;OOM 侧重内存构成分析;写入拒绝侧重线程池与资源。
  3. 关键命令解析:各分支均明确标注了需使用的 API,如 _cluster/allocation/explain_profile 等,并提供检查方向。
  4. 常见误区纠正:避免跳跃式排查,应严格遵循决策树顺序;RED 时不要立刻重启集群,需先定位未分配分片的原因。 以下是对第 7 部分“标准化排查决策树”和第 8 部分“面试高频故障排查专题”的深度扩充,可直接整合进前文相应位置。扩充内容大幅增加了诊断细节、命令输出示例、原理引用和实战指导,使决策树更具可操作性,面试题覆盖更深层的故障分析。

7.1 故障路径一:查询突然变慢

此路径适用于:监控发现查询 P99 延迟突增,但索引规模和查询语句本身未发生变化。

第 1 层:确认问题范围

  • 执行 GET _cat/nodes?v&h=name,node.role,heap.percent,load_1m,cpu,观察所有节点的负载。如果只有个别节点 load 高,可能是热点分片或单节点问题。
  • 执行 GET _cluster/health?level=indices,确认集群状态为 green,排除分片未分配导致的超时重试。
  • 检查慢查询日志(默认 logs/<cluster>_index_search_slowlog.log)或 Kibana 的“Slow Logs”页面,找到具体的慢查询语句及其耗时。记录查询的 took 值、分片数、索引。

第 2 层:快照资源使用

  • 执行 GET _cat/thread_pool/search?v,查看搜索线程池的 activequeuerejected
    • queue 持续不为零,说明搜索请求出现排队,可能是 CPU 不足或单次查询太慢。
    • rejected 增加,线程池已满,需紧急扩容或限流。
  • 执行 GET _nodes/stats/indices/merge 查看当前段合并状态,如果 current 不为零且 total_time_in_millis 在近期激增,说明合并操作正在争抢 I/O 资源(根因见第 3 篇段合并机制)。
  • 执行 iostat -x 1 或云监控查看磁盘 awaitutil。如果磁盘 I/O 接近饱和,查询延迟必然上升。

第 3 层:定位慢查询的执行阶段

使用 Kibana Profiler 或 _profile API 对典型慢查询进行分析:

POST /products/_profile
{
  "profile": true,
  "query": { ... }  // 粘贴慢查询语句
}

分析返回的 breakdown,重点看每个阶段的耗时占比:

  • next_docadvance 耗时高(> 100ms 且占比 > 30%):说明倒排链扫描本身开销大。可能原因:
    • 查询匹配的文档量过多,或未使用 filter 导致评分计算(见 2.4 案例)。
    • 通配符前缀 * 扫描(见 2.2 案例)。
    • 段数量过多,每个查询需遍历大量小段。可通过 GET _cat/segments/index?v 查看段数,如果每分片段数 > 100,则段膨胀严重(见 1.7 案例)。
  • score 耗时高:说明评分计算占用大量 CPU。检查是否在 must 中使用了 term 等确定性条件,应移到 filter。检查是否有 script_scorefunction_score 的复杂脚本(见 2.5 案例)。
  • build_scorer 耗时高:可能与复杂的布尔查询、嵌套查询有关。
  • 聚合阶段(collectbuild_ordinals)耗时高:说明聚合开销大。检查:
    • terms 聚合的 size 是否过大(见 2.3 案例)。
    • 聚合字段是否为 text 类型,触发了 fielddata 构建(见 1.2 案例)。
    • 是否存在 nested 聚合,导致子文档聚合扫描。

第 4 层:检查缓存命中率

  • 执行 GET _nodes/stats/indices/query_cache,计算 hit_count / (hit_count + miss_count)。若命中率低,说明可缓存的过滤条件未被重用,或者缓存大小不足。
    • 优化:将 termrange 等确定性过滤统一放入 filter 上下文,以利用 query cache。
  • 检查 request_cache 命中率:GET _nodes/stats/indices/request_cache,对分页聚合查询有加速作用。

第 5 层:索引级优化

  • 如果段数量过多,对只读索引在低峰期执行 _forcemerge?max_num_segments=5,并监控 I/O。
  • 检查索引的 refresh_interval 设置,若写入压力大,可适当调大以减少段产生速度(见 1.6 案例)。
  • 考虑为索引添加合适的分析器和映射,例如将字符串精确值设为 keyword

快速排查清单

检查项命令/工具正常指标异常可能原因
集群健康GET _cluster/healthgreenYELLOW/RED 影响查询
节点负载GET _cat/nodes?vCPU < 80%, load 适中热点节点,资源争抢
搜索线程池GET _cat/thread_pool/searchqueue=0, rejected=0查询排队,OOM 风险
慢查询日志文件/Kibana无超阈值语句具体慢查询信息
Profile breakdown_profilenext_doc < 50ms定位阶段瓶颈
段数量_cat/segments每分片 < 50段膨胀,I/O 压力
Query Cache 命中率_nodes/stats> 50%过滤条件未缓存
Fielddata 占用_nodes/stats/indices/fielddata< 1GB 或合理值聚合字段类型错误

7.2 故障路径二:集群 RED

第 1 层:总览与应急

  • 执行 GET _cluster/health?level=shards,关注 statusunassigned_shardsactive_primary_shards
  • 红色必须立即响应。如果有活跃写入受到影响,可临时将索引只读或通知上游降级。

第 2 层:定位未分配分片

GET _cat/shards?v&h=index,shard,prirep,state,unassigned.reason,docs,store,node

过滤出 stateUNASSIGNEDprirepp 的记录。unassigned.reason 提供最直接的线索:

  • NODE_LEFT:持有分片的节点离线(可能是网络分区或进程挂掉)。
  • ALLOCATION_FAILED:分配尝试失败,通常因磁盘空间、分片数超限等。
  • INDEX_CREATED:索引刚创建,副本尚未分配,通常不是严重问题,但主分片未分配则可能是节点不足。

第 3 层:深度分配分析

对每个未分配主分片执行:

POST _cluster/allocation/explain
{
  "index": "my_index",
  "shard": 1,
  "primary": true
}

返回中的关键字段:

  • current_state:应为 unassigned
  • unassigned_info.last_allocation_status:失败原因摘要。
  • can_allocate:yes/no,决定是否可分配。
  • allocate_explanation:解释原因,如 “cannot allocate because allocation is not permitted to any of the nodes”。
  • node_allocation_decisions:逐个节点列出不分配的原因,常见 decider 包括:
    • disk_threshold:磁盘水位线拒绝。
    • node_version:版本不匹配。
    • filter:分片分配过滤规则排除。
    • snapshot:正在从快照恢复中。
    • max_retry:分配重试次数耗尽。

第 4 层:分类处理

  1. 磁盘空间不足 (disk_threshold)
    • 现象:explain 输出 disk_usage_exceeded_cat/allocation 显示磁盘使用超限。
    • 解决:删除旧索引或清理无用数据,或扩大磁盘/调整水位线。紧急情况下先临时调高 disk.watermark.flood_stage 并解除只读锁(PUT /index/_settings {"index.blocks.read_only_allow_delete": null}),再清理空间。
  2. 节点离线 (NODE_LEFT)
    • 检查 _cat/nodes 确认节点是否已恢复。若节点可修复,重启节点即可,ES 会自动重新分配分片。
    • 若节点永久丢失且无副本可用,数据丢失风险极大。此时可选择:
      • 从快照恢复分片(POST /_snapshot/repo/snapshot/_restore)。
      • 接受数据丢失,手动分配空主分片(POST _cluster/reroute { "commands": [{ "allocate_empty_primary": { ... } }] }),务必谨慎
  3. 分片分配规则限制:检查 cluster.routing.allocation.include/exclude 等设置是否误将分片排挤。
  4. 分配重试耗尽:执行 POST _cluster/reroute?retry_failed=true 重试分配。

第 5 层:恢复与验证

修复后,观察 GET _cluster/health 状态逐渐变为 yellow(主分片已分配,副本待分配)或 green。使用 _cat/recovery 查看恢复进度。

7.3 故障路径三:节点 OOM

第 1 层:确认 JVM 内存压力

  • GET _nodes/stats/jvm?filter_path=**.heap_used_percent,**.heap_max 查看各节点堆使用百分比。
  • heap_used_percent 持续 > 85% 且伴随频繁 Full GC(GC 日志或 _nodes/stats/jvmgc.collectors 的 count 增长快),则存在内存问题。

第 2 层:分离堆内存占用大户

通过 GET _nodes/stats/indices 获取以下核心内存数据:

  • fielddata.memory_size_in_bytes:fielddata 占用。若超过 2~3 GB,需要检查哪个字段消耗。
  • segments.memory_in_bytes:段内存。包括 term dictionary、fst、norms 等。段数量越多,此值越高。
  • query_cache.memory_size_in_bytes:查询缓存。
  • request_cache.memory_size_in_bytes:请求缓存。
  • indexing_buffer.memory_size_in_bytes:写入缓冲,通常不大。

第 3 层:追踪 Fielddata 来源

GET _nodes/stats/indices/fielddata?fields=*

返回每个字段的 memory_size。若发现某个字段占用巨大,且该字段类型为 text,这就是根因:聚合或排序触发了 fielddata 加载(见 1.2 案例)。修复:改用 keyword 类型或子字段,并清理 fielddata cache:POST _cache/clear?fielddata=true

第 4 层:检查段数量与聚合查询

  • GET _cat/segments?v&h=index,shard,segment,docs.count,size 统计每分片段数。若平均超过 100,segment memory 将不可忽视。优化手段:对只读索引执行 _forcemerge,并调整合并策略。
  • 使用慢查询日志和 _profile 查找近期大聚合查询。关注 terms 聚合的 size 参数是否设为极大值(如百万级),以及是否使用了 execution_hint: map 等耗费内存的方式。修正:限制 size,改用 composite 聚合。

第 5 层:配置与长期优化

  • 设置 Heap 大小:不超过物理内存的 50%,且不超过 31 GB(以启用压缩普通对象指针)。
  • 配置 Circuit Breaker:检查 indices.breaker.fielddata.limit 等,避免单查询拖垮节点。
  • 启用 fielddata 断路器并设置合理限制,如 indices.breaker.fielddata.limit: 40%

7.4 故障路径四:写入拒绝

第 1 层:确认写入线程池状态

GET _cat/thread_pool/write?v

关注 rejected 计数是否持续增长。若 rejected 出现,说明写入请求被丢弃,客户端会收到 429 错误。

第 2 层:分析队列积压原因

  • 检查 queue 是否常满(queue 列显示当前排队数,q_size 是队列容量)。若队列经常饱和,通常是写入处理跟不上请求速度。
  • 查看节点资源:_cat/nodes 检查 CPU、磁盘 I/O。如果 CPU 或 disk 接近瓶颈,写入能力下降。

第 3 层:写入链条瓶颈排查

  • Refresh 频率GET _nodes/stats/indices/refresh 查看 totaltotal_time_in_millis。若 refresh 频率过高(每秒数万次),说明 refresh_interval 设置过小。结合索引设置:GET /index/_settings 查看 refresh_interval。日志场景通常设为 30s 或 60s。
  • Merge 压力GET _nodes/stats/indices/merge 查看合并速率和节流时间。若 throttle_time_in_millis 很大,说明合并 I/O 受限,可调整 indices.store.throttle.max_bytes_per_sec
  • Bulk 大小与并发:检查应用端 Bulk 批次大小。过小会导致网络开销和索引线程池利用不足;过大则可能拖慢单个批次并引发内存压力。建议通过压测确定 5~15 MB 的最佳批次。
  • Translog 设置:如果使用异步 translog 且 sync_interval 很长,虽然能提高写入,但可能因为刷盘不及时在压力下产生其他问题,一般不是主因。
  • 磁盘水位GET _cat/allocation 确保磁盘未触及 flood_stage。一旦触及,索引会被锁定只读,写入全部拒绝。

第 4 层:应急与长期优化

  • 立即措施:若磁盘满,清理数据;若 refresh 频繁,动态调整 refresh_interval;若队列积压,可临时增加 thread_pool.write.queue_size(但可能延迟恢复)。
  • 长期:分离日志/指标与搜索业务;启用 ILM 管理索引生命周期;根据硬件优化合并限速和 bulk 大小。

8. 面试高频故障排查专题

说明:本题库聚焦故障排查场景,与前 10 篇侧重原理与架构的面试题形成互补。以下每题均深入展开,涵盖模拟真实场景、逐层排查命令、根因的源码级/原理级分析,以及可复用的最佳实践。

8.1 查询 P99 突然飙升至 5 秒,索引和查询未变,如何排查?

场景深潜
某搜索业务集群有 5 个数据节点,索引 products 有 6 个主分片 1 个副本,数据量 200GB,日常 P99 延迟 200ms。凌晨 3 点,监控告警 P99 升至 5s,但 QPS 无显著变化,也未做代码发布。

排查过程详解

  1. 宏观检查
    • GET _cluster/healthstatus: green,排除 RED 影响。
    • GET _cat/nodes?v&h=name,heap.percent,cpu,load_1m 发现节点 2 的 load_1m 很高,cpu 80% 且 heap.percent 65% 正常。
    • GET _cat/thread_pool/search?v 显示 active=2queue=15rejected=0。队列堆积说明处理能力不足。
  2. 定位慢查询:Kibana Slow Logs 中看到多条 terms 查询,took: 4500ms。复制查询语句。
  3. Profiler 分析
    • 执行 POST /products/_profile 并粘贴该查询。Breakdown 显示聚合阶段 collect 耗时占比 80%,进一步查看是 terms 聚合在字段 tags 上。
    • GET /products/_mapping 发现 tagstext 类型,未设置 keyword 子字段。
    • GET _nodes/stats/indices/fielddata?fields=tags 显示 memory_size 达 3GB,且不断增长。
  4. 确认根因tags 字段被用于聚合,但类型为 text,每次聚合都会从倒排索引中动态构建并加载 fielddata 到堆内存。由于近期写入量增加,fielddata 膨胀导致频繁 GC 和 CPU 竞争,引起查询延迟飙升。根因详见第 1.2 案例和 fielddata 内存模型

修复与验证

  • 短期:清理 fielddata cache:POST /_cache/clear?fielddata=true,查询延迟暂时回落。
  • 长期:重建索引,将 tags 映射改为 keyword 类型,聚合使用 tags 字段(或 tags.keyword)。执行 _reindex 后,延迟恢复正常。

最佳实践

  • 聚合字段必须使用 keyword 类型。
  • 设置 fielddata 断路器阈值(如 40%),防止内存耗尽。

8.2 集群 RED,多个主分片未分配,如何紧急处理?

场景深潜
集群有 3 个 master 节点,10 个 data 节点。凌晨 4 点,监控报集群状态 RED,写入全面中断。运维发现 3 个索引的主分片未分配。

排查步骤与命令解读

  1. GET _cluster/health?level=shards
    {
      "status": "red",
      "unassigned_shards": 5,
      "active_primary_shards": 300,
      ...
    }
    
  2. GET _cat/shards?h=index,shard,prirep,state,unassigned.reason
    indexA   2 p UNASSIGNED NODE_LEFT
    indexA   4 p UNASSIGNED NODE_LEFT
    indexB   0 p UNASSIGNED ALLOCATION_FAILED
    
    • 两个分片因节点离开,一个因分配失败。
  3. 深入分配失败分片:POST _cluster/allocation/explain {"index": "indexB", "shard": 0, "primary": true}: 返回显示 can_allocate: noallocate_explanation: "cannot allocate because disk usage exceeded the high watermark on all eligible nodes"。检查 _cat/allocation,发现多数节点磁盘使用 92%,超过默认 high watermark=90%
  4. 根因分析:磁盘空间不足导致部分分片分配失败。同时离线的节点可能因为磁盘满导致进程异常退出,加剧了未分配分片数量。根因详见第 4.3、4.4 案例

应急处理

  • 立即删除旧索引 index-old-* 释放磁盘空间(谨慎评估后可执行)。
  • 临时调高磁盘水位线:PUT _cluster/settings {"transient": {"cluster.routing.allocation.disk.watermark.high": "95%", "cluster.routing.allocation.disk.watermark.flood_stage": "98%"}}
  • 重试分配:POST _cluster/reroute?retry_failed=true
  • 几分钟后,集群变为 YELLOW(副本未分配),然后逐渐 GREEN。之后排查节点离线原因,重启节点并监控磁盘。

最佳实践

  • 磁盘使用监控告警设置在 80%,预留充足缓冲。
  • 使用 ILM 定期删除或迁移旧索引。
  • 副本数至少为 1,并定期快照。

8.3 节点频繁 Full GC,写入和搜索间歇超时

场景深潜
节点 3(32G 堆,data 节点)出现周期性停顿,监控显示 GC 频率从每小时一次变为每 5 分钟一次,停顿 2~3 秒。写入 TPS 掉底,查询超时率上升。

排查路径

  1. JVM 统计:GET _nodes/node3/stats/jvm
    "heap_used_percent": 88,
    "old_gen": { "used_in_bytes": 26GB, "max_in_bytes": 28GB },
    "gc": { "collectors": { "ConcurrentMarkSweep": { "collection_count": 50, "collection_time_in_millis": 120000 } } }
    
    确认老年代压力大,Full GC 频繁。
  2. 内存分解:GET _nodes/node3/stats/indices
    • fielddata.memory_size_in_bytes: 6,442,450,944 (约 6GB)
    • segments.memory_in_bytes: 2,147,483,648 (2GB)
    • query_cache.memory_size: 536,870,912 (512MB) 字段数据异常高。
  3. 字段分析:GET _nodes/stats/indices/fielddata?fields=* 发现字段 user_comment 占用 5.8GB,该字段为 text 类型,且有一个业务监控面板频繁对其执行 terms 聚合,size 参数设为 100000。
  4. 根因text 字段的 fielddata 加载了大量不分词 token,聚合 size 过大进一步加剧内存消耗,触发频繁 GC。详见 1.2 和 2.3 案例

修复方案

  • 立即停止面板查询,清除 fielddata:POST _cache/clear?fielddata=true
  • 重建索引,将 user_comment 映射改为 keyword(若只需精确聚合),或使用 fields.keyword 子字段。聚合面板改用 user_comment.keyword 并限制 size=1000。
  • 增加 fielddata 断路器限制:indices.breaker.fielddata.limit: 40%

最佳实践

  • 严格审计生产中的聚合查询,禁止对 text 字段做大聚合。
  • 监控 fielddata 大小并告警。

8.4 写入请求被拒绝,线程池 rejected 持续增长

场景深潜
日志集群写入压力大,每秒 5 万 docs。突然客户端收到大量 429 Too Many Requests_cat/thread_pool/write 显示 rejected 不断跳动。

排查过程

  1. 线程池详情:GET _cat/thread_pool/write?v
    node_name   active queue rejected queue_size
    node1       8      200   150      200
    
    queue 打满,active 线程数达到核数,写入任务处理不完。
  2. 检查节点资源:_cat/nodes 显示 CPU 80%,磁盘 util 90%。
  3. 查看 refresh 统计:GET _nodes/stats/indices/refresh 显示每秒 refresh 操作 500 次,total_time 高。索引设置 refresh_interval 仍为默认 1s,但日志索引不需要如此高的实时性。
  4. 检查 Bulk 大小:客户端每个 bulk 只有 100 条文档,约 5KB,导致大量小请求,线程池频繁切换。
  5. 根因:refresh 间隔过小,生成过多小段,I/O 压力大;Bulk 太小,网络和处理开销占高。两者结合导致写入线程池饱和并拒绝。详见 3.1 和 3.3 案例

修复

  • 动态调整日志索引 refresh_interval30sPUT /logs-*/_settings {"index": {"refresh_interval": "30s"}}
  • 调整 Bulk 批次大小为 10MB,约 2000 条文档。
  • 观察 rejected 逐渐归零,写入恢复。

最佳实践

  • 写多读少的日志/指标场景,refresh_interval 设置 30s~60s。
  • 通过压测确定最优 Bulk 大小,并使用异步 bulk 处理器。

8.5 搜索“苹果”匹配到大量无关文档

场景深潜
用户搜索“苹果”,期望找到包含“苹果手机”或“苹果”的文档,结果却返回了大量包含“水果苹果”、“苹果公司”甚至“苹”、“果”拆开匹配的文档。

排查

  1. GET /articles/_mapping 查看 title 字段使用 standard 分析器。
  2. GET /articles/_analyze {"field": "title", "text": "苹果"} 得到 ["苹", "果"],说明中文被逐字分词。
  3. 查询使用 match 匹配,每个单字都会被查找,导致高召回低精度。
  4. 根因standard 分析器按 Unicode 标准切分中文为单字,没有中文词汇识别能力。详见第 6 篇中文分词实战

修复

  • 安装 elasticsearch-analysis-ik 插件,将索引映射改为 ik_max_wordik_smart 分析器。
  • 重建索引后,搜索“苹果”只匹配包含“苹果”词条的文档。

最佳实践

  • 中文业务必须使用专业分词器,并在索引前通过 _analyze 测试分词效果。

8.6 聚合结果桶数量不符预期

场景深潜
一个 terms 聚合查询按 user_id 统计,预期 Top 1000 活跃用户,却只返回了 500 个桶,有些高频用户缺失。

排查

  1. 检查聚合 DSL:"terms": { "field": "user_id", "size": 1000 }
  2. 查看 shard_size 默认值:即使 size 设置为 1000,每个分片默认返回的桶数等于 size * 1.5 + 10,但协调节点最终只取前 1000。如果某个用户在所有分片上都不是前 1000,就会被遗漏。
  3. 使用 GET _cluster/settings?include_defaults=true 查看 search.default_allow_partial_results 等。
  4. 根因terms 聚合是近似精确的,当文档分布不均时,全局高频词可能在部分分片排名较低而丢失。详见第 5 篇聚合引擎中的 terms 聚合精度

修复

  • 增加 shard_size 到 5000 或更大(但需评估内存)。
  • 或改用 composite 聚合分页获取全量结果。
  • 或使用 cardinality 获取去重数,但无法得到桶。

最佳实践

  • 需要精确 Top N 时,shard_size 应远大于 size,建议 2~3 倍,内存允许下可更大。
  • 业务允许近似值时使用 cardinality

8.7 索引存储空间远超原始数据

场景深潜
50GB 原始 JSON 数据,索引后占用 300GB 磁盘(含 1 副本),怀疑空间膨胀异常。

排查

  1. GET _cat/indices?v&h=index,pri,rep,docs.count,pri.store.sizepri.store.size 210GB。
  2. GET _cat/segments/index?v 显示段数 4000,每段平均 50MB。
  3. GET /index/_mapping 统计字段数,发现 3500 个字段,大量由动态映射生成。
  4. 分析存储组成:_source 压缩、doc_values、倒排索引、norms 等。字段爆炸导致每个字段都要建立倒排和列存,存储成倍放大。
  5. 根因:动态映射失控,字段过多,加上段未及时合并(4000 段),产生大量冗余。详见 1.1 和 1.7 案例

修复

  • 重建索引,映射中 dynamic: strict,只保留核心字段,其余 JSON 存入单个 text 字段或忽略。
  • 对旧索引执行 _forcemerge?max_num_segments=10 后存储下降明显。
  • 副本占用 1 倍,可接受。

最佳实践

  • 控制映射字段数 < 1000,使用 dynamic: false
  • 定期合并只读索引段。

8.8 elastic 用户默认密码未改导致数据泄露

场景深潜
安全团队扫描发现公网 ES 集群无认证,使用 elastic/changeme 可直接登录。经查,运维在搭建后未修改密码。

排查

  • 尝试 curl -u elastic:changeme http://es:9200/,返回 200 且可删除索引。
  • 检查 elasticsearch.ymlxpack.security.enabled: true 已开启,但密码未改。

根因:忽视了内置用户密码修改,等同于大门洞开。详见第 8 篇安全体系

修复

  • 立即执行 elasticsearch-reset-password -u elastic 设置强密码。
  • 审查所有 API Key,禁用非预期 Key。
  • 启用 TLS,配置防火墙仅允许内网。

最佳实践

  • 集群初始化 checklist 第一条:修改所有内置用户密码。

8.9 force_merge 后性能反弹

场景深潜
对正在写入的索引执行 _forcemerge?max_num_segments=1,查询延迟从 500ms 降到 100ms,但 2 天后又回到 400ms。

排查

  • GET _cat/segments/index?v 显示段数从 1 重新增长到 200。
  • 该索引仍在持续写入,新段不断生成,而合并策略默认较保守。
  • 根因force_merge 是一次性操作,不解决写入率与合并率的动态平衡。详见 3.2 案例

修复

  • 只在索引变为只读(如 ILM cold 阶段)时执行 force_merge
  • 调整合并策略和限速,使自动合并能跟上写入速率(index.merge.policy.segments_per_tier 等)。

最佳实践

  • 合并应是一个后台持续的过程,不要依赖一次性的强制合并解决在线索引性能问题。

8.10 滚动升级后节点无法加入集群

场景深潜
从 8.5 升级到 8.7,先升级一个数据节点,重启后该节点日志显示 MasterNotDiscoveredException

排查

  1. 检查节点配置,discovery.seed_hosts 指向了主节点列表,地址正确。
  2. 检查网络,能 ping 通主节点。
  3. 查看主节点日志,发现 node xxx has a different major version 错误。原来主节点还在 8.5 版本,滚动升级需先升级所有主节点。根因:升级顺序错误。详见第 10 篇运维升级

修复

  • 按文档要求:先升级所有 master 候选节点,再升级 data 节点。
  • 调整计划,将 master 节点逐一升级。

最佳实践

  • 严格按照官方滚动升级顺序,且升级前在测试环境验证。

8.11 快照恢复速度慢

场景深潜
从 S3 快照恢复一个 600GB 索引,速率仅 20MB/s,预计需 8 小时,不满足 RTO。

排查

  • GET _cat/recovery?index=restored_idx&active_only=true 显示 bytes_per_sec 20MB。
  • 检查集群设置 indices.recovery.max_bytes_per_sec 为 40mb。
  • 节点网络带宽充裕,但 S3 读吞吐限制为单分片 25MB/s?检查 S3 性能。
  • 发现恢复时目标节点磁盘为 HDD,I/O 成为瓶颈。

根因:恢复限速和磁盘 I/O 限制导致速度慢。详见运维篇快照恢复

修复

  • 调高恢复带宽:PUT _cluster/settings {"transient": {"indices.recovery.max_bytes_per_sec": "200mb"}}
  • 增加并发恢复数:cluster.routing.allocation.node_concurrent_recoveries: 4
  • 若可能,将目标索引分配到 SSD 节点恢复。

最佳实践

  • 预先规划恢复带宽,在灾难恢复时临时调高限速。

8.12 综合设计题:搜索服务集群故障应急预案与监控体系

设计要求
为一套核心搜索服务(24x7,日均 1 亿查询,500GB 索引,10 节点)设计一套故障应急与监控方案,基于本文决策树思想。

详细方案

  1. 监控指标体系
    • 集群级:_cluster/health statuspending_tasks 数、active_shards
    • 节点级:CPU、Heap%、磁盘使用、_cat/nodes 的 load。
    • 查询级:P50/P99 延迟、QPS、慢查询计数。
    • 写入级:_cat/thread_pool/write rejected、indexing rate。
    • 段与内存:_cat/segments 每分片段数,fielddata size,GC stats。
  2. 告警阈值
    • RED 状态:立即 PagerDuty。
    • Heap > 85% 持续 5min 或 Full GC 频率 > 5次/10min。
    • 磁盘使用 > 85%。
    • 写入拒绝 > 0。
    • 查询 P99 > 500ms。
  3. 应急预案 Runbook(基于决策树)
    • 查询变慢:自动抓取慢查询,调用 _profile API 分析,若为聚合问题则通知聚合优化组;若为缓存问题则建议清理或调整。
    • 集群 RED:自动执行 _cluster/health_cat/shards_cluster/allocation/explain 并汇总报告。提供磁盘清理脚本(删除匹配 *-old-* 索引)和一键重试分配脚本。
    • OOM:自动采集 heap dump 路径(配置 -XX:HeapDumpOnOutOfMemoryError),自动清理 fielddata cache,并重启节点(需审批)。通知查询开发限制大聚合。
    • 写入拒绝:自动检查磁盘水位,若超限执行清理;动态调大日志索引 refresh_interval;若仍无缓解,触发扩容或限流。
  4. 自动化与工具
    • 使用 Elasticsearch Exporter + Prometheus + Grafana 实现可视化监控。
    • 使用 Alertmanager 管理告警路由。
    • 自研 Bot 或脚本,可执行诊断命令并推送结果到值班群。
    • 定期(每周)故障演练,验证恢复流程。
    • ILM 策略:索引 30GB 或 1 天滚动,hot-warm-cold 分层,warm 阶段 force merge,cold 阶段 shrink 分片。

扩展思考:预案应区分非关键索引(如日志)和核心索引(订单),前者可自动执行清理,后者必须人工确认防止数据丢失。监控需覆盖混合部署下的网络分区风险。


延伸阅读

  • 《Elasticsearch: The Definitive Guide》
  • Elasticsearch 官方文档 Troubleshooting 章节
  • Kibana 用户指南

ES 排障工具速查表

工具名作用域关键命令示例典型现象映射
_cluster/health集群状态GET _cluster/health?level=shardsRED/YELLOW,未分配分片
_cat/nodes节点资源GET _cat/nodes?v&h=name,heap.percent,role节点 OOM、CPU 高
_cat/shards分片分布GET _cat/shards?h=index,shard,state,unassigned.reason未分配、分配不均
_cat/thread_pool线程池状态GET _cat/thread_pool/write?v写入拒绝、队列堆积
_nodes/hot_threadsCPU 热点GET _nodes/hot_threads查询脚本、合并线程卡顿
_nodes/stats/jvmJVM 内存GET _nodes/stats/jvm?prettyFull GC、堆内存耗尽
_profile查询剖析POST _profile { "query": ... }慢查询各阶段耗时
_explain评分分析GET /index/_explain/1 { "query": ... }不期望的文档匹配
_cluster/allocation/explain分配诊断GET _cluster/allocation/explain { "index": "x", "shard": 0 }分片分配失败原因
_cat/segments段信息GET _cat/segments?v&h=index,segment,docs.count段过多、存储膨胀
_security/api_keyAPI Key 管理GET _security/api_key权限过大、未过期
_xpack/security/_authenticate用户认证GET _xpack/security/_authenticate确认当前用户权限

本文通过 24 个反模式案例和系统化的诊断决策树,将前 10 篇的核心知识转化为实战排障能力,帮助 ES 工程师在线上故障时快速回溯根因并采取正确修复措施。掌握这套方法论,不仅能应对常见故障,更能从根本上设计出更健壮的 ES 集群。