ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Elasticsearch性能调优实战:JVM、分片与查询优化

Elasticsearch性能调优实战:JVM、分片与查询优化 Elasticsearch性能调优这个话题网上写的人多写透的人少。我前后维护过几套ES集群从单机到多节点从写入密集到查询密集都折腾了一遍最深刻的体会是调优不是多改几个参数而是先搞清楚瓶颈在哪个环节。很多朋友一上来就堆参数最后CPU还是爆查询还是慢原因就是没按场景找对调优点。这篇文章想把我在实际项目中积累的调优路径完整梳理一遍从安装起步到写入、查询、故障排查尽量讲清楚每个参数背后的为什么也方便你直接拿去用。先给个入门门槛的判断如果你是第一次装ES建议先按官方要求把基础环境跑通不要一上来就魔改。刚把Windows或Linux上的服务启动起来先把监控打开把日志出口配上再谈优化。如果你已经在生产环境被CPU负载报警搞过几次那这篇文章的重点你更容易get到性能和稳定性是同一个问题的两面很多调优动作其实是在给系统留余地。1. 内容整体设计与思路拆解1.1 性能调优到底在调什么ES的整体链路其实不复杂数据进来先过分析器和Mapping写进Lucene索引再通过translog和refresh对外可见查询时走协调节点路由到分片借助filter cache、fielddata等机制组织结果。整条链路任何一个环节卡住都会体现在“写不进去”“查不快”“集群黄红”这三种症状上。我用一个生活类比ES调优跟开一台老车一样得先明确是发动机没力、轮胎漏气、还是你挂错了挡。很多新手调ES把三大症结全归到“JVM堆太小”就像只要车跑不动就怪机油最后机油加得溢出来车更跑不动。堆不是唯一变量也不是首要变量。所以我在做性能诊断时第一件事就是画一张“压力地图”把问题定位到具体层面再动手。这张图通常包括四个维度服务器层面CPU、磁盘IO、内存、网络吞吐JVM层面堆使用率、Full GC频率、线程池拒绝次数索引层面分片数、段数量、索引缓冲、刷新频率查询层面候选分片数、缓存命中率、路由是否合理。这四个维度不是并列关系而是有先后顺序的。我一般的排查顺序是先用系统的CPU和IO指标排除硬件和操作系统问题再看JVM的GC情况排除堆内问题然后看索引的分片设计和查询路由是否合理最后才深入具体的慢查询和缓存。顺序反了很容易被表象误导比如看到CPU高就往查询方向查结果其实是段合并或分片恢复在抢资源。1.2 不同业务场景下的优化重心ES的业务场景大方向只有两个拿它做搜索引擎和拿它做日志或时序分析。前者以查询为主索引写入量相对平稳后者以写入为主半夜还有高峰查询窗口反而是碎片化的。两者的优化侧重点完全不一样。写入密集场景核心诉求是快速持久化重点盯索引延迟、bulk拒绝率、translog落盘耗时。这时候优先考虑批量提交、调整refresh间隔、适当调低副本数同时把分片数量控制好。这个场景里数据可见性可以稍微妥协比如离线导入任务通常不需要秒级可查那就可以把refresh关掉或大幅拉长。查询密集场景核心诉求是低延迟和高吞吐重点关注缓存命中率、堆内存的合理分配、查询语句的层级优化、段合并策略。这个场景下宁可稍微牺牲一点写入吞吐也要保证在线查询不抖动。比如电商商品搜索几万个商品实时更新查询QPS上百写入量并不大优化重心就应该放在filter缓存和doc_values上而不是盲目扩大堆内存。把场景分清楚之后我就不会再拿一套配置去套所有集群了。这篇文章后续讲的每个参数也都尽量标注适用场景方便你对号入座。2. 环境搭建与JVM参数配置详解2.1 安装启动的常见细节很多人以为性能调优是从改elasticsearch.yml开始的其实从解压安装那一刻性能走向就已经定了。最近总有人问Windows上启动ES、Win11装ES和Kibana的问题我简单说几个要点。官方发行包有zip和tar.gz两种Windows上用zip包解压就能跑但要先确认JDK版本。ES 7.x之后自带JDK解压后直接用bin目录下的启动脚本就行如果你用了旧发行版PATH里的JDK版本不对启动时报“Unsupported major.minor version”的概率很高。在Windows上启动还有一个隐藏问题启动脚本读配置文件时偶尔会拿不到正确路径导致JVM参数没有生效。我的做法是启动前先确认config目录里的jvm.options能被读取把-Xms和-Xmx显式写在jvm.options里不要依赖环境变量。这个细节很容易被忽略但很多Windows上启动后内存占用异常、频繁GC的问题根子就在这。连接Kibana时ES和Kibana版本必须一致别图方便混着装。Kibana默认读localhost:9200如果你是跨机访问要改kibana.yml里的elasticsearch.hosts还要确认ES的network.host不是只绑定了回环地址。生产环境我建议把network.host显式设置成实际内网IP但别为了方便直接开公网访问安全认证一定要跟上。2.2 堆内存别只盯着“越大越好”ES的堆分配业界比较共识的一句话是不要超过32GB。超过32GB后JVM会压缩对象指针失效反而造成地址空间浪费。更常用的经验值是物理内存的一半打个比方机器64G内存堆给31G剩下给Lucene做文件缓存只有16G内存的机器就别硬塞堆能分8G给ES就不错了。我在生产环境踩过很痛的一次把堆从8G调大到31G以为查询快一倍结果Full GC从几小时一次变成几分钟一次查询延迟反而上去了。原因很简单堆修得太大老年代垃圾回收的时间也变长吞吐量明显下降。调堆一定要用监控数据说话不要拍脑袋。垃圾回收器这块ES 7.x开始已经默认用G1到了8.x、9.x基本也是这个路线。用G1的核心理念是把Stop The World尽量控制在可接受范围内而不是追求单次GC最快。如果你还在手动折腾CMS参数建议直接删掉让ES用默认值。堆之外还有两个容易被忽略的参数bootstrap.memory_lock设为true锁定内存避免内存交换到磁盘。Linux服务器上要配合ulimit -l去掉限制否则启动日志会警告一片。swap设置能禁用swap就禁用。ES对内存交换非常敏感一旦内存落盘查询延迟秒级抖动能把人逼疯。这两个参数在Windows环境里通过配置文件也能设置但Windows的内存管理机制和Linux不同锁定内存的效果有限更重要的是保证机器上别同时跑一堆吃内存的进程。ES最怕的就是和其他重量级应用抢内存系统频繁换页那种性能下降不是任何调优参数能救回来的。3. 索引与分片设计——性能的根基3.1 分片数量定多少、副本怎么取舍分片数量可以说是ES性能里的“地基工程”定了之后基本很难改。很多人喜欢给每个索引开几十个分片以为并行度高就快实际上分片多了每个分片都是独立Lucene索引查询要广播到所有分片再合并结果开销是翻倍式的。我的经验值很简单数据总量除以期望单分片大小单分片建议控制在20GB到50GB之间。如果你一条索引要承载500GB数据拆成20到25个分片是比较稳妥的如果每天数据只有几个GB按天滚动加少量分片更合适。分片数量还得留扩展余量因为ES是弹性伸缩的但你总不能没事就做reindex吧。副本数量对性能的影响更直观副本太少读压力全压在主片上副本太多写入要做同步复制磁盘和网络开销都会膨胀。写入高峰期很多人用副本数0配合批量写写完再切回副本1这是合理的操作但别忘记切回来。我还见过一种更隐蔽的问题节点数等于副本数加一看起来副本分配正常但某个节点宕机后所有副本都挤到剩余节点上红色健康状态一挂就是半天。分片分配时要考虑节点的故障域别把鸡蛋都放一个篮子里。3.2 索引生命周期管理让数据按热度分流数据总不会永远一样热。日志型数据最近三天的索引天天被查三个月前的索引吃灰但还占着硬盘却让它们和热索引一起跑在同样的副本数和同样的段配置下这本身就是性能损失。我在日志集群里落地ILM的时候通常设两层hot阶段新索引写入副本数1refresh_interval保持默认保证实时可查warm或cold阶段几天后滚动把数据迁移到容量型节点强制合并段甚至把副本数降到0。这套东西光靠人力维护不现实得用ILM策略自动执行。ILM的滚动条件我常用“索引容量超过30GB或创建超过24小时”两种条件一起写哪个先到滚哪个。这样索引数量不会无限膨胀查询也不会被拖进不该去的分片里。索引别名也很重要。业务查询尽量通过别名访问不要把索引名写死在代码里。一旦发生滚动或重建索引代码改动最少查询路由也稳定。这个习惯我一直坚持碰到过很多次因为硬编码索引名导致维护时抓狂的案例。4. 写入性能调优让数据落盘更快4.1 大批量写入用bulk别一条条搞最常见的写入性能事故不是ES能力不行而是业务侧一条条提交。单个文档就是一个HTTP请求还要做分词、Mapping解析、Lucene写盘几百QPS就能把集群打冒烟。批量写入接口确实是ES的官方答案但批量大小的选择也有讲究。我见过很多人固定拿每批10000条去压结果内存没炸bulk rejection先来了。因为客户端和ES之间网络带宽、ES索引缓冲和刷新频率共同决定了一个合适区间。更稳的做法是用数据量而不是条数来估算每批数据控制在5MB到15MB之间再用压测把临界点试出来。批量提交的架构我顺便说下用消息队列做缓冲再交给ES这种模式在日志场景非常香。先让可控的缓冲层挡在前面ES出现暂时性抖动时数据停留在队列而不是直接压进集群恢复起来也从容。当然队列本身也不能无限堆积要配套消费延迟监控。4.2 refresh、translog和索引缓冲的调优逻辑写入性能的三个最关键参数几乎决定了ES集群的写入天花板。下面我拆开说。refresh_intervalLucene的刷新决定新写入文档能否被搜索到默认1秒一次。绝大多数日志场景不需要秒级可查我会在导入期间改成30秒有些纯离线导入直接改成-1写完再刷。代价是搜索可见延迟变大但写入吞吐有肉眼可见的提升。translog默认每次落盘才刷新一次索引缓冲区如果改成异步写入吞吐能再上一个台阶。但注意translog是崩溃恢复的命根子异步模式下极端故障可能丢一点数据业务能接受才能用。我通常在批量导入窗口临时切async导入完立刻改回来。索引缓冲indices.memory.index_buffer_size默认10%的堆内存这块理解为JVM中预留用来积攒索引数据的缓冲即可。简单项目不需要动但如果堆调大后GC压力大可以适当调低这个百分比给查询留空间。每个参数都不是孤立存在的。机器IO快但堆小瓶颈就在刷新和内存堆大但磁盘IO差translog就常常塞车。这就是为什么我一直强调先用监控定位再动手调参。5. 查询性能调优让检索更快5.1 缓存最直接见效但也有陷阱的优化点ES查询侧的缓存用得好的话性价比超高。首先是filter query的缓存机制ES会把相同条件的filter缓存起来之后的查询直接走缓存结果。那些“用户只看状态为线上且日期在今天”之类的固定条件特别适合放进filter里。我会让研发在写查询时把低频变化的条件全放到filter context不要放到must里。request cache则更适合分页查询、仪表盘这类固定请求的重复调用缓存命中后响应时间能从百毫秒级掉到个位数毫秒。但注意带sort字段或者聚合的查询不一定会被缓存因为每一次的排序结果都依赖当时的段数据。这时候你需要自己评估缓存命中率到底划不划算。一个常见的踩坑对keyword高基数字段做terms聚合并且基数上百万比如用户ID、IP、URL这些字段就算开了doc_values也不该随便聚合。优化手段就是先用filter缩小范围再聚合别给整个索引的数据做全局聚合。这个坑我见过太多次一次聚合查询把整个节点CPU打满页面直接卡死。5.2 查询写法与数据建模性能差距往往藏在mapping里很多查询慢不是ES穷而是mapping一开始就建歪了。最典型的是动态mapping把整个文档里所有字段都开了text分词导致索引膨胀、查询变慢。我现在上线前至少会给每个字段定一次型需要分词的用text不需要的用keyword只做范围和排序的用integer或date不需要聚合的字段干脆关闭doc_values。查询写法上的细节更值得注意。wildcard查询和正则查询在Lucene里优化难度很高能用prefix就尽量别用wildcard前缀匹配在倒排索引结构上还能借力。如果业务真需要“包含某个单词”这种模糊搜索宁可多花成本用ngram分词也别让生产环境跑一堆能拖垮CPU的通配符查询。分页深度也是个老生常谈的问题。深分页要的是全部排序后再取偏移量比如from10000就算每页10条也需要翻一遍前10000条排序结果。业务落地时我会限制from加size的总和上限并用search_after支持真正的深度翻页。翻页超过一定深度后用户体验其实已经难以维持了更需要的是搜索条件的优化。5.3 段合并和存储优化后台任务同样影响在线服务Lucene把数据拆成一个个段文件段越多查询就要在多个段上并行工作性能肯定往下掉。ES默认的合并策略已经尽力平衡IO和性能但我对只读索引的处理是手动强制合并到单个段或少量段查询速度提升是维度层面的。注意强制合并期间IO压力巨大我会在业务低峰窗口执行并且限速。存储和压缩方面冷索引可以切换成压缩比更高的codec比如best_compression代价是查询时CPU略微增加。索引压缩完之后体积小IO也少对冷热数据分层明显的大集群收益很可观。搜索快不快有一种被忽略的因素是操作系统的page cache命中率。让OS的page cache尽可能多地承载热索引数据比什么都香——这就是为什么堆不要贪大留给系统文件缓存才真正实惠。6. 常见问题排查与参数速查6.1 查热点最快的定位手段ES提供了一套诊断思路我按常用程度排一下_cat/health看集群黄红状态_cat/shards看分片分布和未分配原因_nodes/hot_threads看热点线程栈_nodes/stats看JVM和线程池指标打开慢日志定位具体查询和写入。Hot Threads这招特别管用。上次有个集群查询突然变慢用hot_threads看到大量线程停在Lucene的TermsQuery上再定位到某条大范围查询改完写法性能直接恢复。查热点几乎是我出问题时的第一选择能帮你省下一晚上的看日志时间。慢日志的阈值设置也很灵活我会先开一个相对宽裕的口径比如查询超过500ms就记录跑几天再把阈值下降到200ms把隐藏的拖后腿查询揪出来。慢日志不是一次配完的它应该是一个逐步收紧的过程。写入慢日志同样重要能暴露bulk批次过大或磁盘IO抖动的问题。6.2 核心参数速查表我把实战中常用的参数整理成了一张表方便你快速对照。注意所有参数只代表了我的经验取值不是金科玉律必须先测后用。配置项建议值适用场景备注JVM堆Xms等于Xmx物理内存一半不超过31G通用超过32G存在对象指针压缩失效问题bootstrap.memory_locktrue生产环境需配合系统ulimit -lWindows上锁内存效果有限refresh_interval30秒到-1导入或日志场景要求秒级可见时回到1秒translog.durabilityasync批量导入窗口故障可能丢最近数据使用后需改回index.number_of_shards按数据总量除以单分片20-50GB通用索引创建后不可修改index.number_of_replicas1导入期可设0通用写高峰用0副本写入后再改回indices.memory.index_buffer_size堆的10%左右通用堆很大时适当下调给查询留空间node_concurrent_recoveries2到4节点恢复时恢复速度与IO压力平衡indices.recovery.max_bytes_per_sec40mb到100mb大规模恢复或重启避免恢复风暴拖垮集群index.merge.scheduler.max_thread_count2到3机械盘环境SSD可保持默认这张表里的值我都是在常见硬件条件下验证过的但你机器的CPU核数、磁盘类型、数据特征都会影响最优值。所以它更像一个起点不是终点。6.3 恢复数据与节点重启时的调优细节最后说一个很多人问的重启恢复场景。ES节点因为扩容或运维需要重启时如果同时启动多个节点分片恢复流量可能瞬间把网络和IO打满其他节点查询也跟着遭殃。建议重启前先把cluster.routing.allocation.enabled设为none节点全部起来后再恢复分配同时把恢复并发和吞吐都限制在可接受范围。这种恢复场景在Windows和Linux上都会遇到尤其是日志集群节点重启后一堆未分配分片争着抢资源看起来就像集群被攻击了。我通常在恢复窗口把max_bytes_per_sec调到中等偏低的数值让恢复慢但稳避免全集群查询超时。恢复期间热点查询变慢是正常现象给业务侧打好招呼不要一看到告警就手忙脚乱。我自己还遇到过节点启动直接失败日志里显示memory lock失败的情况。这类问题一般就是ulimit配置没生效或者Windows上锁内存的权限不足。任何配置改动后第一次启动建议直接看启动日志前100行确认没有warning再继续操作。这个习惯帮我省了很多排查时间。最后分享一点个人经验这些参数和经验都是拿钱和数不清的凌晨换来的。但真正的调优诀窍反而不全在参数里每次只改一个变量改完留足观察期拿曲线说话。没有监控就调ES等于闭着眼开车有监控但一次改一堆参数出了问题你也不知道是哪个参数惹的祸。另一件事是调优是持续动作。新业务上线、新节点加入、数据总量翻倍都意味着之前的“最优值”可能已经失效。我每季度会重新审视一遍集群的监控面板把已经不适用的参数调整过来。很多系统不是被调坏的而是被“调过一次就再也不管”拖垮的。如果让我用一个顺序来收尾先解决分片和mapping问题再谈JVM参数最后才折腾各种缓存和查询写法。这个顺序我在多套集群上验证过从来没后悔过。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进