Elasticsearch 8.x 性能调优,我的实战复盘
前阵子接手了一个 Elasticsearch 8.x 集群,业务方反馈:“写入偶尔会堵,查询有时候卡到一两秒。”注意,这里是“偶尔会堵”,不是“一直很慢”,也不是“某个查询很慢”——这种“薛定谔的慢”最折磨人,因为你不是在调一个稳定的问题,而是在追一个间歇性故障。在 Elasticsearch 8.x 这个版本上,性能问题的表象和根因关系越来越隐蔽,比如 8.x 默认的并发配置、新的搜索优化器、甚至 JDK 版本变化,都可能导致你按照老版本经验去调优时“调了个寂寞”。
这篇文章我就围绕 Elasticsearch 8.x 的性能调优,把从磁盘判断、索引规划、查询改写到监控体系的关键内容,结合我这次实战的经验,完整梳理一遍。适合正在维护 ES 集群的运维/后端同学,以及那些被“ES 慢”三个字折磨过的开发同行。
1. 调优前先搞明白:ES 为什么变慢
很多人在调优开始前就犯了一个错:直接去改参数。改 heap、改 bulk size、改线程池,一顿操作猛如虎,回头一问“为什么慢?”答不上来。
我习惯先把“慢”拆成两条路径:写入路径慢和查询路径慢。这两者的瓶颈点、排查指标、调优手段几乎不重叠。
写入路径的本质是:文档从客户端到索引段,中间经历 routing、analyzer、memory buffer、translog、refresh、segment merge 这一整套流水线。查询路径的本质是:查询 DSL 被解析、重写、分片路由、Lucene 执行、结果归并。ES 之所以成为 ES,不是因为单机快,而是因为把这两个路径分布式化了;但分布式也意味着一句话:“全集群的性能,取决于最慢的那个分片。”
1.1 五个层级的慢,你是哪一层
我把 ES 性能问题按经验分成五个层级,排查时逐层下钻:
- 系统层:CPU 核数、内存带宽、磁盘 IOPS、网络吞吐。这一层的问题最容易被忽略,因为大多数人只看监控大盘上的 CPU 百分比。
- 节点层:进程 GC 频率、线程池队列堆积、节点间网络链路。节点层的问题往往表现为“某一个节点慢,拖垮所有分片”。
- 索引层:分片数、分片大小、segment 合并策略、refresh/translog 配置。索引层的配置是“一劳永逸”的,但也是出错后最痛苦的——因为重建索引往往比调参痛苦得多。
- 查询/写入层:DSL 写法、bulk 批大小、analyzer 成本。这一层调起来最快,见效也最快,但需要写代码的人配合。
- 客户端层:连接池配置、重试语义、数据源返回超时设定。这一层问题常被当成 ES 的问题甩锅,结果调了半天 ES,其实是客户端线程池跑满了。
这五层之间往往互相影响。比如磁盘慢会导致 segment merge 跟不上,merge 跟不上会导致 segment 数量暴涨,segment 暴涨会导致查询多路并发扫描,最终查询也慢了。ES 的问题很少是孤立的,但基本都可以从“某一层最可疑的根因”开始切入。
1.2 厨房类比:你觉得的“慢”是谁的锅
打个比方,整个过程像一家后厨。客人点菜(查询请求)进来了,后厨三个灶位,传菜员和厨师在同一个空间里挤来挤去。如果客人只是感觉“上菜慢了”,你得先弄清楚是点菜单子(DSL)太复杂、厨师手速慢(CPU 跑慢)还是灶台(磁盘)火力不够。
如果只盯着“菜谱”改进(查询优化),但实际是灶台(磁盘 IO)的问题,那怎么改菜谱都没用。反过来,如果灶台很好,但每桌都点 20 道菜的豪华套餐(大分页 + 高基数聚合),再怎么换锅也不行。
这也是我给团队定的一个工作习惯:任何调优开始前,必须先用监控证据回答“慢在哪一层”,再谈方案。
2. 判断写入慢:核心指标组合与磁盘根因定位
回到文章开头说的“写入偶尔堵”。要判断写入慢到底是磁盘问题还是索引设计问题,不能只靠感觉,要靠指标组合。
2.1 先看这组指标组合
- Bulk 请求耗时分布:p50、p90、p99。如果 p99 明显高于 p50 一个数量级,基本可以确定有间歇性阻塞。
- Threadpool write queue:如果 write 队列持续有积压,说明磁盘或内存已经跟不上了。
- refresh 耗时:refresh 是 ES 把 memory buffer 变成可检索 segment 的过程,这个过程是 Lucene 内部执行的,不受外部配置完全控制。
- segment merge 速率与耗时:merge 是非常消耗磁盘 IO 的,大 segment 合并期间很容易出现写入抖动。
- 磁盘 I/O 指标:await、%util、r/s、w/s,以及 ES 自己的 indexing pressure 统计。
这里必须强调一点:单看 disk %util 已经过时了,尤其是在 SSD 上。SSD 的 %util 常年 100% 但延迟只有几毫秒很常见,百分比高不代表有问题。重点看 await(平均 I/O 请求处理时间)和 svctm 的差值,以及 io 在队列里的滞留时间。如果 await 超过 20ms(机械盘)、10ms(SATA SSD)、2-5ms(NVMe)那就要警惕了。
2.2 磁盘问题 vs 索引问题的三条证据
怎么用证据链区分?
- 看 merge 指标:如果索引经常处于
merging状态,且 merge 占用了大量 IO,那多半是索引规划有问题(分片过多、segment 未合理控制、映射中字段太多导致 merge 成本高)。这个问题的特点是:写入慢是有规律的重负载,而不是随机抖动。 - 看 refresh 和 translog 的占比:如果 refresh 本身耗时高、translog flush 频繁阻塞,更可能是磁盘性能瓶颈。因为 refresh 和 translog flush 都属于高频小 IO,磁盘响应一慢,第一个扛不住的就是它们。
- 看 JVM GC:如果写入慢的时间窗口正好伴随 Old GC 频繁,那可能是堆内存/对象分配问题,而不是磁盘。很多人容易忽略 object 分配——在写入高并发场景,ES 的 BulkRequest 如果被反复拷贝、序列化,GC 会被打到飞起。
2.3 实操:用 api 快速看一眼节点状态
排查时,我的固定动作是先敲几个 api,形成“现场快照”:
# 1. 节点健康与资源概览 GET /_nodes/stats/os,process,jvm,fs,thread_pool # 2. 看索引层面的 segment/merge 情况 GET /_cat/indices?v&s=segments:desc # 3. 看线程池积压 GET /_cat/thread_pool/write,search?v&h=node_name,name,active,queue,rejected,completed # 4. 看压测/链路中最耗时的分片分布 GET /_cat/shards?v&s=store:desc这一步之后,基本能锚定问题层级。注意rejected数量别只关心是否为 0——有些 8.x 版本中,即使队列没满,但等待时间很长,也会造成写入超时,所以 queue 的滞留时间比 queue 长度更有参考意义。
2.4 与 MySQL 调优思路的对照
热词里频繁出现“mysql性能调优”,很多人其实是带着 MySQL 惯性来理解 ES。这里必须同步一个认知:ES 不是 MySQL 的替代品,它的调优逻辑和 MySQL 根本是两套剧本。
MySQL 调优通常是“表结构 + 索引 + 慢查询”三件套,因为 MySQL 的数据模型是行存储、有事务边界、有主键约束。ES 是倒排索引 + 列存 doc values,没有事务(8.x 虽然引入了部分特性,但面向场景不同)、没有 join 优化(在绝大多数场景都不建议用 join)。所以用 MySQL 的思维去调 ES,最常见的错误是:
- MySQL 里习惯“索引加越多越好”,ES 里是keywords 字段、text 字段、doc_values、fielddata 每个都要花钱,字段越少越省,特别是磁盘和内存。
- MySQL 里 join 查询再慢也就是几表 join,ES 里如果每个 query 带多个
must/should+ 高基数聚合,分分钟把 CPU 打满。 - MySQL 的慢查询日志很容易看,ES 的慢查询 log 需要开启 slowlog 阈值,而且查询慢不等于写入慢,二者要分开开。
理解了这套差异,你再看 ES 调优,就有了方向感:ES 的优化大部分是在“少做不必要的事”:少存字段、少分词、少聚合、少深分页。
3. Elasticsearch 8.x 查询调优的核心实操
查询慢,是最常见也最容易被“语文题”化的部分。下面是我基于 8.x 实践整理的几个高频优化动作。
3.1 用 Profile API 找到真正的“费电大户”
ES 慢查询,先不要凭感觉改。开启 profile:
{ "profile": true, "query": { "bool": { "filter": [ { "term": { "status": "active" } } ], "must": [ { "match": { "title": "elasticsearch 性能调优" } } ] } } }返回结果中重点看time_in_nanos和breakdown。我见过最典型的案例:一个看似简单的 term 查询,在advance阶段花了 90% 的时间,最后发现是因为没有走 cache,导致每次查询都要遍历整个 segment 的 term dict。
8.x 里尤其要留意match_phrase和wildcard这类查询。match_phrase在高频词场景下,overall 执行非常耗 CPU;wildcard更是经典性能杀手,如果业务允许,尽量改成ngram或edge_ngram索引。
3.2 filter context 与 query context 的差异要刻进本能
ES 查询分两种 context:query context需要计算相关度分数,filter context只要匹配和缓存,MUST 的 term 查询其实是 filter context。很多人写完查询,发现慢,根本原因是——该用 filter 的地方用了 must,甚至 match 而不用 term。
from ES 8.x 的实践来看,能放进 filter 的尽量放 filter,好处是:
- 不需要计算 score,省 CPU。
- 可以被节点级 request cache 缓存(注意:filter context 的查询结果集如果超过 10000 个,也不能被 cache,所以 filter 最好加 limit)。
- 对
bool中的should组合,filter 可以用minimum_should_match精确控制。
这句话几乎每次培训都会说,但真正把每个已存在查询都改成 filter 的团队,十个里不超过两个——大部分是“能用就行,慢再说”。慢再说的结果就是,最后在业务高峰期加机器、加节点,成本翻倍。
3.3 分页与深分页:别再让 from + size 背锅
来自热词里的一个高频痛点:ES 深分页慢。8.x 的默认 max_result_window 是 10000,超过这个还查,直接报错。很多开发者的第一反应是改index.max_result_window,把它调大,比如到 1000000。
我从实战角度建议:不要调,除非你清楚代价。深分页慢的本质是每个分片都要把全部命中文档堆到协调节点做全局排序,from 越大,需要丢弃的数据越多。消耗的时间和内存呈线性上涨,不病则已,一病就是集群瘫痪。
正确做法:
- 翻页场景:改用
search_after,依靠排序值游标翻页,不依赖全局深度。 - 导出场景:用
scroll(注意 8.x 里 scroll 仍在,不推荐长存活,能用 PIT 更好),PIT 是 8.x 推荐的方案,支持增量快照,避免 scroll 上下文堆积。 - 业务确实需要跳页:如果用户要看第 1000 页,很多时候是伪需求,产品上做一个“前 1000 页 + 后续滚动”的方案即可。
3.4 聚合与排序:分布式的排序没有“免费午餐”
聚合慢在 ES 里很常见,尤其是 big 聚合 +terms在高基数场景下的 bucket 排序。很多开发喜欢一次聚合直接给全量 bucket,其实完全可以加size和shard_size限制。ES 8.x 的 composite aggregation 也适合处理超大分页式聚合,它不像 terms 那样一次性把 bucket 全部算完,而是按批次取,适合离线或大报表场景。
排序最贵的不是数字排序,而是对 text 字段排序。text 字段默认没有 doc_values,你要排序就必须启用 fielddata;fielddata 一来,堆内存就炸。实测中,一个高流量的查询如果对 text 排序,堆内存的上涨肉眼可见。所以如果你知道某个字段要排序,索引时就给 mapped 成 keyword 或有 doc_values 的字段;如果已经上线了,宁可数据重构重建索引,也不要用 fielddata。
4. 参数级调优:堆、线程池、刷新间隔与并发策略
下面谈到的参数级调整,最容易被“照着网文抄”误导。我写一些我实测的体会和原则,比参数本身更重要的是理解原则,否则你换个版本、换个业务场景,同样的参数可能变成负优化。
4.1 堆大小:不是越大越好
ES 官方建议堆大小不超过物理内存的 50%,同时不超过 32GB(因为 JVM 指针压缩)。我看到有人把 128GB 内存的机器分配了 64GB 给 ES heap,结果操作系统 page cache 只剩一点点,所有读取都直接落到磁盘,查询反而比 32GB heap + 60GB page cache 的机器慢得多。
我常用的经验值是:单机总内存的 1/3 到 1/2 给 heap,余下的留给 OS page cache。ES 的 Lucene 极度依赖 OS 缓存,热数据如果能被 page cache 兜住,大部分查询根本不碰磁盘。8.x 里还有一些对堆内存的优化,比如用堆外内存做某些数据结构的缓存,但在绝大多数场景下,给 OS 留足够余地仍然是最优解。
4.2 bulk size:不要迷信网上说的“1MB 或 1000 条”
bulk size 是个常见坑。网上一搜就是“单批不要超过 10MB/1000 条”,这话对也不对。bulk size 最合理的判断标准是每批数据的耗时曲线。我是这么测的:同一份测试数据,从 1MB 起步,每批翻倍,记录 bulk 请求的 p50/p99 耗时以及 rejected 情况,画出曲线。
通常你会看到一条“U 型曲线”——批太小,网络程序和请求处理开销比大;批太大,单次请求内存分配和 GC 压力上升。U 型谷底就是适合你当前集群和数据的 bulk size。我用这个方法测过的集群,实际最优值往往是网上建议值的 2-3 倍。
另外,bulk 的并发度比单批大小重要得多。很多时候写入慢不是批太小,而是并发不够。每个节点写线程池默认是固定核数的,但你客户端如果只开 1 个连接,每秒能发的请求数就有上限。8.x 的 HTTP 客户端默认连接池大小,最好根据节点数和写入量一起合理配置——这一块配合 bulk size 调整,写入 p99 能降一半以上。
4.3 refresh_interval:默认 1s 适合日志,未必适合你
ES 默认每秒 refresh 一次,也就是你写入的数据最多 1s 后就能被搜到。这个“近实时”的设计对大多数业务很合适,但对高吞吐写入场景就是灾难:每秒一次 refresh 意味着每秒都要把 memory buffer 里的数据刷成新的 segment,小 segment 大量产生,merge 压力攀升。
如果你的业务能接受 5s 或 30s 的可见性延迟(比如日志系统、指标分析系统),把 refresh_interval 调到 30s 甚至更久,能在不牺牲写入速度的前提下显著降低 merge 和磁盘 IO。结合上文的磁盘判断,你会发现很多“写入慢”其实是被默认的 1s refresh 压出来的。
4.4 translog:非要每 5 秒落盘一次吗?
translog 的 fsync 频率直接影响写入延迟。ES 默认是每个请求都要 fsync(durability=request),如果换成async模式,每 5 秒批量 fsync 一次,写入吞吐能大幅提升。代价是如果节点崩溃,会有最多 5 秒的数据丢失。
这个取舍要跟业务方聊清楚。对日志、指标、非核心数据,果断开 async;对交易、核心业务数据,别动这个,数据可靠性比性能重要。我的原则是:性能优化不能以悄悄丢数据为代价,如果开 async,部门内必须同步确认过。
4.5 线程池:关注队列而不是大小
ES 的线程池大小是由 CPU 核数决定的,不要试图手动改大,因为线程太多会造成 CPU 上下文切换开销。真正该关注的是队列。如果队列经常积压,说明线程池处理不过来了,你需要的是“减少请求量”或者“提升单请求处理效率”,而不是扩大队列。
8.x 里可以看_cat/thread_pool的active和queue字段。如果你发现 write 队列的 queue 值持续大于 0,且不同节点之间差异很大,通常是数据分布不均或者慢节点拖后腿。这种场景下,调线程池没用,要查看分片分布和磁盘性能是否均衡。
5. 监控体系与慢日志:没有监控,调优等于盲人摸象
调优这件事,如果没有数据支撑,就是在赌博。我常年依赖几组监控指标,这里一并列出来。
5.1 必看的节点层指标
- JVM 老年代 GC 次数与耗时:老年代 GC 频繁,堆分配压力大;GC 卡顿时间若超过几百毫秒,会直接影响查询 p99。
- CPU 使用率(含 iowait):iowait 高基本指向磁盘;用户态 CPU 高,大概率是查询或 merge。
- 磁盘吞吐与延迟:除了 iostat,ES 自身的
_nodes/stats/fs也提供了 io 统计,可以结合看。 - 网络流量与丢包:容易被忽视,但节点间数据迁移或者大量分片复制时,网络延迟高会拖垮整个查询链路。
5.2 慢日志开启与解读
ES 支持三套 slowlog:查询慢日志、fetch 慢日志、索引慢日志。配置的时候建议分级设置,比如:
{ "index.search.slowlog.threshold.query.warn": "5s", "index.search.slowlog.threshold.query.info": "2s", "index.search.slowlog.threshold.fetch.warn": "3s", "index.indexing.slowlog.threshold.index.warn": "5s", "index.indexing.slowlog.threshold.index.info": "2s", "index.indexing.slowlog.source": "1000" }慢日志的价值不仅在于抓到慢请求,更在于你可以通过慢日志里记录的分片数、命中文档数、重写后 query 等内容,判断慢是查询本身太复杂,还是数据量太大。很多调优的突破口,就是通过慢日志发现“一条看似简单的查询,重写后变成了几百个 term 的组合”。
5.3 Kubernetes 部署下的监控注意点
热词里有“kubesphere部署elasticsearch”,这里多说一句。容器化部署 ES 8.x,最大的坑是resources 限制与 JVM 堆大小不匹配。比如 Pod 的 memory limit 设成 16GB,JVM 堆用 12GB,那 OS page cache 只有 4GB,文件系统缓存会频繁 miss,查询大概率变慢。
另一个坑在 CPU limit:如果你设置了 CPU 配额,ES 启动时会去读/sys/fs/cgroup判断可用核数,但有些环境配置不当,会导致 ES 拿到错误核数,线程池大小跑偏。部署在 Kubernetes 的团队,注意在jvm.options里明确-XX:ActiveProcessorCount,让 JVM 正确感知容器 CPU 配额。
6. 常见问题速查与典型案例复盘
整理几个我在实际群里被问到最多的问题,全部基于真实经验,非文档搬运。
6.1 常见问题速查表
| 现象 | 优先排查指标 | 常见根因 | 解决方向 |
|---|---|---|---|
| 写入慢,p99 飙高 | write 线程池 queue、磁盘 await、refresh 耗时 | 磁盘 IO 不够、refresh 间隔过短、bulk size 太小 | 调整 refresh_interval、增大 bulk 并发/批次、检查磁盘类型 |
| 查询慢,CPU 飚满 | search threadpool、profile api | 深分页、通配符查询、高基数聚合、text 字段排序 | 改 search_after、避免 wildcard、控制聚合 size/shared_size |
| 堆内存持续上涨 | JVM old gen、fielddata | fielddata 过大、缓存未命中、segment 过多 | 禁用 fielddata、优化 mapping、规划 segment merge |
| 节点间响应差异大 | shard 分布、节点磁盘 io | 分片不均、磁盘故障、热分片 | rebalance 分片、换盘、调整 routing |
| 集群 yellow/red 但查询不报错 | cluster health、allocation explain | 主分片未分配或副本缺失 | _cluster/allocation/explain定位阻塞原因 |
6.2 一段写入慢的真实复盘
这次压测中,A 集群写入 p99 从 40ms 涨到了 3s。一开始团队觉得是数据量增长了,磁盘不够快,准备申请新硬件。我先把数据拉了两天监控:write queue 在高峰时积压 500+,磁盘 util 经常 100%,但 await 只有 6ms,明显不是磁盘本身响应慢,而是write 线程池处理不过来。
再往下钻,bulk 的平均批大小只有 200 条,很多请求体里还有大字段(一个 base64 图片字段)。写到 ES 里被 analyzer 处理时,CPU 都在做分词,造成处理速度跟不上。最终动作做了三件事:
- 把大 base64 字段从
text改为keyword(不做分词),加上ignore_above限制索引长度; - 客户端 bulk 并发从 4 提升到 16,批大小提高到 1000 条;
- refresh_interval 从 1s 调到 30s。
结果 p99 从 3s 降到了 55ms。全程没有加一台机器,没有换任何硬件。这轮调优最大的收获是:不要用“加资源”的逻辑来解决“消耗不合理”的问题。
6.3 小心 8.x 的“新特性”在旧硬件上是负优化
8.x 自带了引入 SIMD 指令优化的编码器(在不同 CPU 上表现差异明显)。在我测试的另一台旧服务器上,开启某些新特性后,查询性能反而波动很大。我的建议是:启用 8.x 新特性前,一定要在不断言的硬件上做 AB 压测,别因为“新版本默认开启”就觉得一定更好。
同理,8.x 里对match_only_text这类新字段类型的支持,虽然文档说省存储省 CPU,但如果你在业务里同时要用 highlighter 或 phrase query,它反而会有限制或更慢,要实际验证。
7. 调优的尽头:不是参数,是设计
和所有技术一样,ES 调优的尽头不是把参数背得滚瓜烂熟,而是在整体设计中干净利落地减少工作量:字段该不该存、需不需要分词、查询该不该这么写、分片数量是否匹配业务增长节奏。参数调整是手段,数据管理和查询设计是根。
我见过一个团队把index.refresh_interval、translog.durability、bulk.size全部调了一次性到位,然后某天业务方说“我要改实时性要求”,又全部改回来,性能打回原形。这就是典型“重参数而轻设计”的代价——ES 的性能天花板其实在索引和查询设计阶段就大致定好了,后续参数调优只能在设计框架内做到最优。
所以我的做法是:每个新索引上线前,必过一遍字段清单和查询场景清单,把“性能隐患”消灭在索引创建之前。线上出了问题再去调,永远是亡羊补牢。掌握了这套方法和第一手经验,你的 Elasticsearch 8.x 调优才算是入了门。