概述
衔接前文段落
本系列从 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. 面试高频故障排查专题]
架构图说明
- 总览说明:全文 8 个模块以前 5 个反模式领域的案例分析为主体,每个案例采用设计/运行时双视角剖析,后接诊断工具集和决策树,最后以面试故障排查专题收束。
- 逐模块说明:模块 1-5 覆盖 ES 全生命周期中的典型错误,每个案例根因直接引用前文原理并深入源码;模块 6 提供可打印的速查工具箱;模块 7 绘制故障决策路径图;模块 8 面试巩固。
- 关键结论: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.version、error.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 排查思路
- 使用
GET /logs/_mapping统计字段数量,或执行GET /logs/_field_caps?fields=*获取全局字段信息。 - 查看索引设置:
GET /logs/_settings,确认index.mapping.total_fields.limit是否维持默认或已被手动调高。 - 检查写入端代码,确认是否将未受控的 JSON 直接写入 ES,可通过抓取写入日志或使用
_cat/indices?v&h=docs.count对比文档数与预期是否相符。 - 使用
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设置为false或strict。
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 排查思路
GET /products/_mapping确认category为text类型。GET _nodes/stats/indices/fielddata?fields=category观察memory_size。- 使用
_profile查看聚合阶段,build_ordinals或collect时间占比极高。 - 检查集群堆内存使用
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_values 与 fielddata 的内存模型。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 最佳实践
- 所有用于排序、聚合、精确匹配的字符串字段,一律使用
keyword或text的keyword子字段。 - 定期检查 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/indices中docs.count增长速度与预期不符。 - 磁盘空间消耗远大于同等数据量的普通对象结构索引。
- 使用
_nodes/hot_threads观察,看到大量线程执行NestedDocumentParser相关操作。
1.3.3 排查思路
GET _cat/indices/products?v&h=index,pri,rep,docs.count,store.size对比文档数和存储量。POST _reindex预演:创建一个测试索引将部分文档索引为非nested类型,比较写入速度和存储大小。_nodes/hot_threads检查热点线程,确认NestedDocumentWriter或Lucene80DocValuesProducer等耗时。- 查看段合并线程状态,
_cat/thread_pool/force_merge或GET _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/nodes的fd.used很高。 - 查询延迟 P50 和 P99 均升高,尤其对
match_all等需要扫描多个分片的查询。 _cat/segments统计显示整个索引的 segment 总数超过 5000,而平均每个分片 segment 数约 500+。
1.4.3 排查思路
GET _cat/shards/logs-2024.01.01?v查看分片分布和大小。GET _cat/segments/logs-2024.01.01?v&h=index,shard,segment,docs.count,size统计 segment 数量。GET _nodes/stats/indices/segments获取 segment memory 占用。- 计算每个分片平均 segment 数,远超过 50~100 的健康范围。
- 检查索引设置,确认分片数是否过高。
1.4.4 根因分析 每个分片是一个独立的 Lucene 索引实例,内部包含多个 segment。ES 的段合并(Merge)策略默认会在各分片内独立运行。当分片数过多时:
- 所有分片的 segment 元数据都需要在节点堆内存中维护,导致 heap 压力大。
- 每个 segment 会打开文件句柄,分片数 * segment 数决定了文件句柄数量,可能耗尽 OS 限制。
- 查询需要访问所有相关分片,分片过多增加了协调节点的扇出开销,也使得缓存效率降低。
根因详见第 7 篇集群分布式原理与分片划分,以及第 3 篇段合并机制。
1.4.5 修正方案
- 对历史数据,通过
_reindex到分片数更合理的新索引,如 2 个主分片。 - 新索引结合 ILM 和 Rollover,每个索引大小控制在 20
50 GB,分配 13 个主分片。 - 对现有大量段的情况,可在只读状态下执行
_forcemerge?max_num_segments=5来减少段数,但必须在非高峰期执行。
1.4.6 最佳实践
- 单个分片大小控制在 10
50 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 排查思路
GET _cat/shards/orders?v查看分片大小。_cat/nodes检查各节点负载是否均衡。_cluster/health确认无未分配分片,但查询延迟高。- 使用
_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 排查思路
GET /logs/_settings获取refresh_interval。GET _cat/thread_pool/write?v检查队列和拒绝数。GET _nodes/stats/indices/refresh统计 refresh 总次数和耗时,发现 refresh 频率极高。- 评估业务对日志可见性的要求,确认是否可以容忍 30s~60s 延迟。
1.6.4 根因分析 每次 refresh 都会将内存 buffer 中的数据生成新的 segment 并打开,使新写入的文档可被搜索。这涉及:
- 创建新的 segment 元数据和文件句柄。
- 清空内存 buffer,并触发后续可能的段合并。
- 频繁的 refresh 会导致大量小 segment 产生,增加合并压力和 I/O。
根因详见第 3 篇写入刷新(refresh)机制与性能影响。
1.6.5 修正方案
- 对于日志索引,将
refresh_interval调整为30s或60s:
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 排查思路
GET _cat/segments/index?v&h=index,shard,segment,docs.count统计段数。GET _nodes/stats/indices/merge查看合并速率和节流时间。- 检查集群设置:
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 breaker或request breaker限制。 - 协调节点堆内存 spike,Minor GC 频繁,甚至 Full GC。
- 慢查询日志记录此查询耗时 >10s。
2.1.3 排查思路
- 使用
_profile查看查询,发现协调节点排序阶段耗时巨大,且内存占用高。 GET _nodes/stats/jvm查看协调节点的 heap 使用,old_gen接近满。- 检查查询参数,发现
from值过大。 - 查看
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]
}
- 若需导出大量数据,使用
scrollAPI,注意其维护的搜索上下文会占用资源,用后需清理。 - 限制
from参数,在前端使用“无限滚动”替代跳页。
2.1.6 最佳实践
- 业务分页必须使用
search_after或scroll(非实时)。 - 严格限制
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输出显示WildcardQuery的next_doc耗时占比极大,且扫描文档数等于总文档数。
2.2.3 排查思路
_profile详细输出显示matched_terms为大量甚至全部 term。- 检查通配符位置,发现以
*开头。 - 通过
_explain查看任意文档为何匹配,理解其需遍历 term 字典。
2.2.4 根因分析
Lucene 的倒排索引 term 字典使用 FST 结构,能够高效支持前缀搜索(如 prefix 查询)。但前导通配符 *keyword 相当于“任意前缀”,FST 无法提供加速,必须遍历整个 term 字典,对每个 term 获取倒排列表进行匹配。这等效于全索引扫描,性能极差。
根因详见第 2 篇倒排索引与 FST 数据结构,以及 WildcardQuery 在 Lucene 中的执行流程。
2.2.5 修正方案
- 禁止使用前导通配符。如果业务需要后缀搜索,可在索引时对字段做
reverse分词,然后使用prefix查询。 - 使用
edge_ngramtoken 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 排查思路
GET _nodes/stats/indices/fielddata?fields=user_id查看 fielddata 占用,发现异常大。GET _nodes/stats/jvm查看堆使用,heap_used_percent接近 100%。_profile分析聚合查询,collector阶段内存分配巨大。- 定位到具体的 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显示TermQuery的score时间占比较多。 - Query Cache 命中率极低,
GET _nodes/stats/indices/query_cache显示hit_count远小于miss_count。 - CPU 使用率偏高,但查询逻辑简单。
2.4.3 排查思路
_profile分析查询,TermQuery的breakdown中score不为零。- 检查
bool结构,term放在了must而不是filter。 _stats/query_cache查看缓存利用情况。
2.4.4 根因分析
query 上下文(must、should)会计算文档的相关度评分(_score),即使对于 term 这种确定性查询,每次也需要进行评分计算,无法利用 Query Cache。filter 上下文不会计算评分,只进行文档匹配,并且匹配结果可被自动缓存,后续相同过滤条件直接复用缓存结果,跳过磁盘读取和评分。
根因详见第 5 篇高级查询 DSL 中 filter 与 query 上下文的区别,以及 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 时间消耗在ScriptScoreQuery的score方法上。GET _nodes/stats/indices/script_cache显示编译缓存正常,但执行开销巨大。
2.5.3 排查思路
_profile定位到script_score阶段耗时占比超过 80%。- 查看脚本逻辑,涉及
Math.log等计算。 - 测试移除
script_score后的基准查询时间,发现差异巨大。
2.5.4 根因分析
脚本在 Lucene 层对每个匹配的文档逐个执行,无法利用索引加速。即使脚本编译后缓存在 ScriptCache 中,执行阶段仍需为每个文档调用 Painless 引擎进行计算,CPU 开销随文档数量线性增长。复杂的数学运算和多字段访问会显著放大延迟。
根因详见第 5 篇脚本查询与性能考量,以及 Painless 脚本引擎的执行模型。
2.5.5 修正方案
- 写入时预计算:增加一个
score_factor字段,在写入时计算好Math.log(2 + clicks * weight),查询时使用function_score的field_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
图下四层说明
- 图例说明:该流程图展示了从发现查询变慢到定位根因的标准排查路径,覆盖
_profile分析、缓存诊断和查询结构调整。 - 诊断路径解读:首选慢查询日志收集具体语句;再利用
_profile的breakdown拆解各阶段耗时,快速区分是查询阶段还是聚合阶段的问题;最后根据问题类型采取优化。 - 关键命令解析:
GET _nodes/stats/indices/query_cache获取缓存统计;POST /_profile注入查询进行性能剖析;GET _cat/thread_pool判断是否因排队导致延迟。 - 常见误区纠正:不要一遇到慢查询就加硬件;避免忽略
filter缓存的巨大收益;_profile结果应关注next_doc和advance时间而不是总时间。
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/search和write队列积压。 - 集群负载急速攀升,甚至导致节点假死。
3.2.3 排查思路
_cat/tasks?detailed&actions=indices:admin/forcemerge*查看当前正在执行的强制合并任务。iostat -x 1或通过监控查看磁盘读写 IOPS 和吞吐。_nodes/hot_threads显示大量线程阻塞于write_bytes或fsync。- 确认
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 mergeaction 在索引进入 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/write的active线程数少,但延迟尚可。 - 批次过大:写入延迟抖动大,节点 GC 频繁,
_cat/thread_pool/write出现rejected,_nodes/stats/jvm显示 heap 突然飙升。
3.3.3 排查思路
GET _cat/thread_pool/write?v查看活跃、队列、拒绝。- 调整客户端批量大小进行压测,观察吞吐量和延迟变化。
- 监控节点 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 大小。
- 启用
_bulk的pipeline来处理批次失败重试。
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-mapping或create-index任务长时间未完成。 - 节点 Full GC 时,可能触发主节点选举风暴,日志中出现
master left和elected 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/settings无cluster.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: rack1、rack2。 - 开启强制感知:
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/healthstatus = red。 _cat/shards显示主分片UNASSIGNED,原因NODE_LEFT或ALLOCATION_FAILED。- 错误日志
unassigned shard, reason=NODE_LEFT, node_left=[node1]。
4.4.3 排查思路
GET _cluster/health?level=shards,找出未分配分片。GET _cat/shards?h=index,shard,prirep,state,unassigned.reason查看具体原因。GET _cluster/allocation/explain请求体指定index和shard,获取详细决策信息,如node_allocation_decisions中每个节点的deciders解释。- 若原因
NODE_LEFT且节点已永久丢失,立即检查是否有可用快照。
4.4.4 根因分析
当主分片由于节点离开或磁盘故障导致无法分配时,集群会变为 RED。若该主分片的唯一副本不可用(或未分配),则数据丢失风险极高。分配解释 API 可展示具体原因:例如 disk_watermark_low_failed、shard_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
图下四层说明
- 图例说明:序列图展示了运维人员通过三个核心 API 逐步定位 RED 原因的流程。
- 诊断路径解读:第一步用
_cluster/health确认整体状态和未分配分片数;第二步用_cat/shards查看分片状态和未分配原因字段;第三步用_cluster/allocation/explain获取详细分配失败解释。 - 关键命令解析:
_cluster/allocation/explain可指定index和shard,返回node_allocation_decisions,包含每个节点的判断结果,是诊断分配问题的利器。 - 常见误区纠正:不要盲目重启节点;RED 时优先诊断而不是修改配置;
unassigned.reason为NODE_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:图形化查询剖析,定位查询耗时阶段。_profileAPI:类似 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/explain | GET _cluster/health?level=shards | 主分片未分配(磁盘满、节点离线) |
| 查询 P99 突增 | Kibana Profiler、_profile | POST _profile 分析 breakdown | 未使用 filter、深分页、脚本开销 |
| 节点 OOM | _nodes/stats/jvm、_cat/nodes | GET _nodes/stats/jvm?filter_path=**.heap_used_percent | terms 聚合 size 过大、fielddata 膨胀 |
| 写入拒绝 | _cat/thread_pool、_cat/health | GET _cat/thread_pool/write?v | 写入线程池队列满、磁盘水位线 |
| 索引存储膨胀 | _cat/indices、_cat/segments | GET _cat/segments?v&h=index,shard,docs.count,size | 字段爆炸、segment 数过多 |
| 查询缓慢但 CPU 不高 | _profile、_explain | GET _explain 查看评分细节 | 磁盘 I/O 瓶颈、segment 数量大 |
| CPU 突然飙升 | _nodes/hot_threads、_nodes/stats | GET _nodes/hot_threads | 复杂脚本、大量通配查询、段合并 |
| 集群 YELLOW 持续 | _cat/shards、_cluster/allocation/explain | GET _cat/shards?h=index,shard,prirep,state | 副本未分配,节点数不足或磁盘空间 |
| 节点掉线频繁 | _cat/nodes、_cluster/health | 日志查看主节点选举 | 主数据混合、GC 停顿、网络分区 |
| 安全事件 | _xpack/security/_authenticate、_security/api_key | GET _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 膨胀
图下四层说明
- 图例说明:序列图演示了从发现节点 OOM 到定位根因的多工具联合排查流程。
- 诊断路径解读:首先检查 JVM 堆内存使用,确认 OOM 现象;接着查看 fielddata 内存和 segment 数量等内存占用大户;最后通过
_profile分析罪魁祸首查询的聚合细节。 - 关键命令解析:
_nodes/stats/jvm可查看堆内存各代使用;_nodes/stats/indices/fielddata得到 fielddata 占用;_profile显示聚合的build_ordinals等阶段消耗。 - 常见误区纠正:不能仅通过堆总大小判断,需细分 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[检查网络或单节点压力]
图下四层说明
- 图例说明:总图汇总了四种常见故障的决策路径,每个节点包含判断逻辑和下一步操作。
- 诊断路径解读:从故障现象出发,进入对应分支。查询变慢侧重剖析执行阶段;RED 侧重分片分配诊断;OOM 侧重内存构成分析;写入拒绝侧重线程池与资源。
- 关键命令解析:各分支均明确标注了需使用的 API,如
_cluster/allocation/explain,_profile等,并提供检查方向。 - 常见误区纠正:避免跳跃式排查,应严格遵循决策树顺序;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,查看搜索线程池的active、queue、rejected。- 若
queue持续不为零,说明搜索请求出现排队,可能是 CPU 不足或单次查询太慢。 - 若
rejected增加,线程池已满,需紧急扩容或限流。
- 若
- 执行
GET _nodes/stats/indices/merge查看当前段合并状态,如果current不为零且total_time_in_millis在近期激增,说明合并操作正在争抢 I/O 资源(根因见第 3 篇段合并机制)。 - 执行
iostat -x 1或云监控查看磁盘await和util。如果磁盘 I/O 接近饱和,查询延迟必然上升。
第 3 层:定位慢查询的执行阶段
使用 Kibana Profiler 或 _profile API 对典型慢查询进行分析:
POST /products/_profile
{
"profile": true,
"query": { ... } // 粘贴慢查询语句
}
分析返回的 breakdown,重点看每个阶段的耗时占比:
next_doc或advance耗时高(> 100ms 且占比 > 30%):说明倒排链扫描本身开销大。可能原因:- 查询匹配的文档量过多,或未使用
filter导致评分计算(见 2.4 案例)。 - 通配符前缀
*扫描(见 2.2 案例)。 - 段数量过多,每个查询需遍历大量小段。可通过
GET _cat/segments/index?v查看段数,如果每分片段数 > 100,则段膨胀严重(见 1.7 案例)。
- 查询匹配的文档量过多,或未使用
score耗时高:说明评分计算占用大量 CPU。检查是否在must中使用了term等确定性条件,应移到filter。检查是否有script_score或function_score的复杂脚本(见 2.5 案例)。build_scorer耗时高:可能与复杂的布尔查询、嵌套查询有关。- 聚合阶段(
collect、build_ordinals)耗时高:说明聚合开销大。检查:terms聚合的size是否过大(见 2.3 案例)。- 聚合字段是否为
text类型,触发了fielddata构建(见 1.2 案例)。 - 是否存在
nested聚合,导致子文档聚合扫描。
第 4 层:检查缓存命中率
- 执行
GET _nodes/stats/indices/query_cache,计算hit_count / (hit_count + miss_count)。若命中率低,说明可缓存的过滤条件未被重用,或者缓存大小不足。- 优化:将
term、range等确定性过滤统一放入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/health | green | YELLOW/RED 影响查询 |
| 节点负载 | GET _cat/nodes?v | CPU < 80%, load 适中 | 热点节点,资源争抢 |
| 搜索线程池 | GET _cat/thread_pool/search | queue=0, rejected=0 | 查询排队,OOM 风险 |
| 慢查询日志 | 文件/Kibana | 无超阈值语句 | 具体慢查询信息 |
| Profile breakdown | _profile | next_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,关注status、unassigned_shards、active_primary_shards。 - 红色必须立即响应。如果有活跃写入受到影响,可临时将索引只读或通知上游降级。
第 2 层:定位未分配分片
GET _cat/shards?v&h=index,shard,prirep,state,unassigned.reason,docs,store,node
过滤出 state 为 UNASSIGNED 且 prirep 为 p 的记录。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 层:分类处理
- 磁盘空间不足 (
disk_threshold):- 现象:
explain输出disk_usage_exceeded,_cat/allocation显示磁盘使用超限。 - 解决:删除旧索引或清理无用数据,或扩大磁盘/调整水位线。紧急情况下先临时调高
disk.watermark.flood_stage并解除只读锁(PUT /index/_settings {"index.blocks.read_only_allow_delete": null}),再清理空间。
- 现象:
- 节点离线 (
NODE_LEFT):- 检查
_cat/nodes确认节点是否已恢复。若节点可修复,重启节点即可,ES 会自动重新分配分片。 - 若节点永久丢失且无副本可用,数据丢失风险极大。此时可选择:
- 从快照恢复分片(
POST /_snapshot/repo/snapshot/_restore)。 - 接受数据丢失,手动分配空主分片(
POST _cluster/reroute { "commands": [{ "allocate_empty_primary": { ... } }] }),务必谨慎。
- 从快照恢复分片(
- 检查
- 分片分配规则限制:检查
cluster.routing.allocation.include/exclude等设置是否误将分片排挤。 - 分配重试耗尽:执行
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/jvm中gc.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查看total和total_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 无显著变化,也未做代码发布。
排查过程详解
- 宏观检查:
GET _cluster/health→status: green,排除 RED 影响。GET _cat/nodes?v&h=name,heap.percent,cpu,load_1m发现节点 2 的load_1m很高,cpu80% 且heap.percent65% 正常。GET _cat/thread_pool/search?v显示active=2,queue=15,rejected=0。队列堆积说明处理能力不足。
- 定位慢查询:Kibana Slow Logs 中看到多条
terms查询,took: 4500ms。复制查询语句。 - Profiler 分析:
- 执行
POST /products/_profile并粘贴该查询。Breakdown 显示聚合阶段collect耗时占比 80%,进一步查看是terms聚合在字段tags上。 GET /products/_mapping发现tags是text类型,未设置keyword子字段。GET _nodes/stats/indices/fielddata?fields=tags显示memory_size达 3GB,且不断增长。
- 执行
- 确认根因:
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 个索引的主分片未分配。
排查步骤与命令解读
GET _cluster/health?level=shards:{ "status": "red", "unassigned_shards": 5, "active_primary_shards": 300, ... }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- 两个分片因节点离开,一个因分配失败。
- 深入分配失败分片:
POST _cluster/allocation/explain {"index": "indexB", "shard": 0, "primary": true}: 返回显示can_allocate: no,allocate_explanation: "cannot allocate because disk usage exceeded the high watermark on all eligible nodes"。检查_cat/allocation,发现多数节点磁盘使用 92%,超过默认high watermark=90%。 - 根因分析:磁盘空间不足导致部分分片分配失败。同时离线的节点可能因为磁盘满导致进程异常退出,加剧了未分配分片数量。根因详见第 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 掉底,查询超时率上升。
排查路径
- JVM 统计:
GET _nodes/node3/stats/jvm:确认老年代压力大,Full GC 频繁。"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 } } } - 内存分解:
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) 字段数据异常高。
- 字段分析:
GET _nodes/stats/indices/fielddata?fields=*发现字段user_comment占用 5.8GB,该字段为text类型,且有一个业务监控面板频繁对其执行terms聚合,size参数设为 100000。 - 根因:
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 不断跳动。
排查过程
- 线程池详情:
GET _cat/thread_pool/write?v:node_name active queue rejected queue_size node1 8 200 150 200queue打满,active线程数达到核数,写入任务处理不完。 - 检查节点资源:
_cat/nodes显示 CPU 80%,磁盘 util 90%。 - 查看 refresh 统计:
GET _nodes/stats/indices/refresh显示每秒 refresh 操作 500 次,total_time高。索引设置refresh_interval仍为默认1s,但日志索引不需要如此高的实时性。 - 检查 Bulk 大小:客户端每个 bulk 只有 100 条文档,约 5KB,导致大量小请求,线程池频繁切换。
- 根因:refresh 间隔过小,生成过多小段,I/O 压力大;Bulk 太小,网络和处理开销占高。两者结合导致写入线程池饱和并拒绝。详见 3.1 和 3.3 案例。
修复
- 动态调整日志索引
refresh_interval为30s:PUT /logs-*/_settings {"index": {"refresh_interval": "30s"}}。 - 调整 Bulk 批次大小为 10MB,约 2000 条文档。
- 观察
rejected逐渐归零,写入恢复。
最佳实践
- 写多读少的日志/指标场景,
refresh_interval设置 30s~60s。 - 通过压测确定最优 Bulk 大小,并使用异步 bulk 处理器。
8.5 搜索“苹果”匹配到大量无关文档
场景深潜
用户搜索“苹果”,期望找到包含“苹果手机”或“苹果”的文档,结果却返回了大量包含“水果苹果”、“苹果公司”甚至“苹”、“果”拆开匹配的文档。
排查
GET /articles/_mapping查看title字段使用standard分析器。GET /articles/_analyze {"field": "title", "text": "苹果"}得到["苹", "果"],说明中文被逐字分词。- 查询使用
match匹配,每个单字都会被查找,导致高召回低精度。 - 根因:
standard分析器按 Unicode 标准切分中文为单字,没有中文词汇识别能力。详见第 6 篇中文分词实战。
修复
- 安装
elasticsearch-analysis-ik插件,将索引映射改为ik_max_word或ik_smart分析器。 - 重建索引后,搜索“苹果”只匹配包含“苹果”词条的文档。
最佳实践
- 中文业务必须使用专业分词器,并在索引前通过
_analyze测试分词效果。
8.6 聚合结果桶数量不符预期
场景深潜
一个 terms 聚合查询按 user_id 统计,预期 Top 1000 活跃用户,却只返回了 500 个桶,有些高频用户缺失。
排查
- 检查聚合 DSL:
"terms": { "field": "user_id", "size": 1000 }。 - 查看
shard_size默认值:即使size设置为 1000,每个分片默认返回的桶数等于size * 1.5 + 10,但协调节点最终只取前 1000。如果某个用户在所有分片上都不是前 1000,就会被遗漏。 - 使用
GET _cluster/settings?include_defaults=true查看search.default_allow_partial_results等。 - 根因:
terms聚合是近似精确的,当文档分布不均时,全局高频词可能在部分分片排名较低而丢失。详见第 5 篇聚合引擎中的terms聚合精度。
修复
- 增加
shard_size到 5000 或更大(但需评估内存)。 - 或改用
composite聚合分页获取全量结果。 - 或使用
cardinality获取去重数,但无法得到桶。
最佳实践
- 需要精确 Top N 时,
shard_size应远大于size,建议 2~3 倍,内存允许下可更大。 - 业务允许近似值时使用
cardinality。
8.7 索引存储空间远超原始数据
场景深潜
50GB 原始 JSON 数据,索引后占用 300GB 磁盘(含 1 副本),怀疑空间膨胀异常。
排查
GET _cat/indices?v&h=index,pri,rep,docs.count,pri.store.size→pri.store.size210GB。GET _cat/segments/index?v显示段数 4000,每段平均 50MB。GET /index/_mapping统计字段数,发现 3500 个字段,大量由动态映射生成。- 分析存储组成:
_source压缩、doc_values、倒排索引、norms 等。字段爆炸导致每个字段都要建立倒排和列存,存储成倍放大。 - 根因:动态映射失控,字段过多,加上段未及时合并(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.yml中xpack.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。
排查
- 检查节点配置,
discovery.seed_hosts指向了主节点列表,地址正确。 - 检查网络,能 ping 通主节点。
- 查看主节点日志,发现
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_sec20MB。- 检查集群设置
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 节点)设计一套故障应急与监控方案,基于本文决策树思想。
详细方案
- 监控指标体系:
- 集群级:
_cluster/health status、pending_tasks数、active_shards。 - 节点级:CPU、Heap%、磁盘使用、
_cat/nodes的 load。 - 查询级:P50/P99 延迟、QPS、慢查询计数。
- 写入级:
_cat/thread_pool/write rejected、indexing rate。 - 段与内存:
_cat/segments每分片段数,fielddata size,GC stats。
- 集群级:
- 告警阈值:
- RED 状态:立即 PagerDuty。
- Heap > 85% 持续 5min 或 Full GC 频率 > 5次/10min。
- 磁盘使用 > 85%。
- 写入拒绝 > 0。
- 查询 P99 > 500ms。
- 应急预案 Runbook(基于决策树):
- 查询变慢:自动抓取慢查询,调用
_profileAPI 分析,若为聚合问题则通知聚合优化组;若为缓存问题则建议清理或调整。 - 集群 RED:自动执行
_cluster/health、_cat/shards、_cluster/allocation/explain并汇总报告。提供磁盘清理脚本(删除匹配*-old-*索引)和一键重试分配脚本。 - OOM:自动采集 heap dump 路径(配置
-XX:HeapDumpOnOutOfMemoryError),自动清理 fielddata cache,并重启节点(需审批)。通知查询开发限制大聚合。 - 写入拒绝:自动检查磁盘水位,若超限执行清理;动态调大日志索引
refresh_interval;若仍无缓解,触发扩容或限流。
- 查询变慢:自动抓取慢查询,调用
- 自动化与工具:
- 使用 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=shards | RED/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_threads | CPU 热点 | GET _nodes/hot_threads | 查询脚本、合并线程卡顿 |
_nodes/stats/jvm | JVM 内存 | GET _nodes/stats/jvm?pretty | Full 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_key | API Key 管理 | GET _security/api_key | 权限过大、未过期 |
_xpack/security/_authenticate | 用户认证 | GET _xpack/security/_authenticate | 确认当前用户权限 |
本文通过 24 个反模式案例和系统化的诊断决策树,将前 10 篇的核心知识转化为实战排障能力,帮助 ES 工程师在线上故障时快速回溯根因并采取正确修复措施。掌握这套方法论,不仅能应对常见故障,更能从根本上设计出更健壮的 ES 集群。