1.背景:ES 作为缓存存储全量商品数据,更新机制有两种,一是数据变更消息更新,另一种是定时任务全
量更新(兜底方案),项目上线一段时间后,全量更新会导致 ES 机器 OOM 导致宕机。
问题:ES 节点机器为 8C 16G 虚拟机,三台组成集群,分配给 ES 内存按照 80%方案分配为 12G,商品数据
约 5G 左右。全量更新数据时因 ES 索引进行段合并并不占用 ES 分配的内存而是用的剩余的 4G 内存进行。
项目上线初期数据量并没有超过 4G,业务增加,数据增多,进行索引段合并时会把索引放进内存进行,导
致 OOM。
解决方案:将内存空间重新分配,ES 节点占用 6G,当进行数据批量更新时,数据不会超过 6G 即可,这样虽
然会导致 ES 更新速度变慢,但是此时操作时兜底补漏,不需要考虑速度问题。

2.背景:ES 商品数据批量更新时导致 OOM
问题: ES 6.5.4 版本在进行 bulk 批量更新或新增机制时,会默认记录一条 log,这个 log 对象在单次执行
完后并没有被 gc 回收掉,而是继续存在,是因为 log 对象被一个 MutableLogEvent 在引用,而这个对象被
一个 ThreadLocal(强引用)存放起来了。所以在这里进行多次 bulk 操作后 导致内存泄漏。注意 ES 中的
log 是 log4,而 log4 这么设计是描述为了减少日志的反复创建对象,减少 GC 压力。但是 Log4 在 WEB 环境
下会默认禁用掉 ThreadLocal 类型变量,但是 ES 并不是 WEB 应用。
解决方案:添加配置,禁用掉 ES 的 log4j 的 ThreadLocal 即可。

3.背景:ES 写入压力大资源分配问题
问题:es7.9.0 版本写入大于读取时,导致 bulk 操作内存溢出,导致集群宕机
解决方案:为集群内存设置阈值,设置读取写入内存分配比例,强制锁定内存,如果在不可解决再升级机
器配置,设置阈值的坏处会导致后进入的执行操作不可再写入,但是集群不会被拖垮,在一定情况下是可
以接受的。

 

 

背景: Es进行多次批量 bulk操作后导致内存溢出

Eclipse MAT 使用查看及分析内存使用情况

es批量更新 java es批量更新,内存溢出_强引用

 

1. bulk线程的 ThreadLocalMap里会保存一个 log4j的 MultableLogEvent 对象。

2. MutablelogEvent 对象引用了 log4j 的 ParameterizedMessage对象。

3. ParamenterizedMessage引用了bulkShardRequest对象

4. bulkShardRequest引用了多次的 BulkitemRequest对象(所有请求bulk操作都会记录)

这样看下来,是log4j 的 logevent 对一个大的 bulk请求对象有强引用而导致无法被垃圾回收掉,产生内存泄漏。

源码中找到bulk相关位置

es批量更新 java es批量更新,内存溢出_强引用_02

 

这里可以看到 ParamenterizedMessage实例化过程中,request作为一个参数传入了。这里的 request是一个 bulkShardRequest 对象,保存的是要写入到 一个shard的一批bulk item request。这样以来,一个批次写入的请求数量越多,这个对象 retain的内存就越多。可问题是,为什么 logger.debug() 调用完毕以后,这个引用不会被释放?

通过和之前 MAT上的 dominator tree仔细对比,可以看到 ParameterizedMessage 之所以无法释放,是因为被一个 MutableLogEvent在引用,而这个 MutableLogEvent被作为一个 ThreadLocal 存放起来了。由于 ES 的 Bulk Thread Pool 是 fix size的,也就是预先创建好的,不会销毁和再创建。那么这些 MutableLogEvent对象由于是 Thread Local 的, 只要线程没有销毁,就会对该线程实例一直全局存在,并且其还会一直引用最后一次处理过的 ParameterizedMessage。 所以在 ES 记录 bulk exception 这种比较大的请求下,整个 request对象会被 Thread Loca 变量一直强引用 无法释放,产生大量的内存泄露。

再继续挖一下 log4j的源码,发现 MutableLogEvent是在 org.apache.logging.log4j.ccore.impl.ReusableLogEventFactory里作为 Thread Local创建的。

es批量更新 java es批量更新,内存溢出_内存泄漏_03

 

而 org.apache.logging.log4j.core.config.LoggerConfig则根据一个常数 MNABLE_THREADLOCALS 的值来决定用哪个 LogEventFactory。

es批量更新 java es批量更新,内存溢出_内存泄漏_04

 

继续深挖,在org.apache.logging.log4j.util.Constants里看到,log4j会根据运行环境判断是否是 WEB 应用,如果不是,就从系统参数 log4j2.enable.threadlocal 读取这个常量,如果没有设置,则默认值是 true。

es批量更新 java es批量更新,内存溢出_内存泄漏_05

 

由于ES并不是一个 web应用,导致 log4j选择使用了 ReusableLogEventFactory,而因使用了 thread_local来创建 MutableLogEvent对象,最终在ES记录 bulk exception 这个特殊场景下产生非常显著的内存泄漏。

再有一个问题,为何 log4j要将 logevent做为 Thread Local创建?跑到 log4j 的官网去扒了一下文档,原因是为了减少记录日志过程中的反复创建的对象的数量,减轻 GC 压力从而提高性能, log4j有很多地方使用了thread_local来重用变量。但使用 thread_local字段装载非 JDK类,可能会导致内存泄漏问题,特别是对于 WEB 应用。因此才会在启动时判断运行环境,对于 WEB 应用会禁用 thread_local 类型的变量。

在 ES的 JVM 配置文件 jvm.options里,添加一个 log4j的系统变量 -Dlog4j2.enable.threadlocals=false,禁用掉 thread local 即可。可有效避开这个内存泄漏问题。