如何优化 Elasticsearch 的查询性能(Search Performance)?
优化 Elasticsearch (ES) 的查询性能是一个系统工程,通常需要从查询语句编写、索引设计、集群配置以及硬件资源四个维度入手。
以下是详细的优化指南:
一、 查询语句层面的优化 (Query DSL)
这是成本最低、见效最快的方法。
多用 Filter Context,少用 Query Context
- 原理:
filter子句不计算相关性评分(Score),且结果会被节点缓存(Node Query Cache)。query子句需要计算评分,无法缓存。 - 做法:对于不需要评分的过滤条件(如状态、时间范围、ID),务必放入
bool查询的filter中。 - 示例:json
GET /_search { "query": { "bool": { "must": { "match": { "title": "search" }}, "filter": { "term": { "status": "active" }} <-- 放在这里 } } }
- 原理:
避免使用高昂的查询操作
- 避免前缀通配符:如
*keyword。这会导致扫描所有倒排索引。尽量使用keyword*(前缀匹配)。 - 慎用 Regexp 和 Wildcard:正则和通配符查询非常消耗 CPU。
- 少用 Script:脚本查询(Painless)通常不能利用索引结构,需要遍历文档。如果必须用,优先使用
stored script并传入参数,或者在写入时预处理数据。
- 避免前缀通配符:如
限制返回字段 (
_sourcefiltering)- 如果不需要文档的全部字段,使用
_source参数指定返回字段,减少网络传输和反序列化开销。
- 如果不需要文档的全部字段,使用
日期舍入 (Date Rounding)
- 在使用时间范围查询时,使用
now/m而不是now。 - 原因:
now是毫秒级变化的,会导致缓存失效。now/m(按分钟取整) 可以让缓存生效一分钟。
- 在使用时间范围查询时,使用
避免深度分页 (Deep Paging)
from + size超过 10,000 条数据时性能急剧下降(因为需要排序和截断)。- 优化:使用
search_afterAPI 进行滚动查询,或者使用 Scroll API(仅用于导出数据,不用于实时查询)。
二、 索引与建模层面的优化 (Index & Mapping)
在数据写入前设计好 Schema 是性能的关键。
选择合适的数据类型
- Keyword vs Text:不需要分词搜索的字段(如 ID、枚举、标签),务必设为
keyword。text字段会分词,占用更多空间且聚合慢。 - 数值类型:够用就好。能用
integer别用long,能用float别用double。 - 禁用不需要的特性:
- 如果不搜索该字段:
index: false - 如果不聚合/排序该字段:
doc_values: false - 如果不算分:
norms: false(对text字段节省大量内存)
- 如果不搜索该字段:
- Keyword vs Text:不需要分词搜索的字段(如 ID、枚举、标签),务必设为
使用
copy_to聚合字段- 如果经常需要跨多个字段搜索(如同时搜
first_name和last_name),在 Mapping 中使用copy_to将它们复制到一个full_name字段,然后只搜这一个字段,比multi_match更快。
- 如果经常需要跨多个字段搜索(如同时搜
预索引数据 (Pre-indexing)
- 以空间换时间。不要在查询时通过脚本计算(如
doc['price'].value * doc['quantity'].value)。 - 在写入数据时,直接算好存入一个新字段
total_amount。
- 以空间换时间。不要在查询时通过脚本计算(如
分片 (Shards) 与 副本 (Replicas) 策略
- 分片大小:单个分片建议在 10GB - 50GB 之间。分片过多会导致元数据管理开销大,查询时这就变成了 Map-Reduce 的负担。
- 副本数量:增加
number_of_replicas可以直接提升读取吞吐量(Read Throughput),因为查询可以在主分片或副本上并行执行。
强制合并 (Force Merge)
- 对于只读索引(如按天生成的日志索引),执行
_forcemerge将 Segment 合并为 1 个。这能极大减少文件句柄打开数和搜索时的 Segment 扫描数。
- 对于只读索引(如按天生成的日志索引),执行
自定义路由 (Custom Routing)
- 如果你的查询总是基于某个 ID(如 UserID),写入时指定
routing=UserID。 - 查询时也带上
routing=UserID,ES 就只需要去这一个分片查,而不是广播到所有分片,性能提升数倍。
- 如果你的查询总是基于某个 ID(如 UserID),写入时指定
三、 聚合查询优化 (Aggregations)
- 使用
keyword字段聚合:永远不要对text字段开启fielddata: true进行聚合,这会撑爆堆内存。 - Breadth_first 模式:对于基数(Cardinality)很大但只需要返回 Top N 的聚合(如嵌套聚合),设置
"collect_mode": "breadth_first"。 - Sampler Aggregation:如果数据量极大,可以使用
sampler聚合先取样,再在样本上做聚合,牺牲少量精度换取极高的速度。 - Composite Aggregation:如果需要导出全部聚合结果,使用
composite聚合代替terms聚合的分页。
四、 集群与硬件配置 (Infrastructure)
文件系统缓存 (Filesystem Cache) 是核心
- Elasticsearch 严重依赖操作系统的文件系统缓存(Lucene 索引文件就在这里)。
- 黄金法则:不要把所有内存都分给 JVM Heap。建议配置 Heap Size 为物理内存的 50%,且不超过 32GB(为了利用 Compressed OOPs)。剩下的 50% 留给操作系统做缓存。
使用 SSD
- 机械硬盘(HDD)是 ES 性能的杀手。必须使用 NVMe 或高性能 SSD。
- 如果是云盘,注意 IOPS 和吞吐量限制。
Swap 必须关闭
- 内存交换会导致 GC 停顿时间剧增,甚至导致节点无响应。在
elasticsearch.yml中配置bootstrap.memory_lock: true。
- 内存交换会导致 GC 停顿时间剧增,甚至导致节点无响应。在
Refresh Interval
- 默认
refresh_interval是 1s。这意味着写入 1 秒后才能被搜到。 - 如果业务允许,将其调大(如 30s)或在批量导入时临时设为
-1。这能减少 Segment 的生成,降低系统负载,间接提升搜索性能。
- 默认
五、 排查与诊断工具
如果不知道慢在哪里,优化就是盲人摸象。
Profile API
- 在查询中添加
"profile": true,ES 会返回详细的执行计划,告诉你哪个分片、哪个查询子句、哪个阶段(Lucene 层面)耗时最长。
- 在查询中添加
Slow Log (慢查询日志)
- 配置
index.search.slowlog.threshold。例如记录超过 500ms 的查询,分析日志中的查询语句。
- 配置
Hot Threads
- 使用
GET /_nodes/hot_threads查看当前节点 CPU 都在干什么(是在做正则匹配?还是在做 JSON 解析?)。
- 使用
总结 Checklist
- 查询:用了 Filter 吗?去掉了前缀通配符吗?
- Schema:字段类型对了吗?不需要的
doc_values关了吗? - 内存:Heap 设为 50% 且 < 32GB 了吗?Swap 关了吗?
- 分片:分片大小在 30GB 左右吗?副本数够吗?
- 缓存:利用好 OS Cache 了吗?