1. 程式人生 > >淺談HTTP介面效能測試指令碼的編寫

淺談HTTP介面效能測試指令碼的編寫

作為測試界的老司機,最近接到一項任務需要寫新的效能測試程式碼。由於之前的測試程式碼風格和自己習慣的編碼風格差別實在太大,因此放棄了模仿原來的測試程式碼繼續新增測試用例的想法,自己從頭開始寫了一些測試程式碼。

結果,進行測試過程中居然出現了問題,問題問題……

問題發現

在用自己新寫的測試程式碼測試一個檔案下載介面的時候,發現了一個奇怪的現象,在併發比較小的情況下(50併發),TPS、響應時間什麼的一切看起來都很正常。但是當併發加到100+時,TPS曲線就變得很神奇了,同時還有部分請求失敗,測試結果如圖1所示:


圖 1 併發100+效能測試結果

被測系統的架構相對簡單,業務服務前端搭個Nginx,客戶端請求都是把請求傳送到Nginx,然後通過Nginx轉發到業務伺服器。 測試環境架構如圖2所示,被測服務為圖2中的NefsProxy:

圖 2 測試環境架構圖
原因查詢
通過檢視測試指令碼的日誌發現,所有的錯誤都是由於建立連線失敗引起的,登入到Nginx所在伺服器上使用netstat命令檢視連線數,發現測試過程中連線數很高,高的時候能達到3000—4000。為了確認上述問題是否是由於測試指令碼有問題而引起的,我馬上用老的測試指令碼跑了一輪測試。結果很明顯,老的測試指令碼跑的測試結果一切正常,TPS也比新指令碼的結果高不少,並且沒有失敗。因此可以斷定是測試指令碼的問題。

接下來就要尋找原因了:

→首先懷疑是不是測試指令碼在每個請求結束後沒有釋放連線。問題的現象是連線數高導致新連線被Nginx拒絕。但是review了程式碼後,發現每次請求結束後都呼叫了:


因此,應該不是測試客戶端沒有主動釋放連線引起的。

→後來想起Nginx配置了將請求強制轉換為長連線。

Nginx的配置如下:

問題好像有點頭緒了。一般配置長連線是為了提高服務端效能,為什麼在我的測試中反而起到了反作用呢?

→接下來就要回過頭來好好看看自己測試程式碼的實現邏輯了。

其中,NosObjectOperation.getObject是java程式碼實現的,每次getObject方法被呼叫時,會new一個HttpClient物件,然後通過HttpClient傳送Http請求。

到這裡,問題的本質慢慢浮出水面:客戶端每傳送一次請求,都會new一個HttpClient,並與Nginx新建一個連線,而Nginx這邊又設定了強制長連線,每個Worker的最大空閒連線數為1024(keepalive 1024)。同時,測試環境的Nginx配置了4個Worker。因此,Nginx最多會保持4096個空閒連線。所以,由於連線數過多,在空閒連線被釋放前,新的連線可能就會被拒絕。

問題解決

既然問題的原因找到了,該怎麼修改測試指令碼呢?

當然最簡單的就是每個請求處理結束後強制將連線關閉。這樣雖然能解決連線數多的問題,但是也同時間接的讓Nginx強制長連線的配置失效了,達不到長連線提升效能的目的。

因此,我採用了這樣的解決方法:儘量模擬真實的使用者場景,每個測試執行緒使用一個HttpClient物件(也就是在python指令碼的__init__方法裡new一個HttpClient物件,getObject測試方法都呼叫這個物件傳送Http請求,同時java方法nosObjectOperation.getObject也不再每次自己建立新的HttpClient物件,而使用傳入的在Python指令碼方法__init__中建立的HttpClient物件):


Attention:使用grinder進行效能測試時,每次建立測試執行緒時會呼叫__init__方法,也就是說有多少個併發執行緒,就會被呼叫多少次。通過這樣的修改,如果測試時是100個併發執行緒,那測試客戶端和Nginx之間就只會有100個ESTABLISHED的連線。

問題解決了,用新的指令碼跑一次測試,結果如圖3所示,很讓人滿意:

圖 3 修改測試指令碼後併發100+效能測試結果
注意,總結!總結!總結!

1)做效能測試,測試指令碼的編寫也是一個很重要的環節,只有模擬真實使用者使用情況的指令碼才能跑出真實的、有參考意義的效能測試結果。

2)當效能測試結果與預期不一致時,定位問題時首先要看測試指令碼是否有問題,並對測試環境的各項配置(Nginx、Tomcat等)進行梳理。

3)當使用grinder測試框架進行比較複雜的效能測試,編寫測試指令碼時要弄清楚grinder測試框架的執行機理,各項配置的作用等。