關於性能測試

  • 2019 年 10 月 4 日
  • 筆記

性能測試已經是一個老生常談的話題了,不同的項目或多或少都會涉及到,但是每個人的經驗肯定有所不同。今天我想從以下幾個方面分享一下我認為關於性能測試需要重視的要點。


1. 性能測試的需求

看似很明確的需求

當問到性能測試的需求,我們的客戶有時候會回答,「我們要支援20000個用戶同時在線」,或者「每天有10000個用戶同時訪問這個網站」,更有甚者「我們也不知道,越多越好吧」。這種情況下我們應該怎麼辦呢?

當用戶說「我們有10000個用戶同時訪問這個網站」的時候,看似我們的需求已經很「明確」了。真的很明確了嗎?就拿這個例子來說,這10000個用戶都在幹什麼呢?假想下,可能有5000個用戶在瀏覽靜態頁面,2000個在創建表單,1000個在做查詢,還有… 真正產生壓力的用戶是誰呢?這恐怕不能單單從10000個用戶同時訪問這個網站得到結果。

之前做過一個項目,客戶為某機場,他們為用戶提供免費的WiFi服務,但是提供在WiFi的同時,會在不同的頁面播放廣告。後台有一個廣告管理系統,可以做一些配置,根據優先順序來投放廣告。這其中涉及到一些複雜的演算法,主要是投放廣告的比例。當然機場還有很多物料伺服器,主要是用來存放廣告的圖片等等。當時用戶給的性能需求也就一句話,「我們每天大概有xxxxxx個用戶連接我們的WiFi」。那怎樣來做性能測試呢?

「多少個用戶連接WiFi」其實只是一個最表面的現象,在連接WiFi的同時,其實系統做了很多事情。首先連接WiFi的時候會去訪問分發伺服器,確定要播放哪幾個廣告,其次會根據分發的廣告,去物料伺服器上找相應的資源,然後打點把廣告訪問的歷史數據記錄到資料庫中。這是能聯想到的一些最基本的後台操作,每一個行為都有可能成為一個瓶頸,都需要去認真考慮。

所以在考慮性能需求的時候,一定要清楚背後的邏輯是什麼,同時需要分析用戶活動,從而確認系統的性能測試目標。

歷史數據

「性能現在的需求也不是很明確,但我們有原來的老系統,能幫我們看下嗎?」

對於性能測試來說,如果有很詳細的歷史數據,這將是一個非常珍貴的數據源,我們可以從中分析出很詳細用戶行為。之前我們做過一個項目,性能測試的需求基本上就是從歷史數據中得到的。我們對資料庫進行了很詳細的分析,從而設計性能測試用例,這其中主要包含系統的哪些service承受了壓力、壓力是多大、用戶量是多大等等。

需要特別注意的是,如果對於業務的了解不夠深,很容易導致場景的設計缺失,從而導致測試的場景和實際的場景有所偏差。比如分析某張表,分析出系統有10000個插卡拔卡記錄,有的人可能直接就開始寫腳本了,模擬一個用戶每天做10000次插卡拔卡的操作。殊不知,這10000次插卡和拔卡的操作是4000個用戶完成的…而性能的瓶頸很有可能就在用戶的管理,而不是插卡拔卡產生的壓力。

所以建議在做歷史數據分析的時候,一定盡量把場景分析全面,並且需要去了解真正的業務,才可以盡量減少分析的錯誤。

不明確的需求

「我們也不知道需求是什麼,你們看著辦吧。」

這種情況通常出現在一個新的項目,客戶對上線後的用戶體量並沒有很好的認知。我們應該怎麼做呢?

通常我們會對系統進行負載測試,就是在被測系統上不斷增加壓力,直到性能指標(如響應時間)超過預期或者某種資源已經快達到飽和狀態,這個時候我們基本就可以確定系統的瓶頸是什麼、支援多大的吞吐量。再通過一段時間的穩定性測試,讓系統在一定的時間內(比如一周)維持一定的壓力,去查看相應的性能指標。

這樣,我們就可以告訴我們的用戶,系統現在支援多大的用戶訪問量、吞吐量是多少。

當然,現在提到的都是客戶的需求,對於性能測試本身來說,我們也有一些明確的需求,也就是我們通常提到的index,下面我會繼續深入討論這個問題。


2. 定義性能測試的指標

性能測試指標

通常來說我們會關注如下的性能測試指標。

響應時間:比較熟悉的就是2-5-8原則(據統計當網站慢一秒就會流失十分之一的客戶),通常來說,2到5秒,頁面體驗會比較好,5到8秒還可以接受,8秒以上基本就很難接受了。但是有的項目也會例外,比如從海量的數據中去查詢某些數據,或者生成報告(年度報告),這種可能就不太適合2-5-8原則,但是前提是要管理好客戶的期望。

吞吐量:指的是在單位時間內客戶端和伺服器成功傳送數據的數量

並發:客戶/服務端同一批用戶同時執行一個操作的數量

資源使用率:通常來說,我們關注的資源就是幾大塊:記憶體、CPU、I/O和網路

成功率:比如在某些情況下,API調用的成功率

當了解了我們所需要關注的性能指標,就可以來定義具體的性能測試指標了。

我之前做一個比較大的項目的性能測試,系統提供很多服務,有web service,有windows的services,基本上每個services都是單獨部署在一台window的伺服器上,所有的services都連到同一個資料庫。由於這個系統並不存在網頁,所以我們並沒有把頁面載入這些考慮在內。我們從歷史數據中首先了解到了系統需要滿足的並發量,針對每個service去做相應的壓力測試,並且定義對應的性能測試指標。如下圖:

  1. 上圖就是對於web service性能測試的指標和結果,這個結果是我們基於每秒有7個Post請求+3個get請求而產生的。
  2. 這其中的性能指標包含了我們對這個service的單獨的記憶體監控、CPU監控,以及成功率的監控,還有相應的原因分析。通常來說,對於記憶體、CPU等的佔用率,業界都有很共識的標準,大家可以拿來作為參考,由於我們伺服器是8g記憶體,8核CPU,所以表現還是相當不錯的。
  3. 其他的service也是類似的,我們考察的點主要在於CPU,記憶體的佔用率,還有成功率。
  4. 由於網路一開始我們就認為不是瓶頸(都在內網),所以我們並沒有把網路相應的指標加進去。
  5. 需要特別注意的資料庫server,和其他windows service或者web service不一樣,對於DB server我們需要特別關注的是I/O,資料庫的瓶頸一般都是出現在I/O上面。

自定義指標

在項目中我們往往還要自定義一些其他的指標,來查看某些特定的性能。比如:

為了分析方便,我們在一些service的關鍵方法上都加上時間戳,便於去做trouble shooting。於是我們有了一下的指標,圖一是某個方法的平均調用時間,圖二是我們計算出的最慢調用方法的top10:

總體來說,性能測試的指標可以從不同的方面去定義,比如響應時間、資源利用、並發數、吞吐量、並發數…


3. 如何trouble shooting

剛才提到了性能測試中的一些相應的指標,當我們發現某些指標不正常的時候,我們應該怎樣去做呢?

性能測試的指標往往又是相互關聯的,當遇到問題時,我們需要從這些指標的結果中去尋找真正的root cause,這也是性能測試最重要的點之一。廢話少說,我們直接來看幾個例子吧:

記憶體泄露

記憶體泄露是性能測試中很常見的一個點,比如下圖,很明顯能看到有一個記憶體上升的趨勢,這種情況下很可能就有記憶體泄露的情況。但是也有例外,比如是C#或者Java寫的程式碼,因為記憶體回收是垃圾回收器自己的行為,不像C語言那樣需要手動回收,所以即使記憶體在一段時間內有上升的趨勢,但是只要能回到原點,就不存在記憶體泄露的現象。

記憶體泄露很多時候並沒有很明顯的徵兆,有一次客戶抱怨服務掛掉了,這個服務之前有好幾個月都運行的好好的,怎麼說掛就掛呢,然後我們對系統日誌做了很詳細的分析,發現就是記憶體泄露惹的禍。每天記憶體泄露一點點,到第三個月的時候才導致服務崩潰,這在短期的性能測試中就很難發現。

下圖是當時我們在做性能測試的時候發現的問題,很明顯記憶體在一段時間內直接從200多MB升到了500多MB,很有可能存在記憶體泄露的情況。怎樣trouble shoooting呢?

我們藉助了第三方的工具ANTS,最終有下面的結果,很明顯Sting類變成了最大的類,高達211MB,這個是什麼原因造成的呢?然後我們在工具上找了String object的調用鏈,最終發現程式碼中有一行的判斷錯誤,導致了記憶體上升。

API響應速度慢

這是一個很常見的問題,單單看現象,其實我們並不知道系統是哪裡出的問題,有可能是網路慢,有可能是記憶體出現了瓶頸,也有可能是I/O的問題… 怎樣定位呢?

首先我們還是找到相應的apiserver,通過性能測試的報告,我們發現了下面的問題,message queue隨著時間的變化增長特別快,這個時候看API server本身的記憶體和CPU並沒有什麼問題,那麼問題到底處在哪呢?

這個時候其實很容易就聯想到是資料庫出現了問題,對比兩個不同的版本,發現我們對資料庫的存儲過程有所更改,最終發現是存儲過程的更改導致了某個索引失效,最終反映到API就是速度明顯變慢。添加索引後問題得到了解決。一般來說,當我們在修改資料庫的時候一定要特別小心,很可能一個小的改動就會導致系統性能的急劇下降。

對於性能測試來說,往往我們發現的問題都是表象上的問題,比如頁面反應很慢。但這有可能是多種原因導致的,需要去做深入的分析。這也要求我們在做性能測試時去收集足夠的資訊,以支撐分析,同時藉助一些第三方工具,才能真正定位到問題。


4. 關於資料庫

其實性能測試我前面已經提到了很多,為什麼還要把資料庫單拿出來說呢?我們來看看資料庫伺服器的特殊之處:

關於記憶體

我們在進行伺服器的記憶體的測試的時候,如果是普通的APP server,當我們對記憶體進行監控時,通常都是有一個metrics, 比如某個APP的service的記憶體使用量不能超過總量的百分之多少,對於整個sever來說,通常也會有比較多的記憶體剩餘。

而當我們去查看一個資料庫server的時候,會發現可用的記憶體量通常只有10M或者5M(取決於你的配置)…… 而當你把資料庫的記憶體增加一倍,可用的記憶體通常還是只有10M左右,這是什麼原因呢?

其實這與資料庫本身的工作原理有關係,資料庫中io操作的基本單位為頁,當資料庫執行一條語句,比如一條查詢語句,它會先從物理磁碟中把相應的頁載入到記憶體,然後再進行操作。

因為資料庫本身就在不停的讀寫,所以資料庫記憶體當中會快取各種各樣的數據,為了更快的讀寫,資料庫會有演算法去維護這些記憶體中的數據,以保證儘可能的使得數據都是從記憶體中獲得,而不是從物理記憶體中得到(記憶體的訪問速度是納秒級,硬碟是微秒級)。所以一般來說,資料庫會基本用光所有的記憶體。

這就是為什麼資料庫會吃記憶體了,這與其工作原理有關係,只要能保證系統能正常運行就可以了,其他的記憶體都可以用來快取數據。

關於死鎖

死鎖通常是指爭搶資源不當,讓雙方因為對方掌握了自己的資源而無限期的等下去。如下圖所示:

發生死鎖的原因很多,大部分是由於事務之間對資源訪問順序的交替,或者並發修改統一記錄導致的,資料庫對待死鎖有著不同的策略,對於SQL server來說,它會隨機殺掉其中的一個,至少保證另外一個事務的正常運行;而對於Oracle來說,它會對兩個事務進行一個評估,會殺掉它認為不那麼重要的一個。

所以我們在數據層面,需要去監控死鎖的發生情況,一般來說,死鎖是不可避免的,但是一旦死鎖發生頻率很多,必定會影響到業務。

我們可以用很多工具去監控死鎖,比如SQL Profile。對於死鎖的修正也是對於高並發的事務,盡量減少長度;把鎖的優先順序調整低一些(用低隔離級別);按同一順序訪問對象,盡量避免事務中的用戶交互。

關於磁碟

因為對於資料庫來說,最重要的就是讀寫的操作,磁碟對於資料庫來說是非常重要的。

現在好多資料庫都是直接放在雲盤上的,雲盤上的磁碟有著自己的結構,但是很多年前,如果是自己的伺服器,我們還要考慮到磁碟陣列,來保證磁碟的效率和安全性。

對於磁碟來說,我們通常會從讀寫方面來衡量,有幾個比較通過的指標可以用來衡量資料庫磁碟的性能,比如Average Disk queue lenth,數據生命周期等等。

另外值得一提的是資料庫的碎片整理。當資料庫讀寫一段時間之後,由於頻繁的插入數據,會導致數據並不是按照順序排列在磁碟上面,這樣當我們在查相關數據的時候,磁碟往往要去不同的區域查找數據,導致性能降低,這個時候我們很有必要去運行磁碟的碎片整理,來保證資料庫性能。

但是這個job本身就很占磁碟的I/O,所以盡量選擇系統不忙的時候進行。

關於索引

相信索引是大家很熟悉的一個話題了,當數據量很小時,不建索引,進行全表掃描的的性能尚可接受。但是數據量大時,必須藉助索引。所以索引的適當與否,是性能好壞的關鍵。

比如執行一條語句

SELECT UserName, UserID FROM US.UserDetail WHERE UserName = 「李四」

這條語句這樣執行與創建的索引有關係,如果沒有索引就需要進行全表掃描,這樣載入到記憶體當中的頁在數據量很大的情況下就相當多了。如果建立了適當的索引,可能只需要載入幾頁記憶體就可以了。

當我們用一些工具比如sql profile跟蹤到一些長的查詢的時候,我們就需要去看看索引是否建立恰當。這在資料庫的優化中也是很重要的一個方面。當然,索引雖然對讀的性能有幫助,但是對寫的性能卻有影響。

我們也需要再適當的時候reindex索引,原因就是如果索引在物理存儲上不連續,也會導致性能的下降,這與磁碟的碎片整理是一個道理。

一般來說,如果系統設計合理,最終的瓶頸都會出現在資料庫上,而我們在做性能測試時,也需要了解資料庫的特殊之處,去更好的做性能測試。


5. 關於性能測試的工具

對於市面上的那麼多性能測試工具,怎樣選擇呢?

性能測試的工具實際上有很多,能列出來的比如Jmeter、Loadrunner,Galtling等等,這取決於很多方面:

  1. 需要測試測試服務端的性能?還是前端的性能?或者是mobile端的性能?後端的話可以選擇Jmeter、Loadrunner或者Gatling。前端的話可以選擇比如Fiddler、httpWatch等,手機端可以選用GT或者APT等。
  2. 項目的成本,很多性能測試的工具是收費的,也有很多是開源的,需要結合具體的項目進行考慮。
  3. 我們也可以自己寫腳本來完成性能測試,只要能滿足要求項目的要求就可以。但是通常來說使用已有的工具一般會產生比較好的測試報告,除非一些特殊情況,比如某些協議的特殊性,或者很簡單的一些場景等,一般來說還是建議使用市面上已有的工具。
  4. 使用工具的同時,盡量保證測試系統和測試的腳本在同一個網路裡面,這樣測試出來的結果才有意義。

寫在最後

總的來說,性能測試其實是一個很複雜的過程。需要結合不同的方面正確分析需求,根據需求設計出合理的測試場景,定義出性能測試的指標,並且在測試中選擇合適的工具進行測試,並且結合測試的多項結果進行分析和trouble shooting。