netty堆外内存使用过多导致ES节点oom问题分析

最近在6.3版本的ES上出现了堆外内存使用过高,导致节点内存被打满,引发oom-killer问题。针对这个问题,我深入进行分析,找到了问题原因。

问题描述

问题出现在协调节点,由于协调节点无需使用pagecache,所以我们会把协调节点的堆内存配置配置成节点内存的2/3左右。
出现问题的是一个16cpu 32g的节点上,我们堆内存配置成21g。
业务在凌晨有大量写入,写入时可以看到节点内存在不断上涨。

image.png

top看内存占用,可以看到es进程占用了28G内存:

image.png

问题分析

oom-killer触发的原因,分析下来主要是以下原因:

  • 客户协调节点16c 32g规格,es设置的堆内存为21g,docker节点内存在29.5g左右,这样留给堆外的内存在8.5g,es未设置MaxDirectMemorySize,这样默认值为跟堆内存一样的21g,超过了节点可用的堆外内存。
  • netty使用了DirectByteBuffer分配堆外内存处理网络请求,而且有些cache机制,在释放DirectByteBuffer时,未清掉DirectByteBuffer内存,由于这些对象在old,这依赖触发CMS回收DirectByteBuffer对象,但是业务节点GC压力很小,一直无法触发CMS gc,导致DirectByteBuffer对象越堆越多,最终触发了oom-killer。

关于netty的问题,接下来展开说明下:
jmap histo查看full gc前后的对比情况:
full gc前:

 num     #instances         #bytes  class name
----------------------------------------------
   1:        584249     4372220968  [B
   2:        182391      160642272  [I
   3:        843882       60280192  [C
   4:        222069       40572000  [Ljava.lang.Object;
   5:        803940       19294560  java.lang.String
   6:         63530       11689520  com.fasterxml.jackson.core.json.UTF8StreamJsonParser
   7:        396785       10656352  [Ljava.lang.String;
   8:        190930        9164640  com.fasterxml.jackson.core.json.JsonReadContext
   9:        212105        8484200  io.netty.buffer.UnpooledSlicedByteBuf
  10:        150361        7217328  java.nio.HeapByteBuffer
  11:        220993        7071776  io.netty.buffer.PoolThreadCache$MemoryRegionCache$Entry
  12:         66563        6922552  org.elasticsearch.action.index.IndexRequest
  13:        205541        6577312  java.util.HashMap$Node
  14:        190930        6109760  com.fasterxml.jackson.core.json.DupDetector
  15:         64268        5141440  io.netty.buffer.PooledHeapByteBuf
  16:         64500        4128000  org.elasticsearch.action.index.IndexResponse
  17:         63536        4066304  com.fasterxml.jackson.core.sym.ByteQuadsCanonicalizer
  18:         41378        3976016  [Ljava.util.HashMap$Node;
  19:          7488        3953664  io.netty.util.internal.shaded.org.jctools.queues.atomic.MpscAtomicArrayQueue
  20:         63697        3567032  com.fasterxml.jackson.core.io.IOContext
  21:         63533        3557848  com.fasterxml.jackson.core.util.TextBuffer
  22:         67201        3225648  java.util.HashMap
  23:         64992        2599680  io.netty.buffer.UnpooledDuplicatedByteBuf
  24:         63530        2541200  com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper
  25:         78830        2522560  io.netty.handler.codec.CodecOutputList
  26:         37953        2428992  java.nio.DirectByteBuffer
  27:         30298        2423840  io.netty.buffer.PooledDirectByteBuf
  28:           409        2317712  [Lio.netty.buffer.PoolSubpage;
  29:         72332        2314624  java.util.AbstractList$Itr
  30:         68912        2205184  org.elasticsearch.transport.netty4.ByteBufBytesReference
  31:         36514        2106728  [Ljava.nio.ByteBuffer;
  32:         64992        2079744  org.elasticsearch.transport.netty4.ByteBufStreamInput
  33:         64500        2064000  org.elasticsearch.action.bulk.BulkItemResponse
  34:         64166        2053312  org.elasticsearch.cluster.metadata.IndexNameExpressionResolver$Context
......
7221:             1             16  sun.util.resources.LocaleData$LocaleDataResourceBundleControl
Total       7752044     4860938104

full gc后:

 num     #instances         #bytes  class name
----------------------------------------------
   1:         49511     1163477560  [B
   2:         28751       15856176  [Ljava.lang.Object;
   3:        128493       12294800  [C
   4:        143253        4584096  java.util.HashMap$Node
   5:          7488        3953664  io.netty.util.internal.shaded.org.jctools.queues.atomic.MpscAtomicArrayQueue
   6:        117522        2820528  java.lang.String
   7:         40173        1928304  java.util.HashMap
   8:         15662        1879024  [Ljava.util.HashMap$Node;
   9:         15463        1698760  java.lang.Class
  10:         12616        1482040  [I
  11:         43724        1399168  java.util.concurrent.ConcurrentHashMap$Node
  12:         34622        1107904  java.util.Collections$UnmodifiableMap
  13:         40684         976416  org.elasticsearch.painless.Definition$MethodKey
  14:           618         957008  [Ljava.nio.ByteBuffer;
  15:          3873         707568  [J
  16:           203         626864  [Lio.netty.buffer.PoolSubpage;
  17:         35759         572144  java.lang.Object
  18:          9786         548016  org.elasticsearch.painless.Definition$Struct
  19:          8335         466760  java.lang.invoke.MemberName
  20:         10342         413680  java.util.LinkedHashMap$Entry
  21:          6795         326728  [Ljava.lang.String;
  22:          1373         310984  [Z
  23:           202         306728  [Ljava.util.concurrent.ConcurrentHashMap$Node;
  24:          4575         292800  io.netty.buffer.PoolSubpage
  25:          8694         278208  java.util.concurrent.atomic.LongAdder
  26:         10519         252456  java.util.ArrayList
  27:          6113         244520  java.lang.invoke.MethodType
  28:          7491         239712  java.util.Hashtable$Entry
  29:          6912         221184  io.netty.buffer.PoolThreadCache$SubPageMemoryRegionCache
  30:          4900         196000  org.elasticsearch.painless.Definition$Type
  31:          6122         195904  java.lang.invoke.MethodType$ConcurrentWeakInternSet$WeakEntry
  32:          8119         194856  java.util.Collections$UnmodifiableRandomAccessList
  33:          4600         184000  java.lang.ref.SoftReference
  34:          9887         158192  java.util.HashMap$EntrySet
  35:          4900         156800  java.lang.invoke.DirectMethodHandle
  36:          4890         156480  io.netty.buffer.PoolThreadCache$MemoryRegionCache$Entry
  37:          4818         154176  org.objectweb.asm.Type
  38:          1233         146888  [Ljava.util.Hashtable$Entry;
6641:             1             16  sun.util.resources.LocaleData$LocaleDataResourceBundleControl
Total       1095045     1229886024

full gc后,io.netty.buffer.PooledDirectByteBuf对象直接清空了。java.nio.DirectByteBuffer对象也从从之前30000多,到gc后基本都没了。es进程的内存也直接降低到22GB。

问题复现

通过构造大的bulk请求(84MB),连续nohup执行bulk请求2次,在16c 32g节点上复现了该问题。
在bulk小于10MB之后,再去验证,就发现堆外内存不怎么上涨。
我们最新的7.10.0也尝试复现,发现运行脚本后,堆外内存没有太多变化。
ES在7.2版本之后开始默认添加了MaxDirectMemorySize,默认MaxDirectMemorySize值是jvm堆内存的一半。具体PR见:https://github.com/elastic/elasticsearch/pull/42006

所以尝试在6.3.2加上MaxDirectMemorySize进行验证,加上MaxDirectMemorySize=7g后,再运行复现脚本,节点在堆外内存打满后,会触发一次full gc,内存就顺利降低下来了。

image.png

对比6.3.2和7.10.0的netty参数配置,发现7.10.0多了一个-Dio.netty.allocator.numDirectArenas=0参数,在6.3.2加上该参数,但是发现堆外内存仍然会持续上涨。这块后面结合netty原理再展开说明。

根因分析

netty内存分配会调用这个方法:io.netty.channel.nio.AbstractNioChannel的newDirectBuffer:

    protected final ByteBuf newDirectBuffer(ByteBuf buf) {
        final int readableBytes = buf.readableBytes();
        if (readableBytes == 0) {
            ReferenceCountUtil.safeRelease(buf);
            return Unpooled.EMPTY_BUFFER;
        }

        final ByteBufAllocator alloc = alloc();
        if (alloc.isDirectBufferPooled()) {
            ByteBuf directBuf = alloc.directBuffer(readableBytes);
            directBuf.writeBytes(buf, buf.readerIndex(), readableBytes);
            ReferenceCountUtil.safeRelease(buf);
            return directBuf;
        }

        final ByteBuf directBuf = ByteBufUtil.threadLocalDirectBuffer();
        if (directBuf != null) {
            directBuf.writeBytes(buf, buf.readerIndex(), readableBytes);
            ReferenceCountUtil.safeRelease(buf);
            return directBuf;
        }

        // Allocating and deallocating an unpooled direct buffer is very expensive; give up.
        return buf;
    }

这部分代码在6.3.2和7.10.0使用的netty版本是一样的。这里6.3.2由于没设置-Dio.netty.allocator.numDirectArenas=0 ,所以alloc.isDirectBufferPooled()为true,会走堆外内存分配。

然后6.3.2加上-Dio.netty.allocator.numDirectArenas=0没生效的原因跟ByteBufUtil.threadLocalDirectBuffer()的结果在6.3.2和7.10.0不一样导致的。以下是threadLocalDirectBuffer()代码:

    public static ByteBuf threadLocalDirectBuffer() {
        if (THREAD_LOCAL_BUFFER_SIZE <= 0) {
            return null;
        }

        if (PlatformDependent.hasUnsafe()) {
            return ThreadLocalUnsafeDirectByteBuf.newInstance();
        } else {
            return ThreadLocalDirectByteBuf.newInstance();
        }
    }

其中THREAD_LOCAL_BUFFER_SIZE的默认值,在6.3.2是64*1024,7.10.0默认是0。所以7.10.0不会使用threadLocalDirectBuffer,newDirectBuffer()方法返回的是原buf对象,即堆内的对象。而6.3.2会使用threadLocalDirectBuffer,这个会申请堆外内存。

netty的这个修改是在4.1.22.Final版本,刚好是在6.3.2版本之后,具体PR:https://github.com/netty/netty/pull/7704

6.3.2:

image.png

7.10.0:

image.png

7.10这样配置,会导致无法使用到堆外内存进行网络请求的处理,ES在最新版本又去掉了-Dio.netty.allocator.numDirectArenas=0 的配置,看这个配置,本来在这个PR中优化掉了:https://github.com/elastic/elasticsearch/pull/48310,这个发布在7.6版本,但是由于bug,在8.2版本才修复:https://github.com/elastic/elasticsearch/pull/76593 ,所以目前7.10的版本未使用到堆外内存。

6.3.2加上-Dio.netty.threadLocalDirectBufferSize=0 配置后,再验证复现脚本,实际效果跟7.10.0是一样的,堆外并未出现明显上涨。

6.3.2版本会进入ByteBuf directBuf = alloc.directBuffer(readableBytes);的逻辑,这里会分配出PooledDirectByteBuf对象。

然后由于ES加上了配置“-Dio.netty.recycler.maxCapacityPerThread=0”,导致每次都会new出一个新的PooledDirectByteBuf,相关代码如下:

final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> {

    private static final Recycler<PooledDirectByteBuf> RECYCLER = new Recycler<PooledDirectByteBuf>() {
        @Override
        protected PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
            return new PooledDirectByteBuf(handle, 0);
        }
    };

    static PooledDirectByteBuf newInstance(int maxCapacity) {
        PooledDirectByteBuf buf = RECYCLER.get();
        buf.reuse(maxCapacity);
        return buf;
    }
   ......
public abstract class Recycler<T> {

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(Recycler.class);

    @SuppressWarnings("rawtypes")
    private static final Handle NOOP_HANDLE = new Handle() {
        @Override
        public void recycle(Object object) {
            // NOOP
        }
    };
    private static final AtomicInteger ID_GENERATOR = new AtomicInteger(Integer.MIN_VALUE);
    private static final int OWN_THREAD_ID = ID_GENERATOR.getAndIncrement();
    private static final int DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD = 32768; // Use 32k instances as default.
    private static final int DEFAULT_MAX_CAPACITY_PER_THREAD;
    private static final int INITIAL_CAPACITY;
    private static final int MAX_SHARED_CAPACITY_FACTOR;
    private static final int MAX_DELAYED_QUEUES_PER_THREAD;
    private static final int LINK_CAPACITY;
    private static final int RATIO;

    static {
        // In the future, we might have different maxCapacity for different object types.
        // e.g. io.netty.recycler.maxCapacity.writeTask
        //      io.netty.recycler.maxCapacity.outboundBuffer
        int maxCapacityPerThread = SystemPropertyUtil.getInt("io.netty.recycler.maxCapacityPerThread",
                SystemPropertyUtil.getInt("io.netty.recycler.maxCapacity", DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD));
        if (maxCapacityPerThread < 0) {
            maxCapacityPerThread = DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD;
        }

        DEFAULT_MAX_CAPACITY_PER_THREAD = maxCapacityPerThread;
......
        }

    @SuppressWarnings("unchecked")
    public final T get() {
        if (maxCapacityPerThread == 0) {
            return newObject((Handle<T>) NOOP_HANDLE);
        }
        Stack<T> stack = threadLocal.get();
        DefaultHandle<T> handle = stack.pop();
        if (handle == null) {
            handle = stack.newHandle();
            handle.value = newObject(handle);
        }
        return (T) handle.value;
    }
......
}

如果bulk小的时候,正常PooledDirectByteBuf对象就被young gc掉了,但是在bulk大的时候,PooledDirectByteBuf会进入old区,这个就依赖CMS gc清除。随着PooledDirectByteBuf对象越来越多,它引用的DirectByteBuffer对象也越来越多,最终造成了堆外内存被打满。
大bulk有PooledDirectByteBuf进入old区,目前看可能跟http pipeline机制有关,或者netty到等到整个bulk请求接收完才能处理,相对时间变长,young gc处理不过来,有部分对象就进入了old区。
所以之前问题分析时,手动执行full gc后,可以看到PooledDirectByteBuf对象都被情况了,堆外内存也直接下降了。
加上MaxDirectMemorySize后,内存在上涨到堆外内存上限后,就会触发full gc。清掉这部分堆外内存。

总结

总结下来,6.3的oom问题,主要来自于netty使用的堆外内存,由于进入了old区,又无法触发cms gc,所以堆外内存越对越多。需要加上MaxDirectMemorySize控制下。
最新版本加上了MaxDirectMemorySize,且分配了一半的堆内存,所以节点jvm内存不要设置超过物理内存的2/3。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容