性能測試之 k6 篇
背景
項目的目標是爲客戶交付一個 ToC 的 APP,其後端是基於 RESTful 的微服務架構,同時後端還採用了 Protobuf 協議來提高傳輸效率。在最終上線之前,我們需要執行性能測試以確定系統在正常和預期峯值負載條件下的表現,從而識別應用程序的最大運行容量以及存在的瓶頸,並針對性能問題進行優化以提升用戶體驗。
性能測試是一個較爲複雜的任務,包括確定性能測試目標,工具選擇,腳本開發,CI 集成,結果分析,性能調優等過程,需要 QA,Dev,Devops 協力合作。本文將對這一系列過程進行詳細描述。
爲什麼選擇 k6
在得知需要做性能測試後,我們就開始針對性能測試做了一番調研,在閱讀了一些性能測試工具對比的文章後,最終挑選了 k6,locust 和 Gatling 做了進一步對比,下面是對比的結果。
對我們來說,k6 的優勢在於:
-
k6 支持 TypeScript,由於項目上已經有 TypeScript 使用經驗,因此該工具學習成本相對更少
-
k6 本身支持 metrics 的輸出,可以滿足大部分 metrics 的需求,有需要還可以進行自定義
-
k6 官方支持與多種 CI 工具,數據可視化系統的集成,開箱即用
-
Gatling 支持 Scala/Java/Kotlin,項目上沒有使用相關的技術棧,需要和客戶申請,成本高於 k6
動手寫第一個 case
有了上面的基礎,我們便開始嘗試在項目中集成 k6,在選了一個簡單的 API 寫第一個 case 的時候,發現有以下一些挑戰需要解決:
挑戰 1 - 獲取 Access Token 和保證 token 時效性
由於當前項目的 API 都集成了 OAuth,任何操作都要有一個有效的用戶和 Access Token,因此需要提前生成 token 和測試數據。這一部分因爲項目的不同會有一些差異,需要具體情況具體分析。在此次測試中具體包括以下幾項:
-
用戶賬號準備,比如生成 200 個用戶,並進行一系列的前置處理,讓它們變成可用的正常測試賬號,並且需根據項目安全規範,保存到合適地方,比如 AWS Secrets Manager 或者 AWS Parameter Store,這裏的賬號可以複用。
-
token 生成,運行測試前,生成最新的有效 token,執行測試的時候只需要去讀取 token 數據。
-
token 刷新,由於 token 基本上都具有時效性,如果有效時間短,還需要考慮 renew token,這裏我們採用 refresh token 去獲取新 access token 的方式。
-
需要注意的是測試過程中刷新 token 會計入請求,對性能測試數據會有些許影響,刷新機制需要納入考慮範圍。
挑戰 2-Protobuf 數據的編解碼
下圖簡要說明了前後端的架構,Mobile 和 BFF 是以 Protobuf 格式做數據交換,BFF 和 Backend 是以 Json 格式做數據交換。
另外由於性能測試採用的是 TypeScript 語言,我們還需要將 Protobuf 文件編譯成 TS 版本,這一點在 Protobuf 官方文檔上給出瞭解決辦法,可以很容易的生成 TS 版本代碼。
由於每個 API 的編解碼結構都是一份單獨的 proto,因此還涉及到代碼複用的問題,需要設計合適的方法,讓不同的 API 只需要提供對應的 encode 和 decode schema 即可。
當解決掉前面的兩個挑戰後,可以初步得到符合項目需求的測試框架。
├── protobuf file/ --- protobuf文件
├── dist/ --- ts轉成js的測試文件
└── src/
├── command/ --- 一些腳本文件
├── config/ --- config文件
├── httpClient/ --- http client
├── ProtobufSchema/ --- 編譯好的protobuf文件
├── test/ --- 測試case
└── testAccount/ --- 測試賬戶
優化項目 & 集成 CI & 可視化報告
測試用例設計
當測試 case 逐漸增多後,我們對測試用例進行了多次的調整,例如對 API 進行了分類,並通過不同的方式來對他們進行性能測試。
獨立 API
獨立 API 是指不依賴其他接口提供參數輸入,即可完成請求的 API,例如部分 Get 類 API。
非獨立 API
非獨立 API 是指依賴於其他 API 結果作爲參數輸入纔可完成請求的 API,例如部分 Put、Delete 類 API。由於此類 API 依賴於其他 API 的結果數據,無法單獨做性能測試,在本次性能測試中以整體 journey 的形式來測這些非獨立的 API,在測試 case 中將前一步的結果傳給後一步,從而完成整體的 journey 測試。
我們通過一個例子來說明,我們的 test case 目錄結構如下:
└── test
├── orderService
│ ├── createOrder
│ │ ├── createOrderRequestBuilder.ts
│ │ ├── createOrderRequestClient.ts
│ │ └── createOrderTest.ts
│ ├── getOrders
│ │ ├── getOrdersRequestClient.ts
│ │ └── getOrdersTest.ts
│ ├── orderJourney
│ │ └── orderJourneyTest.ts
│ └── updateOder
│ ├── updateOrderRequestBuilder.ts
│ └── updateOrderRequestClient.ts
├── payService
└── userService
其中
-
對於 createOrder,getOrders 是獨立 API,可以方便的進行單個 API 調用,直接進行測試即可
-
對於 updateOder,它依賴於 createOrder 的結果,所以我們將它們組合起來在 Journey 中測試,orderJourneyTest 裏面可以組合 createOrder -> getOrder -> updateOrder
k6 的 executor 選擇
k6 提供了多個 executor,不同的 executor 會以不同的方式去執行測試。我們可以根據項目的需求來選擇不同的 executor 來執行測試。
讓性能測試在 CI 上跑起來 - 集成 TeamCity
k6 官方提供了目前主流 CI 工具的 How to 文檔,非常容易上手。
唯一需要注意的點就是需要手動設置 thresholds,當性能結果不達標時,k6 會返回非 0 讓 CI 知道 test 失敗。
展示報告 - 集成 New Relic
數據的採集
k6 支持多種數據數據可視化工具,例如 Datadog,New Relic,Grafana 等,加個參數就可以輕鬆搞定。我們用的是 New Relic,通過 K6_STATSD_ENABLE_TAGS=true 配置,可以方便的通過 k6 提供的 tag 進行數據分類,分類統計不同 API,Journey 的性能數據。
指標的展示
指標展示主要是在數據可視化平臺上,通過自定義各種圖表展示性能指標
指標的核對
這裏其實是對上面的指標進行覈對,以保證我們設置的指標是準確的,爲後續性能分析做準備
測試執行 & 結果分析及調優
測試執行
在執行測試時,我們需要分析出影響性能的因素,並儘量控制變量,從而對多次的執行結果進行對比分析,例如都在 pipeline 上執行來減少網絡影響,定期檢查數據庫數據量,關注 K8s 的 pod 數量等等。結合我們的項目特點,我們總結了以下一些因素:
- 數據庫數據量
我們系統從架構上來比較簡單清晰,後端用到了 AWS DynamoDB,所以數據量會對性能有較大的影響,特別是查詢類,計算類的 API,這裏就需要了解用戶各個維度的數據量,比如每個月,每天等。
- 請求的 body 大小
這主要是針對 post 和 put 類接口,因爲涉及到文件上傳,所以文件大小也會對性能有較大影響,需要了解正常用戶使用場景下,附件的大小範圍
- K8s pod 數量,開啓了 HPA 會觸發 Auto Scaling
測試中發現性能不穩定,後來發現是 UAT 環境開啓了 HPA 會觸發 Auto Scaling,所以在執行測試時,需要考慮不同的場景:
-
測試固定 pod 下的性能,方便優化對比性能
-
測試 Auto Scaling 的 Policy 有效性
- 網絡影響
這是一個比較通用的問題,測試時應注意網絡變化對性能指標的影響,防止變量太多,性能數據分析不準確
- 不同 API 的性能差距較大
這裏主要是用例設計時需要考慮,k6 會統計所有的請求數據,導致 API 之間會相互影響,數據失真
-
比如 token 獲取的數據也會被收集,導致實際的業務接口數據受到影響
-
再者像 delete 類的接口,對 create 有依賴,如果把兩個 API 一起測試,create API 的性能數據與 delete API 差距較大,導致 delete 接口的數據嚴重失真。可以通過 tag 進行篩選,拿到單個 API 的部分數據,比如 response time, 這種還是有意義的,像是 rps 這種數據,如果兩個一起跑的,主要還是取決於 create,這樣收集到的 rps 對 delete 來說意義不大了
- 多個後端 API 間的相互影響,例如文件上傳對性能的影響
由於我們是有 BFF 和 BE,BFF 會組合多個 BE,所以需要識別多個 BE 之間的相互影響,儘量保證能準確的測試到目標,減少其他 API 的影響。比如在準備單獨測試某個服務時,可以考慮不添加文件,避免文件服務的干擾
結果分析及優化
對於結果分析來說,k6 自身提供了豐富的 Metrics 可供查看,並且我們也集成了 New Relic,因此可結合這兩者來進行數據收集,分析及調優。
如上圖所示,New Relic 可以將收集到的數據以圖的形式展示出來,並且我們可以按照需求來定製化 Report,這裏不僅僅可以用 k6 收集的數據,還可以疊加一些 APM 的數據,比如 CPU,Memory,Pod 數量等信息。通過鼠標定位橫座標上的某一個點,可以清晰的看到該時刻對應的併發量,總請求數,響應時間,失敗率等等數據。
另外,在執行測試時,我們通過在控制變量的前提下,進行橫向對比,將同類 API 在相同的配置下,對性能數據進行比較,如果數據相差明顯,則可以進一步調查。也可以通過工具對請求進行深入調查,拆解請求中各個模塊的耗時,找到最終的原因。
這裏舉兩個例子來說明這個過程。
案例 1 - 某獲取配置類信息 API
此 API 邏輯比較簡單,主要是讀取一些配置信息,然後做一些簡單的處理返回即可。
運行完測試後,http_req_duration 的平均值大概在 1s 左右,平均 rps 在 108 左右,而且 VU 最高達到了 300,說明此時已經拉滿了用戶,還有 0.7% 的錯誤。而其他需要查詢數據庫的 API 同樣的設置下,http_req_duration 只有 23ms,rps 有 204,VU 最高才到 76。這個 API 只是取一些配置信息,沒有其他太複雜的操作,也不用訪問數據庫,顯然這個性能數據是異常的,於是拉着 Dev 一起先排查一下邏輯,發現是配置文件內容的緩存邏輯有問題,每次請求都會去讀配置文件,導致性能數據異常。
在修改完之後,相同配置下,http_req_duration 爲 12ms,平均 rps 爲 145,VU 最高爲 50,錯誤率爲 0,很顯然,這個數據說明我們還可以繼續加大 Rate,當把 Rate 加到 500 時,平均的 http_req_duration 依舊是 12ms,VU 最大也才 80,依舊沒有到達瓶頸,由此可見修改後性能提升非常明顯。
案例 2 - 某 getAPI
這個 API 是一個 get 類型的 API,職責是去數據庫中獲取一個值,沒有其他額外操作。
運行完測試後,http_req_duration 的平均值大概在 320ms 左右,橫向對比其他 get API 能夠發現 duration 的結果是非常不合理的。但是 k6 只給出最後的運行結果,我們無法從這些結果中得知具體的問題在哪。好在 new relic 上提供了一些具體的 API 信息,其中有一項中提供了 API 的詳細調用流程,以及每一流程中花費的具體時間。由於項目安全需要,這裏以 new relic 提供的圖爲例。
從圖中,可以清楚的看到 API 的 service 調用流程圖,以及與不同的 service 互相 call 的個數。並還能清楚地看到每一步花費的時間,從而找到最費時間的那一步調用。
最後根據這個圖,我們發現原本只是去數據庫取一個值回來,卻由於實現方式不對,導致了和數據庫之間產生了 200 多個 call。這才使得 response time 高達 320ms。經過重新編碼後,該 API 的 response time 降到了 20ms,性能提升了 15 倍。
寫在最後
此次性能測試複雜度較高,非一兩人之力能夠完成,作爲 QA,我們可以主導事情的發生,併成爲其中的主力承擔者,要及時提出問題和尋求幫助,通過團隊的協作,讓問題儘快得到解決,最終順利完成性能測試任務。
Thoughtworks 洞見 最新技術雷達 / 各類技術乾貨 / 精選職位招聘 / 精彩活動預告 / 經典案例故事,就在 Thoughtworks。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/RFUCejMqSfwRK9xm-G-DFA