公司有個匿名聊天的常規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: 'http://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在服務器上調試了。
參考資料: