Elasticsearch 性能优化与排查
ES 线上问题通常不是一句“加机器”就能解决。查询慢、写入慢、搜不到、结果不准、集群 yellow/red、磁盘只读、JVM 抖动、分片未分配,都要从数据模型、查询 DSL、同步链路、分片规划和集群资源一起看。
学习目标
| 目标 | 需要掌握什么 |
|---|---|
| 会排查搜不到 | 主库、同步、ES 文档、Mapping、分词、查询 DSL、权限过滤 |
| 会排查搜不准 | analyzer、同义词、字段权重、filter、_explain |
| 会排查查询慢 | slowlog、profile、深分页、大聚合、wildcard、脚本排序 |
| 会排查写入慢 | bulk、refresh、副本、merge、thread pool、磁盘 IO |
| 会排查集群异常 | health、cat shards、allocation explain、磁盘水位 |
| 会做优化 | Mapping、分片、缓存、分页、聚合、冷热数据、限流 |
总体排查入口
flowchart TD
A["ES 线上问题"] --> B{"问题类型"}
B -- "搜不到" --> C["查同步和查询条件"]
B -- "搜不准" --> D["查分词和评分"]
B -- "查询慢" --> E["查 DSL 和资源"]
B -- "写入慢" --> F["查 bulk、refresh、merge"]
B -- "集群异常" --> G["查 health、shards、磁盘"]第一步先分类,不要一上来重启 ES。
常用排查命令
GET /_cluster/health?pretty
GET /_cat/nodes?v
GET /_cat/indices?v&s=store.size:desc
GET /_cat/shards?v
GET /_cluster/allocation/explain
GET /_nodes/stats/jvm,fs,os,thread_pool
GET /_nodes/hot_threads
GET /product_search/_settings
GET /product_search/_mapping| 命令 | 看什么 |
|---|---|
_cluster/health | 集群 green/yellow/red |
_cat/nodes | 节点 CPU、内存、角色 |
_cat/indices | 索引大小、文档量、状态 |
_cat/shards | 分片分布和未分配原因 |
allocation/explain | 为什么分片不能分配 |
_nodes/hot_threads | 当前热点线程 |
_mapping | 字段类型是否正确 |
_settings | 分片、副本、refresh 等配置 |
搜不到怎么排查
搜不到通常不是 ES 坏了,而是某一层条件不匹配。
flowchart TD
A["用户说搜不到"] --> B["确认主库是否有数据"]
B --> C["按 ID 查 ES 文档"]
C --> D{"ES 是否有文档"}
D -- "没有" --> E["查同步链路、MQ、CDC、补偿"]
D -- "有" --> F["查查询 DSL"]
F --> G["查 Mapping 字段类型"]
G --> H["用 _analyze 看分词"]
H --> I["查 filter、权限、状态是否误伤"]按 ID 查文档:
GET /product_search/_doc/10001看分词:
GET /product_search/_analyze
{
"field": "productName",
"text": "无线蓝牙降噪耳机 Pro"
}常见原因:
| 现象 | 原因 | 处理 |
|---|---|---|
| 主库有,ES 没有 | 同步失败、消息堆积、CDC 延迟 | 查同步日志、MQ Lag、死信、补偿 |
| ES 有,按关键词搜不到 | 分词不匹配、term 查了 text | 用 _analyze,改 match 或 Mapping |
| ES 有,某些用户搜不到 | 权限、租户、状态过滤误伤 | 打印最终 DSL,检查 filter |
| 新写入短时间搜不到 | refresh 未发生 | 接受近实时或必要时手动 refresh |
| 改了 analyzer 仍搜不到 | 历史倒排未重建 | 重建索引 |
搜不准怎么排查
搜不准包括:不相关结果排前面、相关结果排后面、召回太少、召回太多。
flowchart TD
A["搜索结果不准"] --> B["查看最终 DSL"]
B --> C["用 _analyze 看索引和查询分词"]
C --> D["用 _explain 看评分"]
D --> E["检查字段 boost"]
E --> F["检查 should、filter、排序规则"]
F --> G["调整同义词、权重、业务排序"]定位单条评分:
GET /product_search/_explain/10001
{
"query": {
"multi_match": {
"query": "无线降噪耳机",
"fields": ["productName^5", "subTitle^2", "tags"]
}
}
}常见原因:
| 问题 | 原因 | 处理 |
|---|---|---|
| 长详情命中文档排前 | 字段权重不合理 | 标题字段提高 boost |
| 品牌词搜不到 | 分词词典缺品牌词 | 加自定义词典或 keyword 召回 |
| 型号词搜不到 | 中英文数字混合分词不合理 | 自定义 analyzer 或额外 keyword 字段 |
| 结果太泛 | should 太多、minimum_should_match 不合理 | 收紧查询条件 |
| 业务排序压过相关性 | 销量、新品、运营权重过强 | 调整 function_score 或排序策略 |
查询慢怎么排查
查询慢先看是不是某类查询慢,而不是所有查询都慢。
flowchart TD
A["查询慢"] --> B["打开 slowlog 或查接口日志"]
B --> C["找到慢 DSL"]
C --> D["profile 分析耗时"]
D --> E{"慢在哪里"}
E -- "深分页" --> F["限制页数或 search_after"]
E -- "聚合" --> G["缩小范围和 bucket"]
E -- "wildcard" --> H["改 ngram 或 search_as_you_type"]
E -- "脚本/排序" --> I["改字段排序或预计算"]
E -- "资源" --> J["查 CPU、GC、磁盘、线程池"]开启慢日志示例:
PUT /product_search/_settings
{
"index.search.slowlog.threshold.query.warn": "2s",
"index.search.slowlog.threshold.fetch.warn": "1s"
}profile 示例:
GET /product_search/_search
{
"profile": true,
"query": {
"bool": {
"must": [
{ "match": { "productName": "无线降噪耳机" } }
],
"filter": [
{ "term": { "stockStatus": "IN_STOCK" } }
]
}
}
}查询慢全过程:慢在 Query 还是 Fetch
很多人排查 ES 慢查询时只看 DSL,然后直接改索引或加机器。更稳的方式是先把一次查询拆成 Query Phase 和 Fetch Phase,再判断慢在哪一段。
flowchart TD
A["搜索请求慢"] --> B["协调节点接收请求"]
B --> C["Query Phase"]
C --> D["分片本地召回、过滤、评分、排序"]
D --> E["协调节点合并 TopN"]
E --> F["Fetch Phase"]
F --> G["按 docId 拉取 _source"]
G --> H["高亮、字段过滤、返回"]两段慢的表现不一样:
| 慢的位置 | 常见原因 | 证据 |
|---|---|---|
| Query Phase 慢 | 倒排候选多、filter 范围大、评分复杂、排序聚合重、分片多 | profile 中 query collector、rewrite、score、aggregation 耗时高 |
| Fetch Phase 慢 | _source 大、高亮字段大、返回字段太多、inner_hits、磁盘缓存未命中 | slowlog fetch 慢、响应体大、profile query 不慢但接口慢 |
| 协调节点合并慢 | 深分页、分片过多、每个分片返回候选多 | from + size 大、协调节点 CPU 高 |
| 集群资源慢 | CPU、GC、磁盘 IO、线程池排队 | hot_threads、nodes stats、search rejected |
排查时不要只说“ES 慢”。要尽量落到这句话:
这条查询慢主要慢在 Query Phase 的候选召回和排序,还是 Fetch Phase 的取
_source和高亮,还是协调节点深分页合并。
profile 结果怎么看
profile: true 会告诉你查询在每个分片上的执行细节。它适合低风险环境、测试环境或线上小流量排查,不建议对高频大查询长期打开。
排查思路:
flowchart TD
A["拿到慢 DSL"] --> B["开启 profile"]
B --> C["看每个 shard 耗时"]
C --> D{"是否单个 shard 特别慢"}
D -- "是" --> E["查热点分片、分片大小、节点资源"]
D -- "否" --> F["看 query / fetch / aggregation 哪段慢"]
F --> G["定位 match、filter、sort、agg、highlight"]常见字段可以这样理解:
| profile 信息 | 说明 | 常见判断 |
|---|---|---|
rewrite_time | 查询重写耗时 | wildcard、prefix、复杂 bool 可能高 |
collector | 收集 TopN 或聚合结果 | 排序、深分页、聚合常体现在这里 |
score | 评分耗时 | must、BM25、function_score、script_score |
next_doc / advance | 遍历候选文档 | 候选集太大、filter 范围太宽 |
| shard 耗时差异 | 不同分片耗时 | 热点分片、数据倾斜、节点资源不均 |
如果发现某个分片明显慢,要继续看:
GET /_cat/shards/product_search?v
GET /_cat/nodes?v
GET /_nodes/hot_threads这能判断是不是某个分片太大、某个节点 CPU 高、磁盘 IO 慢,或者热点查询集中在某个 routing。
慢查询证据链
成熟排查不能只靠一句“感觉是深分页”。建议按证据链走:
| 步骤 | 要拿到的证据 | 目的 |
|---|---|---|
| 1. 找慢接口 | traceId、接口耗时、最终 DSL | 确认是不是 ES 查询慢 |
| 2. 看 slowlog | query 慢还是 fetch 慢 | 初步定位阶段 |
| 3. 看 profile | 哪个 query、filter、agg、sort 慢 | 定位 DSL 结构问题 |
| 4. 看索引信息 | mapping、settings、分片数、文档量 | 判断模型是否合适 |
| 5. 看集群资源 | CPU、GC、磁盘、线程池 rejected | 判断是不是资源瓶颈 |
| 6. 看业务参数 | 页码、时间范围、聚合维度、权限过滤 | 判断是不是接口没保护 |
flowchart TD
A["接口慢"] --> B["打印最终 DSL 和 traceId"]
B --> C["查 ES slowlog"]
C --> D{"query 慢还是 fetch 慢"}
D -- "query 慢" --> E["profile 看倒排、filter、sort、agg"]
D -- "fetch 慢" --> F["看 _source、高亮、返回字段"]
E --> G["看 mapping、分片、资源"]
F --> G
G --> H["按证据优化并压测"]Query 慢的常见根因和改法
| 根因 | 为什么慢 | 改法 |
|---|---|---|
wildcard: "*abc*" | 很难通过 term dictionary 快速定位,可能扫描大量 term | ngram、search_as_you_type、业务限制前缀 |
script_score | 每个候选都执行脚本 | 缩小候选集、预计算排序字段 |
function_score 候选太大 | 加权计算文档太多 | 先 filter 收窄,再打分 |
terms 参数过大 | 大量 term 匹配和内存开销 | 拆批、改权限模型、预计算可见范围 |
| 大范围时间查询 | 候选文档太多 | 限制时间范围、按时间索引、冷热分层 |
| 深分页 | 每分片返回 from + size | search_after、PIT、限制页数 |
| 分片过多 | 协调和查询 fan-out 成本高 | 合理分片、合并小索引 |
示例:把危险 wildcard 改成受控前缀或 ngram。
危险查询:
{
"query": {
"wildcard": {
"productName.keyword": "*耳机*"
}
}
}更稳的方向:
- 商品名用合适 analyzer 做全文检索。
- 型号、编码、拼音前缀用
edge_ngram或search_as_you_type。 - 服务端限制关键词长度和特殊字符。
- 对低价值模糊查询限流或降级。
Fetch 慢的常见根因和改法
Fetch 慢经常被忽略,因为大家更容易盯着倒排索引。但列表页返回大 _source、高亮长字段、嵌套文档 inner_hits,都可能让 Fetch 很慢。
| 根因 | 为什么慢 | 改法 |
|---|---|---|
_source 很大 | 读取、解压、网络传输都重 | 列表页只返回必要字段 |
| 高亮大字段 | 需要分析文本和生成片段 | 只高亮短字段,限制片段数 |
| 返回大数组 | 响应体大,客户端解析慢 | 拆详情接口,不在列表返回 |
| inner_hits 多 | 嵌套文档拉取成本高 | 控制 size,或调整文档模型 |
| 跨很多分片 fetch | 网络 fan-out 多 | 合理分片和 routing |
字段过滤示例:
GET /product_search/_search
{
"_source": ["id", "productName", "brandName", "price", "saleStatus"],
"query": {
"match": {
"productName": "无线耳机"
}
}
}列表页不要返回商品详情、评论、富文本、规格大数组。详情页按 ID 再查,或者从 MySQL/业务服务回源校验。
为什么加机器不一定解决慢查询
加机器只能增加资源,不会自动修复坏 DSL、坏 Mapping 和坏分页。
| 问题 | 加机器效果 | 真正要改 |
|---|---|---|
| 深分页 | 协调节点仍要合并并丢弃大量候选 | 改 search_after、限制页数 |
| wildcard 前缀星号 | 仍可能扫描大量 term | 改 analyzer 或 ngram |
_source 太大 | 网络和客户端解析仍重 | 控制返回字段 |
| Mapping 错误 | text/keyword 错用仍然错 | 重建索引 |
| 单 routing 热点 | 热点仍可能集中在单分片 | 调整 routing 或拆索引 |
| 聚合 bucket 爆炸 | 更多节点也可能被内存打爆 | 控制 bucket、预聚合 |
面试回答要有边界感:ES 优化不是“调参数和加节点”,而是从业务查询入口、Mapping、DSL、分片、资源和同步链路一起治理。
深分页为什么慢,怎么处理
from + size 深分页的成本会放大。
from = 10000
size = 20
分片数 = 5每个分片可能都要取出前 10020 条候选,协调节点合并后再丢掉前 10000 条。
flowchart TD
A["每个分片返回大量候选"] --> B["协调节点合并排序"]
B --> C["丢弃 from 之前的数据"]
C --> D["只返回 size 条"]处理方式:
| 方案 | 适合场景 |
|---|---|
| 限制最大页数 | 普通商品搜索、后台列表 |
search_after | 下一页、无限滚动 |
PIT + search_after | 翻页期间保持一致视图 |
| Scroll | 大批量导出或重建,不适合用户实时翻页 |
| 业务游标 | 用时间、ID、排序字段做游标 |
search_after 示例:
GET /product_search/_search
{
"size": 20,
"query": {
"match": {
"productName": "无线耳机"
}
},
"sort": [
{ "saleCount": "desc" },
{ "id": "desc" }
],
"search_after": [5821, 10001]
}排序字段必须稳定,最后通常加唯一 ID 兜底。
聚合慢怎么处理
聚合慢常见于:范围太大、bucket 太多、字段类型不合适、分片太多。
| 问题 | 处理 |
|---|---|
对 text 聚合 | 改用 keyword 子字段 |
| 时间范围太大 | 限制时间范围或用预聚合 |
| bucket 太多 | 控制 size,避免超大 terms |
| 嵌套聚合太深 | 拆查询或预计算 |
| 每次都实时统计 | 缓存热门条件或离线统计 |
商品筛选项常见聚合:
GET /product_search/_search
{
"size": 0,
"query": {
"match": {
"productName": "耳机"
}
},
"aggs": {
"brands": {
"terms": {
"field": "brandName",
"size": 20
}
}
}
}size: 0 表示只要聚合结果,不返回文档列表。
写入慢怎么排查
flowchart TD
A["写入慢"] --> B["看 bulk 大小和失败率"]
B --> C["看 refresh_interval"]
C --> D["看副本数"]
D --> E["看 merge 压力"]
E --> F["看 thread_pool rejected"]
F --> G["看磁盘 IO 和 JVM GC"]常见原因:
| 原因 | 现象 | 处理 |
|---|---|---|
| 单条写太多 | 网络往返多、吞吐低 | 使用 Bulk |
| Bulk 太大 | 内存高、失败重试代价大 | 压测合适批次 |
| refresh 太频繁 | 小 segment 多、写入抖动 | 批量导入时调大 refresh |
| 副本多 | 每次写要复制更多副本 | 导入时可临时降低副本 |
| 文档太大 | 网络和解析成本高 | 控制 _source 大小,拆字段 |
| merge 压力大 | IO 高、查询也变慢 | 控制写入速率,观察 merge |
| 磁盘慢或水位高 | 写入、查询都慢 | 清理、扩容、冷热分层 |
Bulk 怎么用才合理
Bulk 不是越大越好。
POST /_bulk
{ "index": { "_index": "product_search", "_id": "10001" } }
{ "id": 10001, "productName": "无线耳机" }
{ "index": { "_index": "product_search", "_id": "10002" } }
{ "id": 10002, "productName": "机械键盘" }建议:
- 按文档大小压测,不只按条数。
- 控制并发,避免 thread pool rejected。
- 失败要按单条 item 处理,不要只看 HTTP 状态。
- 大批量导入时调大 refresh,必要时临时降低副本。
- 写入服务要有限流和重试退避。
集群 yellow / red 怎么排查
flowchart TD
A["集群状态异常"] --> B["GET _cluster/health"]
B --> C{"状态"}
C -- "yellow" --> D["副本未分配"]
C -- "red" --> E["主分片不可用"]
D --> F["GET _cat/shards"]
E --> F
F --> G["GET _cluster/allocation/explain"]
G --> H["处理节点、磁盘、水位、分配规则"]常见原因:
| 状态 | 常见原因 | 处理 |
|---|---|---|
| yellow | 单节点设置了副本、副本无处放 | 开发环境副本设 0,生产增加节点 |
| yellow | 磁盘水位高,副本不能分配 | 清理索引、扩容磁盘 |
| red | 主分片所在节点异常 | 恢复节点,检查快照 |
| red | 磁盘或索引损坏 | 评估恢复、快照、重建 |
| 只读 | flood stage 水位触发 | 清理磁盘后解除只读 |
解除只读示例:
PUT /*/_settings
{
"index.blocks.read_only_allow_delete": null
}前提是磁盘水位已经降下来,否则很快又会触发。
JVM 和内存问题
ES 很依赖 JVM 堆内存,但不是堆越大越好。堆过大可能影响压缩指针和 GC,文件系统缓存也会减少。
常见原则:
- 给 ES 足够文件系统缓存,因为 Lucene 查询依赖 OS cache。
- 堆内存不要把机器内存吃满。
- 关注 old GC、heap used、circuit breaker。
- 大聚合、脚本、字段数据加载可能触发内存压力。
排查命令:
GET /_nodes/stats/jvm,breaker,thread_pool
GET /_nodes/hot_threads热点分片
热点分片是指某些分片承担了远高于其他分片的写入或查询压力。
常见原因:
- routing 设计不合理,例如所有大客户数据落到一个 routing。
- 时间索引中当天索引写入太集中。
- 某个租户、店铺、商品特别热门。
- 分片大小差距过大。
排查:
GET /_cat/shards/product_search?v
GET /_cat/thread_pool/search?v
GET /_nodes/hot_threads处理:
- 调整 routing 策略。
- 拆分热点业务索引。
- 增加分片或重建索引,但要评估长期成本。
- 对热点查询做缓存或预计算。
冷热数据和生命周期
日志、审计、采集明细这类数据通常按时间增长,不应该永久占用热节点。
flowchart TD
A["热数据:最近 7 天"] --> B["热节点,查询和写入频繁"]
B --> C["温数据:最近 30 天"]
C --> D["冷数据:历史归档"]
D --> E["过期删除或快照归档"]商业建议:
- 日志索引用 ILM 管理生命周期。
- 热数据保证查询性能。
- 冷数据降低副本和存储成本。
- 过期数据及时删除或快照。
- 不要让日志无限增长挤爆商品搜索集群。
搜索接口保护
不要把 ES DSL 直接暴露给前端或外部系统。
风险:
- 用户可以构造深分页。
- 用户可以发起大聚合。
- 用户可以使用 wildcard 或脚本拖垮集群。
- 用户可能绕过权限过滤。
正确做法:
flowchart TD
A["前端业务参数"] --> B["搜索服务校验"]
B --> C["权限和租户过滤"]
C --> D["翻译成受控 DSL"]
D --> E["限制分页、聚合、排序"]
E --> F["查询 ES"]生产排查清单
| 问题 | 优先看什么 |
|---|---|
| 搜不到 | 主库、ES 文档、同步日志、Mapping、分词、filter |
| 搜不准 | _analyze、_explain、boost、同义词、业务排序 |
| 查询慢 | slowlog、profile、深分页、大聚合、wildcard、脚本 |
| 写入慢 | bulk、refresh、副本、merge、thread pool、磁盘 |
| 集群 yellow | 副本、节点数、磁盘水位、allocation explain |
| 集群 red | 主分片、节点故障、磁盘、快照恢复 |
| 磁盘只读 | flood stage、水位、过期索引、ILM |
| JVM 抖动 | heap、GC、breaker、大聚合、fielddata |
| 同步延迟 | MQ Lag、CDC 延迟、消费者耗时、ES 写入失败 |
关联知识点
| 知识点 | 继续学习什么 |
|---|---|
| 底层原理 | refresh、segment、query/fetch、BM25 |
| 同步与重建索引 | MQ/CDC 同步、别名切换、补偿 |
| Mapping 与查询 | 字段类型、Query DSL、分页、聚合 |
| 集群 | 节点、分片、副本、磁盘水位 |
| 消息堆积与背压 | ES 同步链路堆积排查 |
小结
ES 排障要分层:先确认是结果问题、性能问题还是集群问题;结果问题看同步、Mapping、分词和 DSL;性能问题看慢日志、profile、深分页、聚合、写入和资源;集群问题看 health、shards、allocation explain、磁盘水位和 JVM。成熟的搜索系统还要用服务端保护 DSL、限制深分页、设计补偿任务、监控同步延迟和集群健康。
