Elasticsearch内存管理与JVM优化:原理剖析与最佳实践

发布于:2025-05-09 ⋅ 阅读:(11) ⋅ 点赞:(0)

#作者:孙德新

一、Elasticsearch缓存分类

1、Node Query Cache:

每一个节点有一个 Node Query 缓存
由该节点的所有 Shard 共享,只缓存 Filter Context 相关内容
Cache 采用 LRU 算法
静态配置,需要设置在每个 Data Node 上
Node Level - indices.queries.cache.size:”10%”
Index Level: index.queries.cache.enabled: true

2、Shard Request Cache:

缓存每个分片上的查询结果
只会缓存设置了 size=0 的查询对应的结果。不会缓存hits。但是会缓存 Aggregations 和 Suggestions
Cache Key
LRU 算法,将整个 JSON 查询串作为 Key,与 JSON对象的顺序相关
静态配置
数据节点: indices.requests.cache.size:“1%

3、Fielddata Cache:

除了 Text 类型,默认都采用 doc_values。:节约了内存。Aggregation 的 Global ordinals 也保存在 Fielddata cache 中
Text 类型的字段需要打开 Fileddata 才能对其进行聚合和排序。Text 经过分词,排序和聚合效果不佳,建议不要轻易使用
配置:可以控制 Indicesfielddatacachesize 避免产生 GC(默认无限制)

缓存失效:
Node Query,Cache。保存的是 Segment 级缓存命中的结果。Segment 被合并后,缓存会失效
Shard Request Cache:分片 Refresh 时候,Shard Request Cache 会失效。如果 Shard 对应的数据频繁发生变化,该缓存的效率会很差
Fielddata Cache:segment被合并后会失效

二、管理内存的重要性:

  1. Elasticsearch 高效运维依赖于内存的合理分配。可用内存一半分配给 JVM,一半留给操作系统,缓存索引文件
  2. 内存问题,引发的问题
    长时间 GC,影响节点,导致集群响应缓慢
    OOM.导致丢节点

三、内存常见的问题

案例一

问题:Segments 个数过多,导致 full GC
现象: 集群整体响应缓慢,也没有特别多的数据读写。但是发现节点在持续进行 Full GC
分析: 查看 Elasticsearch 的内存使用,发现 segments.memory 占用很大空间
解决: 通过 force merge,把 segments 合并成一个
建议: 对于不在写入和更新的索引,可以将其设置成只读。同时,进行 force merge 操作。如果问题依然存在,则需要考虑扩容。此外,对索引进行 force merge ,还可以减少对global ordinals 数据结构的构建,减少对 fielddata cache 的开销

案例二

问题:Field data cache 过大,导致 full GC
现象: 集群整体响应缓慢,也没有特别多的数据读写。但是发现节点在持续进行 Full GC
分析: 查看 Elasticsearch 的内存使用,发现 fielddata.memorysize 占用很大空间。同时,数据不存在写入和更新,也执行过 segments merge。
解决: 将indices.fielddata.cache.size 设小,重启节点,堆内存恢复正常
建议: Field data cache 的构建比较重,Elasticsearch 不会主动释放,所以这个值应该设置的保守一些。如果业务上确实有所需要,可以通过增加节点,扩容解决

案例三

问题:复杂的嵌套聚合,导致集群 full GC
现象: 节点响应缓慢,持续进行 Full GC
分析: 导出 Dump 分析。发现内存中有大量 bucket 对象,经过查看日志发现存在复杂的嵌套聚合

解决: 优化聚合
建议: 在大量数据集上进行嵌套聚合查询,需要很大的堆内存来完成。如果业务场景确实需要,则需要增加硬件进行扩展。同时,为了避免这类查询影响整个集群,需要设置 Circuit Break和 search.maxbuckets 的数值

案例四

包含多种断路器,避免不合理操作引发的 OOM,每个断路器可以指定内存使用的限制
Parent circuit breaker: 设置所有的熔断器可以使用的内存的总量
Fielddata circuit breaker: 加载 fielddata 所需要的内存
Request circuit breaker: 防止每个请求级数据结构超过一定的内存(例如聚合计算的内存)
In flight circuit breaker: Request中的断路器
Accounting request circuit breaker: 请求结束后不能释放的对象所占用的内存

Circuit Breaker 统计信息:
GETnodes/stats/breaker?
Tripped 大于 0,说明有过熔断。Limit size 与estimated size 约接近,越可能引发熔断。千万不要触发了熔断,就盲目调大参数,有可能会导致集群出问题,也不因该盲目调小,需要进行评估。建议将集群升级到 7.x,更好的Circuit Breaker实现机制。增加了indices.breaker.total.use real memory 配置项,可以更加精准的分析内存状况,避免 OOM

四、内参分配最佳实践

1、jvm heap分配

es默认会给jvm heap分配2个G的大小,对于几乎所有的生产环境来说,这个内存都太小了。如果用这个默认的heap size,那么生产环境的集群肯定表现不会太好。
有两个方式来调节es中的jvm heap size。设置环境变量ES_HEAP_SIZE。当es进程启动的时候,会读取这个环境变量的值,然后设置为jvm的heap size。举例,可以这样来设置:export ES_HEAP_SIZE=10g。此外,还可以在启动es进程的时候,传递一个jvm的option,比如:ES_JAVA_OPTS=“-Xms10g -Xmx10g” ./bin/elasticsearch,但是要注意-Xms和-Xmx最小和最大堆内存一定设置的一样,避免运行过程中的jvm heap resize,非常耗时。

在老版本的es中,比如es 2.x里面,一般推荐用ES_HEAP_SIZE环境变量的方式来设置jvm heap size。
在新版本的es中,比如es 5.x里面,一般推荐在jvm.options文件里面去设置jvm相关的参数。

2、将机器上少于一半的内存分配给es

一个常见的问题就是将es进程的jvm heap size设置的过于大了。比如有一台64G的机器,要给es jvm size设置64G内存,不不合理。有人认为,直接将机器上的可用的内存都分配给es jvm heap,性能是绝对高的,因为大量的数据都可以缓存在内存里面。
虽然heap对于es来说是非常重要的,jvm heap被es用来存放很多内存中的数据结构来提供更快的操作性能。但是还有另外一个内存的用户,那就是lucene。lucene的设计就是要使用底层的os filesystem cache来缓存数据结构。lucene的segment是保存在单独的文件中的。因为这些segment是不可变的,所以这些文件实际上也从来不会改变。这样的话,就可以更好的缓存这些文件,底层的os cache会将hot segment驻留在内存中以供更快的访问。这些segment包括了倒排索引(为了全文检索)以及正排索引(为了聚合操作)。lucene的性能是严重依赖于底层的os的,但是如果给了过多的内存到es的jvm heap,那么就没有足够的内存留给lucene。这会极大的影响性能。

es的性能很大的一块,其实是由有多少内存留给操作系统的os cache,供lucene去缓存索引文件,来决定的。所以说lucene的os cache有多少是非常重要的。

一般建议的是,将50%的内存分配给es jvm heap,然后留50%的内存给os cache。留给os cache的内存是不会不使用的,lucene会将剩下的内存全部用光,用来cache segment file。如果我们没有对任何分词的text field进行聚合操作,那么我们就不需要使用fielddata,甚至可以考虑给os cache更多的内存,因为fielddata是要用jvm heap。如果给jvm heap更少的内存,那么实际上es的性能反而会更好,因为更多的内存留给了lucene用os cache提升索引读写性能,同时es的jvm heap的gc耗时会更少。

3、不要给jvm分配超过32G内存

还有另外一个原因不要将过多的内存分配给es的jvm heap。如果heap小于32G的化,jvm会用一种技术来压缩对象的指针,object pointer。在java中,所有的对象都会被分配到heap中,然后被一个pointer给引用。object pointer会指向heap中的对象,引用的是二进制格式的地址。

对于32位的系统来说,jvm最大的heap size就是4G,解释一下,32位,0和1值,0和1在32位的组合是2^32次方的字节,除以1024就是多少k,再除以1024就是多少mb,再除以1024就是多少gb,最后算下来就是4G。对于64位的系统来说,heap size可以更大,但是64位的object pointer会耗费更多的空间,因为object pointer更大了。比浪费更多内存空间更恶劣的是,过大的object pointer会在cpu,main memory和LLC、L1等多级缓存间移动数据的时候,吃掉更多的带宽。

所以jvm用了一种技术,叫做compressed oops来解决object pointer耗费过大空间的问题。这个技术的核心思想是,不要让object pointer引用内存中的二进制地址,而是让object pointer引用object offset。这就意味着32位的pointer可以引用400万个对象,而不是400万字节。这也意味着,使用32位的pointer,最大的heap大小可以到32G。此时只要heap size在32G以内,jvm就会自动启用32位的object pointer,因为32位的对象指针,足够引用32G的内存了,就可以用32位的pointer替代64位的pointer。但是32位的pointer比64位的pointer可以耗费更少的内存耗费。

如果给jvm heap分配的内存小于32G,此时jvm会自动使用32位的object pointer,同时是让pointer指向对象的offset,32位的object pointer就足以引用32G的内存,同时32位的pointer占用的内存空间很少,对cpu和memory之间移动数据的带宽开销也很少。这个过程就叫做compressed oops。

但是一旦越过了32G这个界限,就是给jvm heap分配了超过32G的内存,就没有办法用32位的pointer+引用object offset的模式了,因为32位的pointer最多引用32G的内存,超过了32G,就没法用32位pointer。不用32位pointer,就只能用64位pointer,才能引用超过32G的内存空间。此时pointer就会退回到传统的object pointer引用对象的二进制地址的模式,此时object pinter的大小会急剧增长,更多的cpu到内存的带宽会被占据,更多的内存被耗费。实际上,不用compressed oops时,如果给jvm heap分配了一个40~50G的内存的可用空间,实际上被object pointer可能都要占据十几G的内存空间,可用的空间量,可能跟使用了compressed oops时的32GB内存的可用空间,20多个G,几乎是一样的。

因此即使有很多内存,但是还是要分配给heap在32GB以内,否则的话浪费更多的内存,降低cpu性能,而且会让jvm回收更大的heap。

综上所述,如果给jvm heap分配超过32G的内存,实际上是没有什么意义的,因为用64位的pointer,1/3的内存都给object pointer给占据了,这段内存空间就浪费掉了。还不如分配32G以内,启用compressed oops,可用空间跟你分配50个G的内存,是一样的。

所以也正是因为32G的限制,一般来说,如果es要处理的数据量上亿的话,几亿,或者十亿以内的规模,建议是用64G的内存的机器比较合适,有个5台可以。给jvm heap分配32G,留下32G给os cache。

4、在32G以内的话具体应该设置heap为多大?

这个是根据具体情况而定的,不是固定死的,根据不同的jvm和平台而变。一般而言,将jvm heap size设置为31G比较安全一些。主要是要确保说,你设置的这个jvm heap大小,可以让es启用compressed oops这种优化机制。此外,可以给jvm option加入-XX:+PrintFlagsFinal,然后可以打印出来UseCompressedOops是否为true。这就可以让我们找到最佳的内存设置。因为可以不断调节内存大小,然后观察是否启用compressed oops。

举例来说,如果在mac os上启动一个java 1.7,同时将heap size设置为32600mb,那么compressed oops是会开启的;但是如果设置为32766m,compressed oops就不会开启。相反的是,使用jdk 1.8的化,分配32766m,compressed oops是会开启的,设置为32767m,就不会开启。所以说这个东西不是固定的。根据不同的操作系统以及jvm版本而定。

在es启动日志中,我们可以查看compressed oops是否开启,比如下面的字样:[2015-12-16 13:53:33,417][INFO ][env] [Illyana Rasputin] heap size [989.8mb], compressed ordinary object pointers [true]。

5、对于有1TB内存的超大内存机器该如何分配?

如果机器是一台超级服务器,内存资源甚至达到了1TB,或者512G,128G,该怎么办?首先es官方是建议避免用这种超级服务器来部署es集群的,但是如果我们只有这种机器可以用的话,我们要考虑以下几点:

(1)我们是否在做大量的全文检索?考虑一下分配4~32G的内存给es进程,同时给lucene留下其余所有的内存用来做os filesystem cache。所有的剩余的内存都会用来cache segment file,而且可以提供非常高性能的搜索,几乎所有的数据都是可以在内存中缓存的,es集群的性能会非常高

(2)是否在做大量的排序或者聚合操作?聚合操作是不是针对数字、日期或者未分词的string?如果是的化,那么还是给es 4~32G的内存即可,其他的留给es filesystem cache,可以将聚合好用的正排索引,doc values放在os cache中

(3)如果在针对分词的string做大量的排序或聚合操作?如果是的化,那么就需要使用fielddata,这就得给jvm heap分配更大的内存空间。此时不建议运行一个节点在机器上,而是运行多个节点在一台机器上,那么如果我们的服务器有128G的内存,可以运行两个es节点,然后每个节点分配32G的内存,剩下64G留给os cache。如果在一台机器上运行多个es node,建议设置:cluster.routing.allocation.same_shard.host: true。这会避免在同一台物理机上分配一个primary shard和它的replica shard。

6、swapping

如果频繁的将es进程的内存swap到磁盘上,会对服务性能造成很大影响,内存中的操作都是要求快速完成的,如果需要将内存页的数据从磁盘swap回main memory的化,性能会有多差。如果内存被swap到了磁盘,那么100微秒的操作会瞬间变成10毫秒,那么如果是大量的这种内存操作呢?这会导致性能急剧下降。因此通常建议彻底关闭机器上的swap,swapoff -a,如果要永久性关闭,需要在/etc/fstab中配置

如果没法完全关闭swap,那么可以尝试调低swappiness至,这个值是控制os会如何将内存swap到磁盘的。这会在正常情况下阻止swap,但是在紧急情况下,还是会swap。一般用sysctl来设置,vm.swappiness = 1。如果swappiness也不能设置,那么就需要启用mlockall,这就可以让我们的jvm lock住自己的内存不被swap到磁盘上去,在elasticsearch.yml中可以设置:bootstrap.mlockall: true。


网站公告

今日签到

点亮在社区的每一天
去签到