1. 程式人生 > >記一次Spring Websocket後臺伺服器CPU佔用率過高的問題排查過程

記一次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併發感覺還是不太行,不知道能不能有其他的解決辦法~_~我再想想。