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

一、背景描述#

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

推送服务描述

二、问题描述#

进行压力测试,当消息发送速率达到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速率过快导致内存溢出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,577评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • 1、Netty基础入门 Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应...
    我是嘻哈大哥阅读 4,687评论 0 31
  • Java 应用性能优化是一个老生常谈的话题,典型的性能问题如页面响应慢、接口超时,服务器负载高、并发数低,数据库频...
    Rick617阅读 7,320评论 1 9
  • 生活充满了未知元素,有时会刮风,有时会下雨,没人知道下一刻云飘向哪里,哪里会看到绚烂彩虹,前方的路通向哪里…… 我...
    木知呀先生阅读 372评论 5 4