Elasticsearch 的内存优化
Elasticsearch 的内存优化
找到一篇之前自己总结的 es 内存优化的观察记录,发出来分享保存一下,希望可以对大家有所帮助和启发
关于 ES 选择垃圾收集器
CMS 和 G1
大内存机器( 大于 8 G )建议使用 G1 。
G1 (425 次 young gc)
CMS (1960 次 young gc)
G1 (一夜过后增长到 600 次)
下图 5 s 间隔
CMS (一夜过后增长到 2713 次)
从增长量来看
(2713-1960)/(600-425) = 753 / 175 = 4.3 倍
总时长 9 :75 。CMS 是 G1 的 8 倍
Java 版本为 openjdk version “11.0.9” 2020-10-20 LTS
CMS 机器配置:
VM Flags:
-XX:+AlwaysPreTouch -XX:CICompilerCount=15 -XX:CMSInitiatingOccupancyFraction=75 -XX:ErrorFile=/var/log/elasticsearch/hs_err_pid%p.log -XX:+HeapDumponOutOfMemoryError -XX:HeapDumpPath=/var/lib/elasticsearch -XX:InitialHeapSize=33285996544 -XX:MaxDirectMemorySize=16642998272 -XX:MaxHeapSize=33285996544 -XX:MaxNewSize=2006515712 -XX:MaxTenuringThreshold=6 -XX:MinHeapDeltaBytes=196608 -XX:MinHeapSize=33285996544 -XX:NewSize=2006515712 -XX:NonNMethodCodeHeapSize=8182140 -XX:NonProfiledCodeHeapSize=121738050 -XX:OldSize=31279480832 -XX:-OmitStackTraceInFastThrow -XX:ProfiledCodeHeapSize=121738050 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:SoftMaxHeapSize=33285996544 -XX:ThreadStackSize=1024 -XX:+UseCMSInitiatingOccupancyonly -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC
目前 老年代 30G 年轻代 2 G 建议可以适当调大年轻代大小,没看到指定元空间大小,建议 256 或者 512所以为什么 CMS 看起来堆内存空间波动这么小,是因为他年轻代就只有 2 G ,超出阈值直接触发 young gc ,所以看起来是波动小 gc 频繁
G1 机器配置(这里推荐 G1):
VM Flags:
-XX:+AlwaysPreTouch -XX:CICompilerCount=15 -XX:ConcGCThreads=6 -XX:ErrorFile=/var/log/elasticsearch/hs_err_pid%p.log -XX:G1ConcRefinementThreads=23 -XX:G1HeapRegionSize=8388608 -XX:G1ReservePercent=25 -XX:GCDrainStackTargetSize=64 -XX:+HeapDumponOutOfMemoryError -XX:HeapDumpPath=/var/lib/elasticsearch -XX:InitialHeapSize=33285996544 -XX:InitiatingHeapOccupancyPercent=45 -XX:MarkStackSize=4194304 -XX:MaxDirectMemorySize=16642998272 -XX:MaxHeapSize=33285996544 -XX:MaxNewSize=11089739776 -XX:MinHeapDeltaBytes=8388608 -XX:MinHeapSize=33285996544 -XX:NewRatio=2 -XX:NonNMethodCodeHeapSize=8182140 -XX:NonProfiledCodeHeapSize=121738050 -XX:-OmitStackTraceInFastThrow -XX:ProfiledCodeHeapSize=121738050 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:SoftMaxHeapSize=33285996544 -XX:ThreadStackSize=1024 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
VM Arguments:
jvm_args: -Xms31g -Xmx31g -XX:+UseG1GC -XX:NewRatio=2 -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=45 -Des.networkaddress.cache.ttl=60 -Des.networkaddress.cache.negative.ttl=10 -XX:+AlwaysPreTouch -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -XX:-OmitStackTraceInFastThrow -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dio.netty.allocator.numDirectArenas=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Djava.io.tmpdir=/tmp/elasticsearch-11678995370584773542 -XX:+HeapDumponOutOfMemoryError -XX:HeapDumpPath=/var/lib/elasticsearch -XX:ErrorFile=/var/log/elasticsearch/hs_err_pid%p.log -Xlog:gc*,gc+age=trace,safepoint:file=/var/log/elasticsearch/gc.log:utctime,pid,tags:filecount=32,filesize=64m -Djava.locale.providers=COMPAT -Dio.netty.allocator.type=pooled -XX:MaxDirectMemorySize=16642998272 -Des.path.home=/usr/share/elasticsearch -Des.path.conf=/etc/elasticsearch -Des.distribution.flavor=default -Des.distribution.type=rpm -Des.bundled_jdk=true
目前设置为 堆大小 31 G (32 G 会取消指针压缩这个影响会很大,32G 以上寻址没法压缩,一个问题,理论上等于 32G 也可以指针压缩吧?可能会可能不会,JVM 具体实现不同可能这个阈值的真值不同,所以 31 G 最稳妥)设置了 -XX:NewRatio=2 新老比为 1:2新生代大小约 10G 老年代大小约 20G目前看来 每次 young gc 后仍会有少量对象进入老年代,老年代已用 10.5 G建议可以使用默认值 年轻代最大 60% 即 18.6G 老年代 12.4G。观察老年代是否趋于平衡建议设置元空间大小 512M OR 256M165 s 一次 yang gc old 涨幅 2174 kb ~ 2M 920 1M 假设取 1.5M即每 1500/165~9 约每秒 9kb ;777mb 每天 约 12 天一次 mixed gc目前看来 ES 正常,之前可能是指针压缩的问题-XX:G1HeapRegionSize=8388608,分区数 3968 个 Region 8M Humongous 对象过多,建议扩充一倍,或者使用默认分片数量 2048 。
G1 执行 GC 清理 old user :12G(清理前) mixed gc 后 9.2 G
CMS 执行 full gc 15G -> 6.8 G
这里引发一个问题,ES 堆里都放了些什么?
在索引创建的时候会默认创建 倒排索引和正排索引 内存足够会缓存到 cache 里 ES 使用 倒排索引+正排索引 实现聚合,比如查找 名称带有 苹果 的数据并 以 颜色字段分组聚合。
考虑聚合查询的场景对应字段打开 doc_value 正排索引。否则 filedata load 内存消耗会比较大。 这里考虑了一下 应该是默认会创建 doc_value 实际在查询对应信息的时候也不是 doc_value 占用内存
内存占用分布
ES 的 Heap 内存基本上被Segment Memory、Filter Cache、Field Data Cache、Bulk Queue、Indexing Buffer、Cluster State Buffer、各类聚合查询的结果集fetch所消耗掉
1、Segment Memory
ES 的 data node 存储数据并非只是耗费磁盘空间的,为了加速数据的访问,每个segment都有会一些索引数据驻留在 heap 里。因此 segment 越多,瓜分掉的 heap 也越多,并且这部分 heap 是无法被 GC 掉的! 理解这点对于监控和管理集群容量很重要,当一个 node的 segment memory 占用过多的时候,就需要考虑删除、归档数据,或者扩容了。
常驻内存。
查看一个node上所有segment占用的memory总和
GET _cat/nodes?v&h=name,port,sm
2、Filter Cache
Filter cache 是用来缓存使用过的 filter 的结果集的,需要注意的是这个缓存也是常驻heap,无法GC的。默认 10% heap size 设置,如果实际使用中heap没什么压力的情况下,才考虑加大这个设置。
3、Field Data Cache
对搜索结果做排序或者聚合操作,需要将倒排索引里的数据进行解析,然后进行一次倒排。在有大量排序、数据聚合的应用场景,可以说 field data cache 是性能和稳定性的杀手。这个过程非常耗费时间
ES2.0以后,正式默认启用Doc Values特性,避免使用 Field Data Cache
4、Bulk Queue
Bulk Queue是做什么用的?当所有的bulk thread都在忙,无法响应新的bulk request的时候,将request在内存里排列起来,然后慢慢清掉。一般来说,Bulk queue不会消耗很多的heap,但是见过一些用户为了提高bulk的速度,客户端设置了很大的并发量,并且将bulk Queue设置到不可思议的大,比如好几千。这在应对短暂的请求爆发的时候有用,但是如果集群本身索引速度一直跟不上,设置的好几千的queue都满了会是什么状况呢? 取决于一个bulk的数据量大小,乘上queue的大小,heap很有可能就不够用,内存溢出了。一般来说官方默认的thread
pool设置已经能很好的工作了,建议不要随意去“调优”相关的设置,很多时候都是适得其反的效果。
5、Indexing Buffer
Indexing Buffer是用来缓存新数据,当其满了或者refresh/flush interval到了,就会以segment file的形式写入到磁盘。这个参数的默认值是10% heap size。根据经验,这个默认值也能够很好的工作,应对很大的索引吞吐量。但有些用户认为这个buffer越大吞吐量越高,因此见过有用户将其设置为40%的。到了极端的情况,写入速度很高的时候,40%都被占用,导致OOM。
Cluster State Buffer
ES被设计成每个Node都可以响应用户的api请求,因此每个Node的内存里都包含有一份集群状态的拷贝。这个Cluster state包含诸如集群有多少个Node,多少个index,每个index的mapping是什么?有少shard,每个shard的分配情况等等(ES有各类stats api获取这类数据)。在一个规模很大的集群,这个状态信息可能会非常大的,耗用的内存空间就不可忽视了。并且在ES2.0之前的版本,state的更新是由Master Node做完以后全量散播到其他结点的。频繁的状态更新都有可能给heap带来压力。在超大规模集群的情况下,可以考虑分集群并通过tribe node连接做到对用户api的透明,这样可以保证每个集群里的state信息不会膨胀得过大。
超大搜索聚合结果集的fetch
ES 是分布式搜索引擎,搜索和聚合计算除了在各个 data node 并行计算以外,还需要将结果返回给汇总节点进行汇总和排序后再返回。无论是搜索,还是聚合,如果返回结果的 size 设置过大,都会给 heap 造成很大的压力,特别是数据汇聚节点。
ES 集群配置
bootstrap.memory_lock=true 内存锁,生产环境无脑设为 true 会锁内存,内存不足时会使用 swap 内存空间,其性能极差
JVM 常用命令
top
top -p [pid] 键入 H 查看进程线程情况
jmap
jmap -histo [pid] > log.txt 查看内存信息,实例个数以及占用内存大小 注意导出太大问题
jmap -heap [pid] 堆信息
jmap ‐dump:format=b,file=eureka.hprof [pid] 堆内存 dump 注意导出太大问题
jstack
jstack [pid] 查找死锁
jinfo
jinfo -flags [pid] 查看正在运行的 Java 应用程序的扩展参数
jinfo -sysprops [pid] 查看 Java 系统参数
jstat
jstat -gc [pid] 堆 gc 回收情况
jstat -gccapacity [pid] 查看堆内存统计
jstat -gcnew [pid] 查看新生代 gc 回收情况
jstat -gcold [pid] 查看老年代 gc 回收情况
jstat -gcnewcapacity [pid] 查看新生代内存统计
jstat -gcoldcapacity [pid] 查看老年代内存统计
jstat -gcmetacapacity [pid] 查看元空间统计
jstat -gcutil [pid] 查看各区域使用比例
最主要的危险操作是下面这三种:
jmap -dump这个命令执行,JVM会将整个heap的信息dump写入到一个文件,heap如果比较大的话,就会导致这个过程比较耗时,并且执行的过程中为了保证dump的信息是可靠的,所以会暂停应用。 jmap -permstat这个命令执行,JVM会去统计perm区的状况,这整个过程也会比较的耗时,并且同样也会暂停应用。 jmap -histo:live这个命令执行,JVM会先触发 full gc,然后再统计信息。jmap -histo:live 22078 |more
堆内存为什么不能超过物理机内存的一半?堆对于Elasticsearch绝对重要。它被许多内存数据结构用来提供快速操作。但还有另外一个非常重要的内存使用者:Lucene。Lucene旨在利用底层操作系统来缓存内存中的数据结构。 Lucene段(segment)存储在单个文件中。因为段是一成不变的,所以这些文件永远不会改变。这使得它们非常容易缓存,并且底层操作系统将愉快地将热段(hot segments)保留在内存中以便更快地访问。这些段包括倒排索引(用于全文搜索)和文档值(用于聚合)。Lucene的性能依赖于与操作系统的这种交互。但是如果你把所有可用的内存都给了Elasticsearch的堆,那么Lucene就不会有任何剩余的内存。这会严重影响性能。标准建议是将可用内存的50%提供给Elasticsearch堆,而将其他50%空闲。它不会被闲置; Lucene会高兴地吞噬掉剩下的东西。如果您不字符串字段上做聚合操作(例如,您不需要fielddata),则可以考虑进一步降低堆。堆越小,您可以从Elasticsearch(更快的GC)和Lucene(更多内存缓存)中获得更好的性能。堆内存为什么不能超过32GB?在Java中,所有对象都分配在堆上并由指针引用。普通的对象指针(OOP)指向这些对象,传统上它们是CPU本地字的大小:32位或64位,取决于处理器。对于32位系统,这意味着最大堆大小为4 GB。对于64位系统,堆大小可能会变得更大,但是64位指针的开销意味着仅仅因为指针较大而存在更多的浪费空间。并且比浪费的空间更糟糕,当在主存储器和各种缓存(LLC,L1等等)之间移动值时,较大的指针消耗更多的带宽。Java使用称为压缩oops的技巧来解决这个问题。而不是指向内存中的确切字节位置,指针引用对象偏移量。这意味着一个32位指针可以引用40亿个对象,而不是40亿个字节。最终,这意味着堆可以增长到约32 GB的物理尺寸,同时仍然使用32位指针。一旦你穿越了这个神奇的〜32 GB的边界,指针就会切换回普通的对象指针。每个指针的大小增加,使用更多的CPU内存带宽,并且实际上会丢失内存。实际上,在使用压缩oops获得32 GB以下堆的相同有效内存之前,需要大约40-50 GB的分配堆。以上小结为:即使你有足够的内存空间,尽量避免跨越32GB的堆边界。否则会导致浪费了内存,降低了CPU的性能,并使GC在大堆中挣扎。
发布时间:2021-12-18 10:53:56