尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ElasticSearch性能调优实战:从写入到查询的优化指南

ElasticSearch性能调优实战:从写入到查询的优化指南 做ElasticSearch性能调优这件事我这两年踩了不少坑也攒了不少可以直接用的经验。很多朋友一上来就盯着JVM堆内存调参或者疯狂加副本数其实大多数情况下读写慢的根子根本不在那一层。这篇东西我不打算写教科书式的概念罗列就按我实际在项目里排查和优化ES集群的顺序来聊从写入链路、查询链路、索引设计到系统层配置把每一个“为什么这么做”都说清楚。无论你是刚接手一个慢到让人抓狂的ES集群还是在设计阶段想避开性能坑这篇都适合你花十分钟看完然后照着去改。1. 读写路径与瓶颈判断先搞清楚慢在哪一环1.1 写入链路从bulk请求到segment可见先说写入。ES的写入链路大致是客户端发来bulk请求协调节点coordinating node把数据按路由分发到对应的主分片primary shard主分片写入内存buffer同时追加translog然后经过refresh变成OS cache里的segment最后通过flush落盘。这里面每一个环节都可能成为瓶颈但大部分人的认知只停留在“ES写入慢就调bulk大小”这一步其实远远不够。内存buffer的本质是一块暂存区数据先进来等refresh触发时才生成segment并清空buffer。refresh默认每秒执行一次所以刚写入的数据大概有1秒的延迟才可见这就是ES的“近实时”特性。在性能调优时这个1秒的间隔是完全可以放宽或收紧的。translog则负责崩溃恢复每次写入都会落盘默认是同步刷盘这块磁盘IO开销很不小。如果业务允许一定的数据丢失风险可以把index.translog.durability改成async让translog异步刷盘写入性能会有立竿见影的提升但代价是节点宕机时可能丢几秒的数据。再往后是segment合并。ES会不断把小的segment合并成大的合并过程需要读和写磁盘如果合并线程配置得不合理它会反过来拖慢正常的写入和查询。这里有个反直觉的调优方向segment合并虽然是后台任务但过于频繁的合并会让磁盘IO长期处于高水位影响线上请求。我后面会专门讲怎么压住合并的“火气”。1.2 读取链路query、fetch与缓存机制读请求的链路一样有讲究。一个search请求到了协调节点会被拆分到相关的分片并行执行每个分片先在自己的segment里做query阶段找到匹配的doc id和排序值然后在fetch阶段去取完整文档内容返回给协调节点由协调节点合并排序后返回客户端。这里最常见的问题是深分页和fetch阶段读取了大字段但更隐蔽的是缓存没有生效导致同样的查询反复消耗CPU。ES的查询缓存分三层request cache缓存查询结果但只对size0的聚合生效query cachefilter cache缓存filter子句的位集fielddata缓存则是给排序和聚合用的。很多调优新手只盯着“加filter就能快”但没搞明白filter为什么快、什么时候缓存失效。filter查询会生成一个bitset位数组每个文档都有对应位标记是否匹配这个bitset可以复用。但如果你每次查询都带上随机参数比如带时间戳的now-1h缓存命中率会非常低。这个问题我后面会展开讲。1.3 先判断瓶颈再动手CPU、IO还是GC很多人在调优前根本不做诊断上来就改配置搞得集群反而更糟。我建议第一件事是看监控。ES节点上重点看几个指标CPU使用率、磁盘IO等待、JVM堆内存使用率、Full GC频率与耗时、thread pool队列积压情况。这一套指标看下来基本能定位瓶颈的大方向。CPU高多半是查询复杂、聚合多、或者segment过多导致查询需要扫描太多文件。IO高写入量大、translog同步刷盘、segment合并、或者分片副本过多重点看磁盘类型是否支持得上。GC频繁堆内存不足或过大、fielddata占用太多、或者缓存设置不合理优先检查堆内缓存和mapping设计。thread pool排队说明写入或搜索请求积压往往是上游bulk并发设置过大超过了节点处理能力。这四类指标的优先级有讲究。我见过不少人CPU都快打满了还在加bulk并发结果队列爆炸、响应超时反而更严重。先看监控再决定调什么这个顺序千万别省。2. 写入性能调优实操照着配、照着改2.1 Bulk批次大小与并发策略写入优化绕不开bulk接口。最简单的调优是调批次大小和并发度但网上流传的“一条5000、并发10”的说法属于拍脑袋得看实际数据大小。我建议按“数据体量”而不是“条数”来定单批次数据量控制在5MB到15MB之间大约对应ES官方建议的“单次bulk请求体大小5-15MB”范围。先用curl之类工具拿1000条数据量一下体积反推合适的条数比固定1000条或5000条靠谱得多。并发方面得结合客户端所在机器的资源来设。以Java的RestHighLevelClient为例我一般会先按“客户端节点数乘以分片数”粗算一个并发上限再压测确认。一个实用的判断指标是如果ES节点的thread pool里bulk队列出现积压说明并发开大了如果CPU没吃满、写入延迟也不高说明并发还有提升空间。另外批量写入务必关闭refresh的每次请求自动刷新否则indexing性能会明显下降。创建一个索引做大批量数据导入时我通常直接设refresh_interval: -1导入完成后再改回正常的秒级刷新。2.2 Refresh间隔与Translog策略的取舍这个部分是写入调优最核心的“拧螺丝”环节也最容易出问题。refresh_interval默认是1秒如果你写入量大且允许分钟级的数据可见延迟可以直接改成30秒甚至更大。这样做的收益是一秒钟内产生的数据在内存buffer里攒着一次refresh才生成一个segment减少了segment数量降低了后续merge压力同时也减少了refresh产生的CPU和磁盘IO开销。这里有个坑——如果你只改了索引级别的refresh_interval但忘了关掉bulk请求里自带的刷新等于白干。用ES的bulk API时要注意请求参数里不要带refreshwait_for或refreshtrue否则每个请求都会强制刷新性能立刻退化。translog的落盘策略影响的是可靠性但我不建议无脑改成异步。只有在充分评估过数据丢失风险后才建议把index.translog.durability改成async同时把index.translog.sync_interval调大比如5秒或10秒。我曾经在一个日志系统里做过对比同步刷盘时写入吞吐大概只有1万条每秒改成异步后能跑到2.5万条每秒但那次系统对日志丢失容忍度高所以能接受。业务数据、订单数据这类绝对不允许丢的场景还是老老实实保持同步刷盘。2.3 副本、路由与Merge调优副本数是影响写入性能的一个隐秘变量。每个副本都会完整复制一份主分片的数据所以写入时副本越多节点需要处理的复制流量就越大。大促或离线导入场景我经常临时把副本数降到0导入完成再调回去。这个操作性能收益非常明显因为省下了一半以上的写放大。路由routing也一样。默认情况下ES按_id做哈希路由如果你写入的数据在某个字段上有明显的分组特征比如按用户ID或租户ID分布可以指定routing让同一分组的数据落到同一分片。这样写入和查询都可能更高效尤其是做按租户隔离的场景。但要注意routing一旦设置查询时也最好带上同一个字段否则查询会广播到所有分片反而更慢。merge方面我是这样调的先通过_cat/segments看索引的segment数量如果单个分片的segment数量长期超过两三百个就得考虑加大index.merge.policy.max_merged_segment或者调大index.merge.scheduler.max_thread_count。但在SSD上max_thread_count建议不要太大我一般保持默认或调到max(1, min(4, processor/2))。合并太凶猛时可以在业务低峰期手动执行_forcemerge把segment合并成少数几个大段这样查询时扫描的文件数大幅减少。3. 查询读取性能调优实操慢查询终结指南3.1 正确区分filter与query把缓存吃到饱在ES里query和filter的关键区别是query会计算相关度分数filter只看是否匹配不参与评分。凡是不需要按相关度排序的场景都应该用filter这样做有两个好处一是省去了算分的开销二是可以走filter缓存。我见过最直观的一个优化案例一个带时间范围、状态码、用户ID的查询原本用了should和must的各种组合查询要一两秒。改成把时间范围、状态码、用户ID全部放进filter只用must保留必须参与评分的全文检索条件查询耗时直接降到200毫秒左右。这个变化基本不需要改硬件只是语义对调。但是filter缓存有个前提条件查询条件得能被缓存复用。如果你每次filter里都带一个动态变化的值比如timestamp: {gte: now-1h}这种ES每次生成的bitset都不一样缓存命不中。解决办法是使用date_histogram这类固定桶的方式或者自己算好固定的时间边界再塞进filter里。类似地如果业务上允许把“最近24小时”的查询改成“昨天0点到今天0点”这种固定区间对缓存命中率友好得多。3.2 深分页、Search After与Scroll的适用边界深分页是ES查询性能的经典杀手。from100000, size10这种写法协调节点会把前10万条结果的排序信息都捞出来然后丢弃前99990条浪费非常严重。应对方式有三类如果只翻前几百页且可以接受实时性用search_after。它通过上一页最后一条结果的排序值来定位下一页避免了深分页的全局排序开销。如果你要批量导出大量数据用scroll。scroll本质是快照游标适合顺序全量拉取但注意它会在内存里保留一个上下文用完要立刻清理不然堆内存会被撑爆。如果业务面临的是用户随机翻几百页的需求那基本说明产品交互有问题不如改成“加载更多”的方式。有朋友用过search_after之后反馈排序字段不稳定出现漏数据或重复数据。这个坑一般是排序值不唯一导致的。解决办法是加一个唯一字段作为排序辅助比如在sort里带上_id或者一个包含唯一标识的doc value字段让排序结果具备确定性。3.3 聚合性能、Fielddata与Doc Values做聚合时最怕的是对text类型字段做terms聚合。text字段默认经过分词器处理后存倒排索引如果直接对它做聚合会触发fielddata内存中构建的字段数据这玩意一旦数据量大会疯狂占用堆内存然后导致GC暴涨。最佳实践是在mapping阶段就规划好需要做精确聚合、排序的字段用keyword类型并开启doc_values全文检索才用text类型。doc_values是列式存储在磁盘上的正排索引做聚合排序时直接从磁盘顺序读不占堆内存。我们线上有一个大索引原先有几十个text字段默认开启了fielddata的坑把堆内存打到了85%以上GC频繁。后来重新索引把只需要精确匹配和聚合的字段全部改成keyword并启用doc_values堆内存直接降到45%查询和聚合都稳定了很多。我建议每次创建索引前都过一遍mapping强制自己回答每个字段的用途。那些又想做全文搜索又想做精确聚合的字段拆成两个子字段fields多字段特性是最常见的解法。4. 索引设计、映射与部署层面的“前置调优”4.1 Mapping设计类型选择与Doc Valuesmapping设计属于“调优前置”环节等数据都进去了再改代价会非常大。核心决策点有三个字段类型选什么、是否需要分词、是否需要doc_values。文本字段要做全文检索选text同时如果需要精确匹配或聚合就在该字段下加一个keyword的fields子字段。最适合的mapping不是在写入前靠猜而是拿一份真实业务文档做模板逐个字段过一遍。ES允许你创建索引时用dynamic_templates做兜底规则避免未知字段默认映射成text然后产生一堆无意义的倒排索引。doc_values开启后会占用一定的磁盘空间但换来的好处是排序、聚合不再吃堆内存。如果你有明确的排序聚合场景建议直接开启如果干脆不需要排序聚合可以关掉以省磁盘。但要注意修改doc_values配置必须重建索引所以这个决策尽量在规划期就定下来。4.2 分片数量规划与容量评估分片数不是越大越好。过多的分片会带来两方面的开销一是每个分片都有自己的segment和内存开销分片多了会浪费堆内存二是查询一个索引时协调节点要把请求广播到所有分片分片越多合并结果的成本也越高。反过来了分片太少又会导致单个分片过大分片恢复和迁移都很慢。我的经验是单个分片控制在30GB到50GB之间比较合适。一个索引的总数据量如果预估在300GB以内设5到10个分片通常够了。还要考虑分片数和节点数的关系建议让每个节点上的分片数控制在10到20个以内包含副本超过这个数节点管理分片的开销会明显上升。这里没有银弹公式先按数据量和查询模式算出大概值再用压测修正。4.3 JVM堆内存、Swap与系统层配置ES的JVM堆内存不是越大越好官方建议最大不超过32GB。原因是超过32GB后JVM会启用压缩指针失效的OOP模型对象头变大内存利用率反而下降。更重要的是ES大量使用OS cache来缓存磁盘上的数据堆内存给得太多留给OS cache的空间就少了而OS cache对读写性能的影响往往是决定性的。我一般建议物理机内存一半给ES堆一半留给OS缓存。Swap必须关掉或者至少在配置里设为极小值。可以用bootstrap.memory_lock: true锁定内存避免运行时发生swap导致GC问题。另外Linux系统层注意文件描述符上限ES对打开文件数量要求很高/etc/security/limits.conf里的nofile建议调到65536以上。磁盘方面有条件就上SSD/NVMe写入和合并都能受益如果必须用机械盘至少要确保translog和segment不共用同一个物理盘不然写入和查询互相抢IO性能会很差。5. 常见问题与排查实录5.1 写入性能突然下降的排查思路一个ES集群写入正常突然某天变慢我建议按这个顺序排查先看bulk响应里有没有reject再看磁盘IO是不是被打满了然后看段合并是否在疯狂运行最后看GC情况。有一次线上日志集群写入变慢我上去看到bulk线程池队列积压了几千条磁盘IO也到了90%以上。当时第一反应是扩磁盘IO后来细查发现是一个大索引长期没有forcemergesegment数量多得离谱后台merge线程一直在抢IO。最后在低峰期对旧索引做了forcemerge并调整了新索引的refresh_interval写入就恢复了。这个过程说明一个问题写入慢经常是“合并引起的次生灾害”单纯加机器不解决问题。5.2 查询慢且缓存命中率低查询慢如果集中在相似的查询模式先看缓存命中率。ES的_cat/nodeattrs和_nodes/stats里能看到各节点缓存情况。缓存命中率低通常是查询条件里有动态参数或者filter条件顺序不对。ES的filter缓存是按子句顺序生成缓存key的相同语义下如果子句顺序每次不一样也可能导致缓存无法复用。另一个常见问题是查询字段没有建索引。某个字段如果在mapping里设置成index: false它就只能存不能查如果拿它做filter性能会很差。这种问题通过_mapping接口检查即可确认。出现这类情况只能重建索引所以事前规划mapping真的很重要。5.3 Segment合并拖垮IO_forcemerge是个好东西但用不好也很坑。forcemerge会触发生成超大segment这个过程中IO压力非常大所以只能在业务低峰期执行且要分批做。我曾经在一个每分钟都有大量写入的索引上执行过forcemerge结果磁盘IO飙升整个节点的查询都跟着抖动。后来改成对“只读索引”做forcemerge或者只对几天前的旧索引段做才彻底避免这个问题。日常情况下我建议给merge线程留一点余量不要让合并占满磁盘IO。可以通过index.merge.scheduler.max_thread_count限制并发合并线程数但这里有个细节——在SSD上磁盘IO不是主要瓶颈线程数反而可以稍微调大在机械盘上必须保守。还有一个经验max_merged_segment设得很大大segment的存储效率高查询扫描文件少但是大segment的生成过程可能很慢。建议按查询性能优先还是写入性能优先来决定。5.4 多轮调优后的效果数据参考给一个我在日志场景下的实测数据供参考原始配置用默认值bulk批次1万条、并发16refresh间隔1秒translog同步刷盘写入吞吐约8000条每秒调整后批次按数据量控制在10MB左右、并发降低到8refresh_interval调成30秒translog异步5秒刷盘同一批机器写入吞吐提升到2.2万条每秒。查询方面把动态filter改成固定时间桶命中缓存后类似查询从800毫秒降到150毫秒。单说数字不一定适用你但这个量级的变化说明调优的空间确实很大。最后再分享一点个人体会ES调优这个事最忌一上来就抄别人的配置。每套集群的数据量、查询模式、硬件条件都不一样正确的做法是用监控数据说话先定位瓶颈在CPU、IO还是内存再针对性地调一个参数观察一段时间再动下一个。我踩过最深的坑就是同时改了好几个配置出了问题都不知道该回滚哪一个。如果你手头正有一个慢集群建议从最容易见效的几件事试起关闭bulk请求里的自动refresh、把不需要评分的查询改成filter、重新设计mapping里的字段类型、控制好分片数。这几步做下来集群一般会有可感知的提升。ES的调优不是一门玄学只要链路通了、原理明白了大部分性能问题都能找到对应的解法。希望这篇经验能帮你少走些弯路。
返回列表