记一次Spring Websocket后台服务器CPU占用率过高的问题排查过程

背景

最近在做Spring Websocket后台程序的压力测试,但是当并发数目在10个左右时,服务器的CPU使用率一直在160%+,出现这个问题后,一开始很纳闷,虽然服务器配置很低,但也不至于只有10个并发吧。。服务器的主要配置如下:

  • CPU:2核 Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz
  • 内存:4GB

使用top命令查看资源占用情况,发现pid为9499的进程占用了大量的CPU资源,CPU占用率高达170%,内存占用率也达到了40%以上。

问题排查

首先使用jstat命令来查看一下JVM的内存情况,如下所示:

jstat -gcutil 9499 1000

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8129 1147.010 1147.661
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8136 1148.118 1148.768
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8143 1149.139 1149.789
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8150 1150.148 1150.799
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8157 1151.160 1151.811
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8164 1152.180 1152.831
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8170 1153.051 1153.701
  0.00   0.00 100.00  94.92  97.45  95.30     24    0.651  8177 1154.061 1154.712
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8184 1155.077 1155.728
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8191 1156.089 1156.739
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8198 1157.134 1157.785
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8205 1158.149 1158.800
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8212 1159.156 1159.807
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8219 1160.179 1160.830
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8225 1161.047 1161.697

可以看到,Eden区域内存占用高达100%,Old区占用高达94.9%,元数据空间区域占用高达97.4%。Young GC的次数一直是24,但是Full GC的次数却高达几千次,而且在程序运行期间,频繁发生Full GC,导致FGC的次数一直增加。
虽然FGC次数一直在增加,但是却没有回收到任何空间,导致一直在运行FGC,根据这些信息,基本可以确定是程序代码上出现了问题,可能存在内存泄漏问题,或是创建了不合理的大型对象。


基于上述分析,我们知道应该是程序的问题,要定位问题,我们需要先获取后台程序的堆转储快照,我们使用jmap工具来生成Java堆转储快照:

jmap -dump:live,format=b,file=problem.bin 9499

Dumping heap to /root/problem.bin ...
Heap dump file created

下面就是对Java堆转储快照进行分析了,我使用了Eclipse Memory Analyzer(MAT)来对快照进行分析,在MAT打开快照文件之前,要将其后缀名修改为hprof,打开文件之后,可以发现如下问题:

9 instances of "org.apache.tomcat.websocket.server.WsFrameServer", loaded by "java.net.URLClassLoader @ 0xc533dc70" occupy 566,312,616 (75.57%) bytes. 

Biggest instances:
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xce4ef270 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xce4f1588 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xcf934b10 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xcf936e28 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xcf9620f8 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xd21c6158 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xd5dc8b30 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xd727bcf8 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xe768bd68 - 62,923,624 (8.40%) bytes. 

可以看到WsFrameServer的实例占用了75.57%的内存空间,而这也就是问题所在了,那WsFrameServer为什么会占用这么高的内存呢?我继续用MAT来查看WsFrameServer实例的内存分布情况:



可以看到,WsFrameServer实例中,有两个类型的变量占了WsFrameServer的绝大部分,它们分别是java.nio.HeapCharBuffer类的实例变量messageBufferText、java.nio.HeapByteBuffer类的实例变量messageBufferBinary。

WsFrameServer继承自WsFrameBase ,messageBufferText和messageBufferBinary属性就在WsFrameBase里,然后我们来debug程序,看看这两个属性是如何被赋值的。

public WsFrameBase(WsSession wsSession, Transformation transformation) {
    inputBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE);
    inputBuffer.position(0).limit(0);
    messageBufferBinary = ByteBuffer.allocate(wsSession.getMaxBinaryMessageBufferSize());
    messageBufferText = CharBuffer.allocate(wsSession.getMaxTextMessageBufferSize());
    wsSession.setWsFrame(this);
    this.wsSession = wsSession;
    Transformation finalTransformation;
    if (isMasked()) {
        finalTransformation = new UnmaskTransformation();
    } else {
        finalTransformation = new NoopTransformation();
    }
    if (transformation == null) {
        this.transformation = finalTransformation;
    } else {
        transformation.setNext(finalTransformation);
        this.transformation = transformation;
    }
}

我们首先看debug结果:


可以看到,这两个变量的capacity都是20971520,它们是根据WsSession返回的大小来分配大小的,我们来看WsSession的方法的返回值:

private volatile int maxBinaryMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
private volatile int maxTextMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;


static final int DEFAULT_BUFFER_SIZE = Integer.getInteger(
            "org.apache.tomcat.websocket.DEFAULT_BUFFER_SIZE", 8 * 1024)
            .intValue();

这两个变量的大小默认都是8192,那如果它们只占用8K的内存大小,应该也不会出现问题啊,那这两个变量一定是在其他地方被修改了,我们继续看源代码,在WsSession的构造方法中有如下两行代码:

this.maxBinaryMessageBufferSize = webSocketContainer.getDefaultMaxBinaryMessageBufferSize();
this.maxTextMessageBufferSize = webSocketContainer.getDefaultMaxTextMessageBufferSize();
@Override
public int getDefaultMaxBinaryMessageBufferSize() {
    return maxBinaryMessageBufferSize;
}

@Override
public int getDefaultMaxTextMessageBufferSize() {
    return maxTextMessageBufferSize;
}

webSocketContainer是在WsSession的构造方法中传入的,webSocketContainer这两个方法分别返回maxBinaryMessageBufferSize和maxTextMessageBufferSize的值,它们默认为:

private int maxBinaryMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
private int maxTextMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;

即这两个方法的默认返回值仍然是Constants.DEFAULT_BUFFER_SIZE,即8192,那它们是在哪里改变成20971520了呢?
WsWebSocketContainer类中还有以下几个方法:

@Override
public int getDefaultMaxBinaryMessageBufferSize() {
    return maxBinaryMessageBufferSize;
}

@Override
public void setDefaultMaxBinaryMessageBufferSize(int max) {
    maxBinaryMessageBufferSize = max;
}

@Override
public int getDefaultMaxTextMessageBufferSize() {
    return maxTextMessageBufferSize;
}

@Override
public void setDefaultMaxTextMessageBufferSize(int max) {
    maxTextMessageBufferSize = max;
}

这几个方法分别可以获取和设置maxBinaryMessageBufferSize和maxTextMessageBufferSize的值,那是不是通过这几个方法来修改的值呢?
ServletServerContainerFactoryBean类中有如下一段代码:

public void afterPropertiesSet() {
    Assert.state(this.servletContext != null,
            "A ServletContext is required to access the javax.websocket.server.ServerContainer instance");
    this.serverContainer = (ServerContainer) this.servletContext.getAttribute(
            "javax.websocket.server.ServerContainer");
    Assert.state(this.serverContainer != null,
            "Attribute 'javax.websocket.server.ServerContainer' not found in ServletContext");

    if (this.asyncSendTimeout != null) {
        this.serverContainer.setAsyncSendTimeout(this.asyncSendTimeout);
    }
    if (this.maxSessionIdleTimeout != null) {
        this.serverContainer.setDefaultMaxSessionIdleTimeout(this.maxSessionIdleTimeout);
    }
    if (this.maxTextMessageBufferSize != null) {
        this.serverContainer.setDefaultMaxTextMessageBufferSize(this.maxTextMessageBufferSize);
    }
    if (this.maxBinaryMessageBufferSize != null) {
        this.serverContainer.setDefaultMaxBinaryMessageBufferSize(this.maxBinaryMessageBufferSize);
    }
}

这个方法将在bean所有的属性被初始化后调用,其实这两个值就是在这修改的了。
为什么这么说呢,我们看着两个截图:
项目启动时serverContainer的相关属性值
WsSession的构造方法中传入的wsWebSocketContainer的属性值

对比这两张图片可知,WsSession的构造方法中传入的wsWebSocketContainer与项目启动时的serverContainer是同一个实例。所以,在afterPropertiesSet()方法中设置的值就是在wsWebSocketContainer中设置的值。

ServletServerContainerFactoryBean类的相关属性如下:

@Nullable
private Integer maxTextMessageBufferSize;

@Nullable
private Integer maxBinaryMessageBufferSize;

这两个属性的初始值是在servlet中设置的:

<bean class="org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean">
    <property name="maxTextMessageBufferSize" value="20971520"/>
    <property name="maxBinaryMessageBufferSize" value="20971520"/>
</bean>

总结

通过上述分析,也就解释了为什么WsFrameServer占用了很大的内存。那程序中为什么一开始将这两个值设置这么大呢?原因是在很久以前,我们刚测试Websocket通信时,发现只能传输小于8K的消息,大于8K的消息都不能进行传输,所以我们干脆把它调大,也就直接设置为了20M,这也就导致了现在的这个问题。
但是程序中发送的消息大小都是100K+的,那我也不能将他们设置太小,所以我们将其改小,设置为200K,然后重新测试,能够达到50并发。但是,50并发感觉还是不太行,不知道能不能有其他的解决办法~_~我再想想。

  • 8
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值