ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Elasticsearch 8.x 性能调优实战:从磁盘判断到查询改写复盘

Elasticsearch 8.x 性能调优实战:从磁盘判断到查询改写复盘 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机械盘、10msSATA SSD、2-5msNVMe那就要警惕了。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?vssegments:desc # 3. 看线程池积压 GET /_cat/thread_pool/write,search?vhnode_name,name,active,queue,rejected,completed # 4. 看压测/链路中最耗时的分片分布 GET /_cat/shards?vsstore: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 查询再慢也就是几表 joinES 里如果每个 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 执行非常耗 CPUwildcard更是经典性能杀手如果业务允许尽量改成ngram或edge_ngram索引。3.2 filter context 与 query context 的差异要刻进本能ES 查询分两种 contextquery 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你要排序就必须启用 fielddatafielddata 一来堆内存就炸。实测中一个高流量的查询如果对 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 默认是每个请求都要 fsyncdurabilityrequest如果换成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 使用率含 iowaitiowait 高基本指向磁盘用户态 CPU 高大概率是查询或 merge。磁盘吞吐与延迟除了 iostatES 自身的_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 设成 16GBJVM 堆用 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、fielddatafielddata 过大、缓存未命中、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 调优才算是入了门。
返回列表