基础概念:
Netty为了提高网络的吞吐量,在业务层与socket之间又增加了一个ChannelOutboundBuffer。在我们调用channel.write的时候,所有写出的数据其实并没有写到socket,而是先写到ChannelOutboundBuffer。当调用channel.flush的时候才真正的向socket写出。因为这中间有一个buffer,就存在速率匹配了,而且这个buffer还是无界的。也就是你如果没有控制channel.write的速度,会有大量的数据在这个buffer里堆积,而且如果碰到socket又『写不出』数据的时候,很有可能的结果就是资源耗尽。而且这里让这个事情更严重的是ChannelOutboundBuffer很多时候我们放到里面的是DirectByteBuffer,什么意思呢,意思是这些内存是放在GC Heap之外。如果我们仅仅是监控GC的话还监控不出来这个隐患.
CPU占用已经到100%了,而load也非常高的时候,cpu很有可能来不及处理网络事件,这个时候send buffer就有可能会堆满,这就导致socket写不出数据了。
相同问题排查 解决方案:
https://www.jianshu.com/p/13f72e0395c8
上文的现象可以描述为:
(1) 不断进行full GC,导致CPU打满,每次GC后old Gen的内存占用率仍然不下降。用free命令发现剩余物理内存总量非常低
(2) Socket无法发出包,使用wireshark抓包发现出现状态TCP windows update,说明缓冲区已满。(4) 进程挂掉,查看/var/log/message 日志,由于oom发现进程被操作系统干掉:
Aug 19 08:32:38 mybank-ant kernel: : [6176841.247990] Out of memory: Kill process 24673 (java) score 946 or sacrifice childAug 19 08:32:38 mybank-ant kernel: : [6176841.249016] Killed process 24673, UID 1801, (java) total-vm:5120504kB, anon-rss:3703788kB, file-rss:484kB
原因解析:
1. WirteAndFlush的写入速度远远大于TCP发送缓冲区的消化速度,TCP缓冲区满。大量对象在Channelbuffer缓冲区内,其对象为(DefaultChannelPromise、WriteAndFlushTask、Recycler),这些创建的对象经过若干次young GC后进入老年代,由于尚在使用中,不能进行垃圾回收,导致full GC频繁。
2. 频繁Full GC导致cpu打到100%,CPU无法处理网络请求,从而导致虽然TCP缓冲区内有数据,但是数据无法发出。
3. 使用DirectByteBuffer使用堆外内存,在不断申请堆外内存后,导致物理内存占用率急剧增加,当物理内存不足时,开始吃缓存(swap),最后由于oom,被操作系统当做bad process 干掉。
if(channelHandlerContext.channel().isWritable()){
channelHandlerContext.writeAndFlush(commandOut).addListener(future -> {
if (!future.isSuccess()) {
logger.warn("unexpected push. msg:{} fail:{}", msg, future.cause().getMessage());
}
});
}else{
try {
channelHandlerContext.writeAndFlush(commandOut).sync();
logger.info("publish macdonaldMsg,sended. remoteAddress:[{}], msg:[{}]", channelHandlerContext.channel().remoteAddress(), msg);
} catch (InterruptedException e) {
logger.info("write and flush msg exception. msg:[{}]",msg,e);
}
}