我们模拟这么一个场景,客户端和服务端都使用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异常
image.png我们这里通过MAT工具分析下堆空间信息
image.png导入文件.(至于怎么使用MAT工具这里不做介绍)
image.png image.png image.png我们会发现taskQueue中有非常多的Task,这是因为向对端写数据的操作必须是IO线程来完成,业务线程只能把它的需求封装成一个Task放在IO线程的任务队列中.
image.png// 源码位置: 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("达到高水位,暂时不可写");
}
}
}
}
以上代码也只是作为一个思路.
公众号 Netty历险记
网友评论