记一次Netty OOM排错过程及源码分析

系统协议:

架构在UDP协议上的MQTT协议。

内存溢出可能出现的情况:

1. 假设仅在接收客户端报文的时候会申请堆外内存,每个报文为1KB,程序最大的堆外内存为512MB,那么最大支持524288个客户端同时发包,超出可能会导致OOM。

2. 假设程序最大的Direct Memory(可由-XX:MaxDirectMemorySize参数决定)为512MB,此时程序已用的堆外内存为100MB,操作系统仅剩下101MB,那么在这部分已使用的堆外内存没释放之前,去申请新的内存有可能会发生OOM。

3. 程序使用了Direct Memory,但是没有释放。

排查过程

一、问题定位

记一次Netty OOM排错过程及源码分析

根据上图的堆栈信息可以看出,程序在申请16777216个字节的堆外内存时报错。结合系统此时的客户端连接数(几百个),可以得知,基本为程序的BUG(申请了堆外内存但是没有成功释放

二、尝试解决

Review了一遍代码,定位到有一处代码:

ByteBuf b = buffer.readRetainedSlice(bytesRemainingInVariablePart);

分析了readRetainedSilice方法的源码:

public ByteBuf readRetainedSlice(int length) {
        ByteBuf slice = retainedSlice(readerIndex, length);
        readerIndex += length;
        return slice;
}

public ByteBuf retainedSlice(int index, int length) {
	    //可以清楚看到,这个方法做了一次retain操作。
        return slice(index, length).retain();
}


由于在使用这个ByteBuf做了一次retain操作,但是在业务线程末端没有对这部分进行释放,随着程序运行时间推移,出现了内存溢出,即

当前内存+申请内存 >io.netty.util.internal.PlatformDependent.DIRECT_MEMORY_LIMIT

内存溢出错误的源码如下:

private static void incrementMemoryCounter(int capacity) {
    if (DIRECT_MEMORY_COUNTER != null) {
        for (;;) {
            long usedMemory = DIRECT_MEMORY_COUNTER.get();
            long newUsedMemory = usedMemory + capacity;
            if (newUsedMemory > DIRECT_MEMORY_LIMIT) {
                throw new OutOfDirectMemoryError("failed to allocate " + capacity + " byte(s) of direct memory (used: " + usedMemory + ", max: " + DIRECT_MEMORY_LIMIT + ')');
            }
            if (DIRECT_MEMORY_COUNTER.compareAndSet(usedMemory, newUsedMemory)) {
                break;
            }
        }
    }
}
三、运行,验证修复结果
遗憾的是,OOM的问题再次出现
四、再次尝试解决

经过分析,在程序中大量使用了ByteBuf的readBytes(int i)方法,源码如下:

@Override
public ByteBuf readBytes(int length) {
    checkReadableBytes(length);
    if (length == 0) {
        return Unpooled.EMPTY_BUFFER;
    }

    ByteBuf buf = alloc().buffer(length, maxCapacity);
    buf.writeBytes(this, readerIndex, length);
    readerIndex += length;
    return buf;
}

@Override
public ByteBuf buffer(int initialCapacity, int maxCapacity) {
    if (directByDefault) { 
    //只要没有设置 -Dio.netty.noPreferDirect=true 
    //并且运行在标准 Oracle JVM(sun.misc.Unsafe存在)中,
    //就会优先使用 Direct Memory,
    //当然还有一个前提是分配了一定数量的Direct Memory
        return directBuffer(initialCapacity, maxCapacity);
    }
    return heapBuffer(initialCapacity, maxCapacity);
}

发现这个方法在会返回一个UnpooledUnsafeNoCleanerDirectByteBuf,
将所有的readbytes(int i)方法替换成readSlice(int i).

五、再次运行,验证修复结果
此时发现OOM问题已经不再出现

正确用法

1. 在IO线程中使用Direct Memory,提高处理效率
2. 在业务线程中尽量使用Heap Memory
3. 申请了堆外内存,谁最后使用就由谁负责释放