使用Netty模擬發生OOM

書唐瑞發表於2020-11-21

我們模擬這麼一個場景,客戶端和服務端都使用Netty進行通訊,客戶端無限迴圈地向服務端傳送資料,過了一會客戶端就會出現OOM,我們分析OOM產生的原因,給我們排查線上問題提供一個思路和角度.

以下所有的分析都是基於以上描述的場景

本文適合對Netty要有一定的基礎
程式碼放在了github上
在這裡插入圖片描述
設定的客戶端虛擬機器引數

-XX:MetaspaceSize=18M 
-XX:MaxMetaspaceSize=18M  
-XX:+HeapDumpOnOutOfMemoryError  
-XX:HeapDumpPath=D:\heapdump.hprof 
-Xmx1000M 
-XX:+PrintGC  
-XX:+PrintGCDetails 

為了講解方便,我把一些主要程式碼貼上如下
客戶端程式碼

EventLoopGroup group = new NioEventLoopGroup();
EventLoopGroup businessGroup = new NioEventLoopGroup(8);
Bootstrap bootstrap = new Bootstrap();

bootstrap.group(group)
        .channel(NioSocketChannel.class)
        .handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) {
                ChannelPipeline channelPipeline = ch.pipeline();
                channelPipeline.addLast(new StringDecoder());// Netty自帶的字串解碼器
                channelPipeline.addLast(new StringEncoder());// Netty自帶的字串編碼器
                channelPipeline.addLast(businessGroup, new ClientHandler());// 自定義處理器
            }
        });

---

public class ClientHandler extends SimpleChannelInboundHandler<String> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
		// Channel啟用之後,便無限迴圈地向服務端傳送資料
        int i = 0;
        for (;;) {
            ctx.writeAndFlush("這個是客戶端傳送的第" + ++i + "個訊息");
        }
    }
}

由於服務端只是接收資料,沒有特殊地方,這裡就不貼上程式碼了.

先啟動服務端,在啟動客戶端.
客戶端就會連線服務端,通道建立之後,業務執行緒就會無限迴圈地向服務端傳送資料.
你也可以通過JDK自帶的工具觀察記憶體的變化.

在這裡插入圖片描述
當程式執行一會之後,就會出現OOM異常

在這裡插入圖片描述

我們這裡通過MAT工具分析下堆空間資訊

在這裡插入圖片描述

匯入檔案.(至於怎麼使用MAT工具這裡不做介紹)

在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
我們會發現taskQueue中有非常多的Task,這是因為向對端寫資料的操作必須是IO執行緒來完成,業務執行緒只能把它的需求封裝成一個Task放在IO執行緒的任務佇列中.

在這裡插入圖片描述

// 原始碼位置: io.netty.channel.AbstractChannelHandlerContext#write(java.lang.Object, boolean, io.netty.channel.ChannelPromise)

private void write(Object msg, boolean flush, ChannelPromise promise) {
        
	final AbstractChannelHandlerContext next = findContextOutbound(flush ?
                (MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
    final Object m = pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {// 判斷當前執行緒是否是IO執行緒
        if (flush) {
            next.invokeWriteAndFlush(m, promise);
        } else {
            next.invokeWrite(m, promise);
        }
    } else {
        final AbstractWriteTask task;
        if (flush) {
        	// 由於當前執行緒不是IO執行緒,所以只能封裝成一個Task,放入到佇列中
            task = WriteAndFlushTask.newInstance(next, m, promise);
        }  else {
            task = WriteTask.newInstance(next, m, promise);
        }
        // Task​放入到佇列
        if (!safeExecute(executor, task, promise, m)) {
            task.cancel();
        }
    }        
}

由於業務執行緒是無限迴圈地寫入資料,導致佇列中的Task一直增多,最後導致OOM
一方面可能是服務端處理的比較慢,導致服務端TCP緩衝區滿了,那麼客戶端的TCP緩衝區也會被寫滿,Netty就不能成功的寫入TCP緩衝區,那麼資料只能放在佇列中,最後導致OOM.(當然我們這裡不是因為這個原因,我們的服務端只是接收資料,沒有任何業務耗時操作)
也有可能是網路等原因,導致客戶端IO執行緒傳送的比較慢(業務執行緒生成的資料比較快).
或者也有其他的原因.

Netty給我們提供了高低水位機制,當我們業務執行緒向Netty寫入的資料過多的時候,一旦達到了高水位值(這個值我們可以設定),Netty就會設定Channel不可能.但是這裡注意了,這裡只是設定成不可能,我們還是依然可以向Netty中寫入資料.但是如果我們忽略它,有可能造成上面這種OOM情況.

因此我們可以基於Netty提供的這種機制,控制我們的業務執行緒向Netty寫入資料的速率.如果達到了高水位值,我們就暫時不要向Netty中寫入資料,也就不會導致OOM發生.

我們改寫客戶端程式碼

public class ClientHandler extends SimpleChannelInboundHandler<String> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

        // 設定高水位值(當然不一定非要在此處設定)
        ctx.channel().config().setWriteBufferHighWaterMark(20 * 1024 * 1024);

        int i = 0;

        for (;;) {
        	// 通道可寫
            if (ctx.channel().isWritable()) {
                ctx.writeAndFlush("這個是客戶端傳送的第" + ++i + "個訊息");
            } else {// 通道不可寫
                System.out.println("達到高水位,暫時不可寫");
            }
            
        }
    }
}

以上程式碼也只是作為一個思路.


個人站點
語雀

公眾號

微信公眾號

相關文章