netty学习系列九:记一次压测异常排查

一、背景描述#

基于Netty做了一个消息推送服务,将消息总线中的消息推送给与消息推送服务器进行socket连接的client。

netty学习系列九:记一次压测异常排查_第1张图片
推送服务描述

二、问题描述#

进行压力测试,当消息发送速率达到5w条/秒时:
1、 JVM进程出现频繁FullGC,且每次FullGC并不能回收年老代堆内存空间;
2、 top命令查看服务器资源使用情况,服务器内存被打满;
3、负责FullGC的线程不断循环执行导致CPU100%;

三、问题排查#

1、查询占用CPU较多线程的堆栈信息####

排查步骤

  • 1)jps或ps命令获取jvm进程的pid;
  • 2)top -H p pid查询jvm进程中各个线程占用的系统资源;
  • 3)jstack pid获取jvm进程的堆栈信息->stack.txt;
  • 4)将占用CPU资源较多线程的tid转换为十六进制,从堆栈信息文件stack.txt中匹配对应堆栈信息;

截取几个堆栈信息如下

at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
at io.netty.channel.nio.NioEventLoop.select(NioEventLoop.java:622)
at sun.nio.ch.FileDispatcherImpl.write0(Native Method)
at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:47)
- locked <0x0000000080d00f38> (a java.lang.Object)
at io.netty.channel.socket.nio.NioSocketChannel.doWrite(NioSocketChannel.java:270)
at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:688)
at io.netty.channel.ChannelOutboundBuffer$Entry.newInstance(ChannelOutboundBuffer.java:693)
at io.netty.channel.AbstractChannelHandlerContext.invokeWrite(AbstractChannelHandlerContext.java:658)
at io.netty.handler.stream.ChunkedWriteHandler.doFlush(ChunkedWriteHandler.java:304)
at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:688)
at sun.misc.Unsafe.freeMemory(Native Method)
at java.nio.DirectByteBuffer$Deallocator.run(DirectByteBuffer.java:94)
at io.netty.util.internal.Cleaner0.freeDirectBuffer(Cleaner0.java:66)
at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:688)

分析堆栈信息,基本是Netty的IO线程进行select、write、flush和freeDirectBuffer的操作。

2、jvm堆内存使用分析####

jstat -gcutil pid 1s 命令,查询JVM进程GC情况,发现Full GC短期大量执行,且无法回收年老代内存空间。

jmap -histo pid > histo.txt命令,打印JVM进程中每个class的实例数目,内存占用,类全名信息,如下:

  • 正常情况刚Yong GC完时的内存占用
   1:        457482       35,387,296  [C
   2:         16678       13,773,280  [I
   3:         63772        9,237,488  [B
   4:        122,459        7,837,376  java.nio.DirectByteBuffer
   5:         53,091        7,193,480  [Ljava.lang.Object;
   6:         90076        6485472  io.netty.channel.DefaultChannelHandlerContext
   7:         59706        4298832  io.netty.buffer.UnpooledUnsafeDirectByteBuf
   8:        122777        3928864  java.util.HashMap$Node
   9:        120320        2887680  java.lang.String
  10:         43714        2447984  io.netty.buffer.UnpooledHeapByteBuf
  Total       2360002      138,412,016
  • 正常情况发生Yong GC前的内存占用
   1:       5800859      438,101,408  [C
   2:        717064      105,584,984  [B
   3:       1375972       88,062,208  java.nio.DirectByteBuffer
   4:        675654       48,647,088  io.netty.buffer.UnpooledUnsafeDirectByteBuf
   5:       1526018       36,624,432  java.lang.String
   6:        621771       34,819,176  io.netty.buffer.UnpooledHeapByteBuf
   7:        620271       34,735,176  java.nio.StringCharBuffer
   8:        621910       29,851,680  io.netty.channel.DefaultChannelPromise
   9:        621523       29833104  java.nio.HeapByteBuffer
  10:       1235512       29652288  java.lang.StringBuilder
Total      19200129     1,093,142,504
  • 不断进行Full GC时的内存占用
   1:      14,315,627      687,150,096  io.netty.channel.DefaultChannelPromise
   2:      14,314,546      572,581,840  io.netty.channel.AbstractChannelHandlerContext$WriteAndFlushTask
   3:      14,314,631      458,068,192  io.netty.util.Recycler$DefaultHandle
   4:      14,061,517      337,476,408  com.youzan.platform.ordermsgpush.server.handler.MacdonaldMessageHandler$$Lambda$34/1592197748
   5:        339988       11723152  [C
   6:        443875       10653000  com.youzan.platform.ordermsgpush.server.common.CommandOut
   7:        339909        8157816  java.lang.String
   8:        252671        6064104  com.youzan.platform.ordermsgpush.server.common.CommandIn
   9:        252451        6058824  com.youzan.platform.ordermsgpush.server.dispatcher.network.NetWorkEventDispatcher$1
  10:        252451        6058824  com.youzan.platform.ordermsgpush.server.dispatcher.network.
  Total      59789703     2,146,390,000

分析结论:

  • Netty使用DirectByteBuffer缓存待发送的数据,正常情况下,会存在DirectByteBuffer对象,实际内存空间在堆外申请;
  • 不断发生Full GC异常时,JVM堆内存被大量的DefaultChannelPromise、WriteAndFlushTask、Recycler对象填满,可见是因为writeAndFlush任务的产生速度大大超过socket连接的TCP发送缓冲区消化速度,导致Netty不断申请堆外内存在应用层缓存待发送的数据,最终耗尽系统内存资源;

3、产生问题的代码####

发送数据的代码片段如下:

            channelHandlerContext.writeAndFlush(commandOut).addListener(future -> {
                if (!future.isSuccess()) {
                    logger.warn("unexpected push. msg:{} fail:{}", msg, future.cause().getMessage());
                }
            });

由于ChannelHandlerContext.writeAndFlush()只是产生write任务和flush任务,交由Netty的IO线程处理,其ChannelFuture返回时并不保证flush任务已执行完毕并将数据写入到了连接对应的TCP发送缓冲区。
如果不对writeAndFlush()的调用速率进行限制,当应用对socket进行写操作的速率超过socket实际发送速率时,对应连接的TCP发送缓冲区会被填满,后续写操作的待发送数据会由Netty不断申请内存缓存,最终导致内存溢出。

解决方法:

            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);
                }
            }

调用writeAndFlush()方法前先判断当前Channel的可写入状态,若状态为可写入说明待发送数据量并未堆积超限,可以进行异步发送;否则需要执行sync()方法等待发送数据真正成功写入socket的TCP发送缓冲区,然后在执行下一次的write操作,防止write速率过快导致内存溢出。

你可能感兴趣的:(netty学习系列九:记一次压测异常排查)