Elasticsearch 搜索变慢怎么优化?从定位到调优的排查顺序
Elasticsearch 搜索变慢时,最忌讳直接改 refresh_interval 或加节点。
先确认慢在查询解析、数据读取还是结果合并,再决定动查询、动索引还是动集群。
下面按“先取证、后动手”的顺序给出可执行的排查和优化方法,适用于自建或云上托管的 Elasticsearch 7.x/8.x,单集群数据量在百万到亿级文档的常见场景。
先拿到证据:慢日志和 profile 比猜更可靠
搜索慢的第一反应应该是打开慢查询日志,而不是凭感觉调参数。
在 elasticsearch.yml 中确认慢日志阈值,或在索引级别动态设置:
PUT /my_index/_settings
{
"index.search.slowlog.threshold.query.warn": "2s",
"index.search.slowlog.threshold.query.info": "1s",
"index.search.slowlog.threshold.fetch.warn": "1s"
}
日志路径通常在 $ES_HOME/logs/<集群名>_index_search_slowlog.log。
查看日志时重点看 took、query 和 fetch 两段耗时占比:query 阶段慢说明查询条件重,fetch 阶段慢说明返回字段多或文档大。
拿到具体慢查询后,用 profile API 拆解耗时:
GET /my_index/_search
{
"profile": true,
"query": { "match": { "title": "优化" } }
}
返回结果里看 time_in_nanos 最大的节点,通常是 advance、next_doc 或 score。
如果大量时间花在 score,考虑是否需要算分;
如果花在 advance,检查是否有高基数字段参与范围查询。
查询层:先改写法,再考虑加资源
很多搜索慢是查询写法造成的,改查询的成本远低于扩容。
- 避免
wildcard前后都带通配符,例如*关键词*,这类查询无法利用倒排索引,数据量大时必然慢。需要前缀匹配时用prefix或edge_ngram分词器。 - 分页不要用
from + size翻到很深的位置。超过index.max_result_window(默认 10000)时改用search_after,或者用scroll做批量导出。 - 只返回需要的字段,用
_source过滤:
GET /my_index/_search
{
"_source": ["title", "price"],
"query": { "term": { "status": "on" } },
"size": 20
}
- 确认查询是否命中缓存。对重复的过滤条件,
request_cache可以显著降低耗时,但只对size: 0的聚合或排序结果稳定的查询有效。验证方式是在 profile 结果中看query_cache命中情况。
如果查询本身已经精简,仍然慢,再进入索引和集群层。
索引与分片:段合并和分片数量是两个高频问题
段(segment)是 Lucene 的存储单位,小段过多会让查询需要遍历更多文件。
查看段数量:
GET /my_index/_segments
如果单个分片的段数量达到几十甚至上百,可以手动触发强制合并:
POST /my_index/_forcemerge?max_num_segments=1
注意 _forcemerge 会消耗大量 IO 和 CPU,只在低峰期执行,且对持续写入的索引不要频繁做。
合并后再次查看 _segments,段数应明显下降。
分片数量也要回头看。
分片不是越多越好,每个分片都有固定开销。
经验上单个分片大小控制在 10GB 到 50GB 之间,搜索场景分片数不宜过多。
查看分片大小:
GET _cat/shards/my_index?v&h=index,shard,prirep,store,docs
如果大量分片只有几百 MB,搜索时要 fan-out 到很多分片再合并结果,延迟会上升。
此时应考虑重建索引并减少分片数,而不是在旧索引上继续加节点。
资源层:JVM 堆、文件系统缓存和线程池
JVM 堆内存设置不当会直接拖慢搜索。
堆内存建议不超过物理内存的 50%,且不要超过 31GB(压缩指针边界)。
查看当前堆使用:
GET _nodes/stats/jvm
如果 heap_used_percent 长期高于 75%,且伴随频繁 GC,需要扩容或减少缓存占用。
注意 Elasticsearch 依赖文件系统缓存来加速段读取,不要把机器内存全部分给 JVM 堆。
线程池也是排查点。
搜索请求进入 search 线程池,队列满时请求会排队。
查看线程池状态:
GET _nodes/stats/thread_pool
如果 search 的 queue 持续大于 0 且 rejected 增长,说明并发搜索量超过处理能力,优先优化查询或增加节点,而不是单纯调大队列。
验证优化是否生效
每次调整后都要用同一组慢查询复测,对比 took 和 profile 中的耗时分布。
建议记录调整前后的慢日志条数、P95 延迟和段数量三个指标:
- 慢日志条数:调整后同一时间段应下降。
- P95 延迟:从监控或
_nodes/stats的搜索耗时统计中取。 - 段数量:
_forcemerge后应下降,且不再快速反弹。
如果调整后指标没有变化,说明瓶颈不在这一层,回到 profile 结果重新定位,不要继续叠加参数。
两个容易被忽略的边界
第一,refresh_interval 调大能降低写入压力,但对搜索延迟的改善有限,且会让新数据可见时间变长,不适合对实时性要求高的业务。
第二,增加副本能提升搜索吞吐,但会占用更多磁盘和写入带宽,搜索慢如果是单次查询耗时长,加副本并不能解决。
搜索慢的优化顺序可以概括为:先看慢日志和 profile 确认瓶颈,再改查询写法,然后检查段合并和分片设计,最后才动 JVM 和节点资源。
每一步都用同一组查询验证,避免把多个变量混在一起。