Node.js躬行記(14)——壓力測試

  公司有個匿名聊天的常規H5介面,運營向做一次 50W 的推送,為了能配合她的計劃,需要對該介面做一次壓力測試。

一、JMeter

  壓測工具選擇了JMeter,這是Apache的一個項目,它是用Java編寫的,所以需要先安裝Java的SDK,選擇當前的作業系統。

   

  隨後到JMeter官網下載應用程式,選擇 Binaries 中的壓縮包

  

  在終端中進入解壓後的 bin 目錄,通過 sh jmeter 命令來啟動 JMeter。 

  

  Don’t use GUI mode for load testing:這段提示資訊是不要在GUI介面進行壓力測試,GUI介面僅僅用於調試。

  程式會自動打開 JMeter 的介面,如果在 選項 -》 選擇語言 -》中文,那麼有可能亂碼。

  

  只需選擇 選項 -》 外觀 -》System 或 Metal,就能避免亂碼,網上有許多使用教程可以參考。

  當測試計劃都編寫完後,保存,然後在終端輸入命令,就能開始壓測了,其中目錄相對於bin,couples.jmx 是測試計劃,webreport是統計資訊。

sh jmeter -n -t ../demo/couples.jmx -l ../demo/result/couples.txt -e -o ../demo/webreport

  

二、實踐

  在正式開始壓測之前,也瀏覽了許多網路資料作為知識儲備。

  首先需要理解Socket(套接字)的概念,它是對TCP/IP協議的封裝,本身並不是協議,而是一個調用介面,Socket連接就是長連接。

  在創建Socket連接時,可以指定傳輸層協議,通常選擇的是TCP協議,所以一旦通訊雙方建立連接後就開始互發數據,直至連接斷開。

  而每個TCP都要佔用一個唯一的本地埠號,但是每個埠並不會禁止TCP並發。

  然後去網上搜索了百萬長連接可能遇到的瓶頸,包括TCP連接數、記憶體大小、文件句柄打開數等,例如:

  每個TCP連接都要佔用一個文件描述符,而作業系統對可以打開的最大文件數的限制將會成為瓶頸。

  如果對本地埠號範圍有限制(例如在1024~32768),當埠號佔滿時,TCP就會連接失敗。

  網上給出了很多解決方案,大部分都是修改作業系統的各類參數。

1)開始測試

  上來就干,執行緒數直接填200以上。

  

  紅框中的欄位含義如下所示:

  • Label: 請求名稱
  • #Smaples: 請求計數,其中108.4是TPS(每秒處理的事務數)
  • Average: 請求響應平均耗時
  • Min: 請求響應最小耗時
  • Max: 請求響應最大耗時
  • Error %: 請求錯誤率
  • Active:執行緒數(圖中並未顯示)

  查看報告頁面,出現了多個錯誤,在網上查資源,做了些簡單地掙扎,並沒有得到好的解決辦法。

Non HTTP response code: java.net.SocketException/Non HTTP response message: Connection reset
Non HTTP response code: javax.net.ssl.SSLHandshakeException/Non HTTP response message: Remote host terminated the handshake
Non HTTP response code: javax.net.ssl.SSLException/Non HTTP response message: java.net.SocketException: Connection reset
Non HTTP response code: java.net.SocketException/Non HTTP response message: Malformed reply from SOCKS server

  後面想想還是根據當前實際情況來吧,運營需要50W的推送,兩小時內完成,平均每秒推送70條,將這個數據作為當前每秒的執行緒數,模擬後一切正常。

  注意,執行緒數和伺服器的並發量不能完全畫等號。

  

  然後讓4000個執行緒1分鐘完成請求,配置Ramp-Up時間為60S,成功率是99.93%。

  圖中的Ramp-Up時間指所有執行緒在多長時間(單位秒)內全部啟動。例如500個執行緒10S,那麼每秒啟動 500/10=50 個執行緒,不寫就是所有執行緒在開啟場景後立即啟動。

  再讓5000的執行緒維持2分鐘,配置Ramp-Up時間為120S,報無法創建新的本機執行緒的錯誤。 

Uncaught Exception java.lang.OutOfMemoryError: unable to create new native thread in thread Thread[StandardJMeterEngine,5,main]

  為了解決此問題,期間走了很多誤區,網上的很多資料都是說修改 jmeter.sh文件,像下面這樣,但是改來改去仍然會報那錯。

set HEAP=-server -Xms768m -Xmx768m -Xss128k 
set NEW=-XX:NewSize=1024m -XX:MaxNewSize=1024m

  或者是用命令來修改本機的一些參數,像下面這樣,但仍然無濟於事。 

launchctl limit maxfiles 1000000 1000000
sysctl -w kern.maxfiles=100000
sysctl -w kern.maxfilesperproc=100000

  後面看到篇文章說在macOS中,對單個進程能夠創建的執行緒數量是有限制的,下面的命令可以讀取最大值,例如本機是4096,但該參數是只讀的,無法修改。

sysctl kern.num_taskthreads

  於是馬上就改變策略,一番查找下來,了解到JMeter還提供了一種遠程模式。

2)遠程模式

  既然一台機器的執行緒數有限,那可以通過多台機器來模擬更多的虛擬用戶,JMeter有一種遠程模式可以實現這個方案。

  首先需要在bin目錄中的 jmeter.properties 文件修改remote_hosts參數,127.0.0.1改成本機地址,如下所示。

remote_hosts=192.168.10.10,192.168.10.46

  然後通過bin目錄的create-rmi-keystore.sh生成rmi_keystore.jks,windows的可以直接運行create-rmi-keystore.bat,mac需要運行create-rmi-keystore.sh文件,會問你一堆問題。 

sh create-rmi-keystore.sh

  並且需要將rmi_keystore.jks文件放置到從機的bin目錄中。此時從機在開啟sh jmeter-server時會報一個錯誤。

An error occurred: Cannot start. MacBook-Pro.local is a loopback address.

  修改jmeter-server,取消RMI_HOST_DEF的注釋項,並將IP地址改成當前機器的。

RMI_HOST_DEF=-Djava.rmi.server.hostname=192.168.10.46

  一切準備就緒後,就可以使用壓測命令了,與之前不同的是,需要加一個 -r 參數,其餘照舊。

sh jmeter -n -t ../demo/couples.jmx -r -l ../demo/result/couples.txt -e -o ../demo/webreport

3)繼續測試

  這次執行緒數量加到4000,加上從機,總共是1.2W個執行緒,Ramp-Up時間為60S,下面是結果圖。

  

  其中Throughput一列表示的是每秒處理的事務數(TPS),在此處也就是伺服器的並發量。統計出21個錯誤,佔比是0.17%。

Non HTTP response code: javax.net.ssl.SSLException/Non HTTP response message: Connection reset

  進到測試伺服器,輸入 ulimit -a 命令,open files 的數量有100多W,所以不會出現那種無法打開文件的錯誤。

   

  再詳細的分析暫時不會,還得先去系統的學習一下,然後再回來補充。

三、學習性能測試

   為了學習性能測試,特地在網上找了個專欄《性能測試實戰30講》,順便記錄了些基礎概念。

1)性能場景

  基準性能場景,單交易容量,將每一個業務壓到最大TPS。

  容量性能場景,將所有業務根據比例加到一個場景中,在數據、軟硬體、監控等的配合下,分析瓶頸並調優。

  穩定性性能場景,核心就是時長,在長時間的運行之下,觀察系統的性能表現。

  異常性能場景,宕主機、宕應用、宕網卡、宕容器、宕快取、宕隊列等。

2)指標

  • RT:響應時間
  • TPS:每秒事務數
  • QPS:每秒SQL數
  • RPS:每秒請求數
  • Throughout:吞吐量

  所有相關的人都要知道TPS中的T是如何定義的。如果是介面層性能測試,T直接定義為介面級;如果是業務級性能測試,T直接定義為每個業務步驟和完整的業務流。

  對一個系統來說,如果僅在改變壓力策略(其他的條件比如環境、數據、軟硬體配置等都不變)的情況下,系統的最大 TPS 上限是固定的

    TPS = (1000ms(1秒)/ RT(單位ms))x 壓力執行緒數

  對於壓力工具來說,只要不報錯,我們就關心 TPS 和響應時間就可以了,因為 TPS 反應出來的是和伺服器對應的處理能力,至少壓力執行緒數是多少,並不關鍵。

3)學習期

  性能工具學習期:自己有明確的疑問。通常所說的並發都是指服務端的並發,而不是指壓力機上的並發執行緒數,因為服務端的並發才是伺服器的處理能力。

  性能場景學習期:如何做一個合理的性能測試,調整業務比例,參數化數據的提取邏輯。

  性能分析學習期:面對問題應該是我想要看什麼數據,而不是把數據都給我看看。

  通過你的測試和分析優化之後,性能提升了多少?

  通過你的測試和分析優化之後,節省了多少成本?

4)參數化

  參數化測試數據的疑問:

  • 參數化數據應該用多少數據量?
  • 參數化數據從哪裡來?
  • 參數多與少的選擇對系統壓力有什麼影響?
  • 參數化數據在資料庫中的直方圖是否均衡?

  在性能場景中,我們需要根據實際的業務場景來分析需要用到什麼樣的數據,以便計算數據量。

  參數化時需要確保數據來源以保證數據的有效性,千萬不能隨便造數據。這類數據應該滿足兩個條件:

  • 要滿足生產環境中數據的分布;
  • 要滿足性能場景中數據量的要求。

四、Websocket Bench

  在這次的壓測中,想要測試2000人在線,並且同時聊天,伺服器能否完美處理。

  如果要訪問頁面模擬用戶的行為,會比較麻煩,因為在聊天前需要做兩步操作,第一步是確認協議,第二步是選擇匹配範圍,第三步才開始匹配用戶開始聊天。

  若要兩個用戶匹配成功,首先需要都在線,其次是經緯度計算後的範圍滿足之前的配置。

  為了避免那麼多繁瑣的前置場景,我決定直接對socket進行壓測,於是想到了Websocket Bench

  它支援Socket.IO、Engine.IO、Primus等實時通訊庫的方法,經過簡單的文檔查閱後,開始編碼,直接將官方demo複製修改。

module.exports = {
    /**
     * Before connection (optional, just for faye)
     * @param {client} client connection
     */
    beforeConnect : function(client) {
    },

    /**
     * On client connection (required)
     * @param {client} client connection
     * @param {done} callback function(err) {}
     */
    onConnect : function(client, done) {
      // Socket.io client
      client.emit('say', 100, {
        id: 111,
        avatar: '//www.pwstrick.com',
        userId: 123,
        msg: Date.now().toString(36) + Math.random().toString(36).substr(2),
        msgType: 'text'
      }, (msg) => {
        console.log(msg);
      });
      console.count();
      done();
    },

    /**
     * Send a message (required)
     * @param {client} client connection
     * @param {done} callback function(err) {}
     */
    sendMessage : function(client, done) {
      done();
    },

    /**
     * WAMP connection options
     */
    options : {
      // realm: 'chat'
    }
 };

  啟動命令,-a 是指持久化連接總數 ,-c 是指每秒並發連接數 ,-g 是指要執行的JS文件,-k 保持活動連接,-o 是指日誌的輸出文件。

websocket-bench -a 2000 -c 2000 -g chat.js -k test-web-api.rela.me/chat -o opt.log

  開始運行後,並沒有我設想的那樣,實現2000人並發,TPS最多也就80多,到一個時間後,就持續變少。下圖來自阿里雲的日誌,每次發消息我都會記錄一條日誌。

   

  我對上面的 -a 和 -c 的理解還有誤差,不過也有可能是我本機限制了並發,之後就讓QA在伺服器上調試了。

 

 

參考資料:

淘寶技術分享 HTTP長連接200萬嘗試及調優

如何實現單伺服器300萬個長連接的?

100萬並發連接伺服器筆記之1M並發連接目標達成

websocket非同步高並發_websocket長連接壓力測試踩過的坑

記一次Socket.IO長鏈服務的性能壓測

websoket.io 高並發實現

Linux下實現高並發socket最大連接數的配置方法

Socket和Http通訊原理

單台機器100w連接壓測實戰

Mac OS下Jmeter的入門操作

高並發TCP連接數目問題

JMeter中的HTTPS套接字錯誤

ulimit的坑,讓我的故障一波又一波

jmeter使用總結

修改mac單應用創建執行緒的限制

JMeter 七:遠程測試