作者 | 瞿勛和涂佳瑤
背景
項(xiàng)目的目標(biāo)是為客戶交付一個ToC的APP,其后端是基于RESTful的微服務(wù)架構(gòu),同時后端還采用了Protobuf協(xié)議來提高傳輸效率。在最終上線之前,我們需要執(zhí)行性能測試以確定系統(tǒng)在正常和預(yù)期峰值負(fù)載條件下的表現(xiàn),從而識別應(yīng)用程序的最大運(yùn)行容量以及存在的瓶頸,并針對性能問題進(jìn)行優(yōu)化以提升用戶體驗(yàn)。
性能測試是一個較為復(fù)雜的任務(wù),包括確定性能測試目標(biāo),工具選擇,腳本開發(fā),CI集成,結(jié)果分析,性能調(diào)優(yōu)等過程,需要QA,Dev,Devops協(xié)力合作。本文將對這一系列過程進(jìn)行詳細(xì)描述。
為什么選擇k6
在得知需要做性能測試后,我們就開始針對性能測試做了一番調(diào)研,在閱讀了一些性能測試工具對比的文章后,最終挑選了k6,locust和Gatling做了進(jìn)一步對比,下面是對比的結(jié)果。
對我們來說,k6的優(yōu)勢在于:
- k6支持TypeScript,由于項(xiàng)目上已經(jīng)有TypeScript使用經(jīng)驗(yàn),因此該工具學(xué)習(xí)成本相對更少;
- k6本身支持metrics的輸出,可以滿足大部分metrics的需求,有需要還可以進(jìn)行自定義;
- k6官方支持與多種CI工具,數(shù)據(jù)可視化系統(tǒng)的集成,開箱即用;
- Gatling支持Scala/Java/Kotlin,項(xiàng)目上沒有使用相關(guān)的技術(shù)棧,需要和客戶申請,成本高于k6。
動手寫第一個case
有了上面的基礎(chǔ),我們便開始嘗試在項(xiàng)目中集成k6,在選了一個簡單的API寫第一個case的時候,發(fā)現(xiàn)有以下一些挑戰(zhàn)需要解決:
挑戰(zhàn)1-獲取Access Token和保證token時效性
由于當(dāng)前項(xiàng)目的API都集成了OAuth,任何操作都要有一個有效的用戶和Access Token,因此需要提前生成token和測試數(shù)據(jù)。這一部分因?yàn)轫?xiàng)目的不同會有一些差異,需要具體情況具體分析。在此次測試中具體包括以下幾項(xiàng):
- 用戶賬號準(zhǔn)備,比如生成200個用戶,并進(jìn)行一系列的前置處理,讓它們變成可用的正常測試賬號,并且需根據(jù)項(xiàng)目安全規(guī)范,保存到合適地方,比如AWS Secrets Manager或者AWS Parameter Store,這里的賬號可以復(fù)用。
- token生成,運(yùn)行測試前,生成最新的有效token,執(zhí)行測試的時候只需要去讀取token數(shù)據(jù)。
- token刷新,由于token基本上都具有時效性,如果有效時間短,還需要考慮renew token,這里我們采用refresh token去獲取新access token的方式。
- 需要注意的是測試過程中刷新token會計(jì)入請求,對性能測試數(shù)據(jù)會有些許影響,刷新機(jī)制需要納入考慮范圍。
挑戰(zhàn)2-Protobuf數(shù)據(jù)的編解碼
下圖簡要說明了前后端的架構(gòu),Mobile和BFF是以Protobuf格式做數(shù)據(jù)交換,BFF和Backend是以Json格式做數(shù)據(jù)交換。
我們的性能測試是針對BFF的,因此需要根據(jù)項(xiàng)目中定義的Protobuf格式對請求數(shù)據(jù)進(jìn)行編碼再發(fā)送給BFF,從BFF接受到響應(yīng)數(shù)據(jù)時也需要根據(jù)Protobuf定義的響應(yīng)格式進(jìn)行解碼,從而解析出想要的數(shù)據(jù)。
另外由于性能測試采用的是TypeScript語言,我們還需要將Protobuf文件編譯成TS版本,這一點(diǎn)在Protobuf官方文檔上給出了解決辦法,可以很容易的生成TS版本代碼。
由于每個API的編解碼結(jié)構(gòu)都是一份單獨(dú)的proto,因此還涉及到代碼復(fù)用的問題,需要設(shè)計(jì)合適的方法,讓不同的API只需要提供對應(yīng)的encode和decode schema即可。
當(dāng)解決掉前面的兩個挑戰(zhàn)后,可以初步得到符合項(xiàng)目需求的測試框架。
├── protobuf file/ --- protobuf文件
├── dist/ --- ts轉(zhuǎn)成js的測試文件
└── src/
├── command/ --- 一些腳本文件
├── config/ --- config文件
├── httpClient/ --- http client
├── ProtobufSchema/ --- 編譯好的protobuf文件
├── test/ --- 測試case
└── testAccount/ --- 測試賬戶
優(yōu)化項(xiàng)目&集成CI&可視化報(bào)告
測試用例設(shè)計(jì)
當(dāng)測試case逐漸增多后,我們對測試用例進(jìn)行了多次的調(diào)整,例如對API進(jìn)行了分類,并通過不同的方式來對他們進(jìn)行性能測試。
獨(dú)立API
獨(dú)立API是指不依賴其他接口提供參數(shù)輸入,即可完成請求的API,例如部分Get類API。
非獨(dú)立API
非獨(dú)立API是指依賴于其他API結(jié)果作為參數(shù)輸入才可完成請求的API,例如部分Put、Delete類API。由于此類API依賴于其他API的結(jié)果數(shù)據(jù),無法單獨(dú)做性能測試,在本次性能測試中以整體journey的形式來測這些非獨(dú)立的API,在測試case中將前一步的結(jié)果傳給后一步,從而完成整體的journey測試。
我們通過一個例子來說明,我們的test case目錄結(jié)構(gòu)如下:
└── 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是獨(dú)立API,可以方便的進(jìn)行單個API調(diào)用,直接進(jìn)行測試即可;
- 對于updateOder,它依賴于createOrder的結(jié)果,所以我們將它們組合起來在Journey中測試,orderJourneyTest里面可以組合createOrder -> getOrder -> updateOrder。
k6的executor選擇
k6提供了多個executor,不同的executor會以不同的方式去執(zhí)行測試。我們可以根據(jù)項(xiàng)目的需求來選擇不同的executor來執(zhí)行測試。
讓性能測試在CI上跑起來-集成TeamCity
k6官方提供了目前主流CI工具的How to文檔,非常容易上手。
唯一需要注意的點(diǎn)就是需要手動設(shè)置thresholds,當(dāng)性能結(jié)果不達(dá)標(biāo)時,k6會返回非0讓CI知道test失敗。
展示報(bào)告-集成New Relic
(1) 數(shù)據(jù)的采集
k6支持多種數(shù)據(jù)數(shù)據(jù)可視化工具,例如Datadog,New Relic,Grafana等,加個參數(shù)就可以輕松搞定。我們用的是New Relic,通過K6_STATSD_ENABLE_TAGS=true配置,可以方便的通過k6提供的tag進(jìn)行數(shù)據(jù)分類,分類統(tǒng)計(jì)不同API,Journey的性能數(shù)據(jù)。
(2) 指標(biāo)的展示
指標(biāo)展示主要是在數(shù)據(jù)可視化平臺上,通過自定義各種圖表展示性能指標(biāo)
(3) 指標(biāo)的核對
這里其實(shí)是對上面的指標(biāo)進(jìn)行核對,以保證我們設(shè)置的指標(biāo)是準(zhǔn)確的,為后續(xù)性能分析做準(zhǔn)備
測試執(zhí)行&結(jié)果分析及調(diào)優(yōu)
測試執(zhí)行
在執(zhí)行測試時,我們需要分析出影響性能的因素,并盡量控制變量,從而對多次的執(zhí)行結(jié)果進(jìn)行對比分析,例如都在pipeline上執(zhí)行來減少網(wǎng)絡(luò)影響,定期檢查數(shù)據(jù)庫數(shù)據(jù)量,關(guān)注K8s的pod數(shù)量等等。結(jié)合我們的項(xiàng)目特點(diǎn),我們總結(jié)了以下一些因素:
(1) 數(shù)據(jù)庫數(shù)據(jù)量
我們系統(tǒng)從架構(gòu)上來比較簡單清晰,后端用到了AWS DynamoDB,所以數(shù)據(jù)量會對性能有較大的影響,特別是查詢類,計(jì)算類的API,這里就需要了解用戶各個維度的數(shù)據(jù)量,比如每個月,每天等。
(2) 請求的body大小
這主要是針對post和put類接口,因?yàn)樯婕暗轿募蟼?,所以文件大小也會對性能有較大影響,需要了解正常用戶使用場景下,附件的大小范圍
(3) K8s pod數(shù)量,開啟了HPA會觸發(fā)Auto Scaling
測試中發(fā)現(xiàn)性能不穩(wěn)定,后來發(fā)現(xiàn)是UAT環(huán)境開啟了HPA會觸發(fā)Auto Scaling,所以在執(zhí)行測試時,需要考慮不同的場景:
- 測試固定pod下的性能,方便優(yōu)化對比性能
- 測試Auto Scaling的Policy有效性
(4) 網(wǎng)絡(luò)影響
這是一個比較通用的問題,測試時應(yīng)注意網(wǎng)絡(luò)變化對性能指標(biāo)的影響,防止變量太多,性能數(shù)據(jù)分析不準(zhǔn)確
(5) 不同API的性能差距較大
這里主要是用例設(shè)計(jì)時需要考慮,k6會統(tǒng)計(jì)所有的請求數(shù)據(jù),導(dǎo)致API之間會相互影響,數(shù)據(jù)失真:
- 比如token獲取的數(shù)據(jù)也會被收集,導(dǎo)致實(shí)際的業(yè)務(wù)接口數(shù)據(jù)受到影響;
- 再者像delete類的接口,對create有依賴,如果把兩個API一起測試,create API的性能數(shù)據(jù)與delete API差距較大,導(dǎo)致delete接口的數(shù)據(jù)嚴(yán)重失真。可以通過tag進(jìn)行篩選,拿到單個API的部分?jǐn)?shù)據(jù),比如response time, 這種還是有意義的,像是rps這種數(shù)據(jù),如果兩個一起跑的,主要還是取決于create,這樣收集到的rps對delete來說意義不大了。
(6) 多個后端API間的相互影響,例如文件上傳對性能的影響
由于我們是有BFF和BE,BFF會組合多個BE,所以需要識別多個BE之間的相互影響,盡量保證能準(zhǔn)確的測試到目標(biāo),減少其他API的影響。比如在準(zhǔn)備單獨(dú)測試某個服務(wù)時,可以考慮不添加文件,避免文件服務(wù)的干擾
結(jié)果分析及優(yōu)化
對于結(jié)果分析來說,k6自身提供了豐富的Metrics可供查看,并且我們也集成了New Relic,因此可結(jié)合這兩者來進(jìn)行數(shù)據(jù)收集,分析及調(diào)優(yōu)。
原圖鏈接:https://k6.io/docs/static/f9df206f5a86e9b4c59d2bdb6a9e351f/485a2/new-relic-dashboard.webp
如上圖所示,New Relic可以將收集到的數(shù)據(jù)以圖的形式展示出來,并且我們可以按照需求來定制化Report,這里不僅僅可以用k6收集的數(shù)據(jù),還可以疊加一些APM的數(shù)據(jù),比如CPU,Memory,Pod數(shù)量等信息。通過鼠標(biāo)定位橫坐標(biāo)上的某一個點(diǎn),可以清晰的看到該時刻對應(yīng)的并發(fā)量,總請求數(shù),響應(yīng)時間,失敗率等等數(shù)據(jù)。
另外,在執(zhí)行測試時,我們通過在控制變量的前提下,進(jìn)行橫向?qū)Ρ?,將同類API在相同的配置下,對性能數(shù)據(jù)進(jìn)行比較,如果數(shù)據(jù)相差明顯,則可以進(jìn)一步調(diào)查。也可以通過工具對請求進(jìn)行深入調(diào)查,拆解請求中各個模塊的耗時,找到最終的原因。
這里舉兩個例子來說明這個過程。
案例1 - 某獲取配置類信息API
此API邏輯比較簡單,主要是讀取一些配置信息,然后做一些簡單的處理返回即可。
運(yùn)行完測試后,http_req_duration的平均值大概在1s左右,平均rps在108左右,而且VU最高達(dá)到了300,說明此時已經(jīng)拉滿了用戶,還有0.7%的錯誤。而其他需要查詢數(shù)據(jù)庫的API同樣的設(shè)置下,http_req_duration只有23ms,rps有204,VU最高才到76。這個API只是取一些配置信息,沒有其他太復(fù)雜的操作,也不用訪問數(shù)據(jù)庫,顯然這個性能數(shù)據(jù)是異常的,于是拉著Dev一起先排查一下邏輯,發(fā)現(xiàn)是配置文件內(nèi)容的緩存邏輯有問題,每次請求都會去讀配置文件,導(dǎo)致性能數(shù)據(jù)異常。
在修改完之后,相同配置下,http_req_duration為12ms,平均rps為145,VU最高為50,錯誤率為0,很顯然,這個數(shù)據(jù)說明我們還可以繼續(xù)加大Rate,當(dāng)把Rate加到500時,平均的http_req_duration依舊是12ms,VU最大也才80,依舊沒有到達(dá)瓶頸,由此可見修改后性能提升非常明顯。
案例2 - 某getAPI
這個API是一個get類型的API,職責(zé)是去數(shù)據(jù)庫中獲取一個值,沒有其他額外操作。
運(yùn)行完測試后,http_req_duration的平均值大概在320ms左右,橫向?qū)Ρ绕渌鹓et API能夠發(fā)現(xiàn)duration的結(jié)果是非常不合理的。但是k6只給出最后的運(yùn)行結(jié)果,我們無法從這些結(jié)果中得知具體的問題在哪。好在new relic上提供了一些具體的API信息,其中有一項(xiàng)中提供了API的詳細(xì)調(diào)用流程,以及每一流程中花費(fèi)的具體時間。由于項(xiàng)目安全需要,這里以new relic提供的圖為例。
原圖鏈接:https://docs.newrelic.com/static/distributed-tracing-trace-details-page-1c064ef6a7607f95be583786b6af9251.png
從圖中,可以清楚的看到API的service調(diào)用流程圖,以及與不同的service互相call的個數(shù)。并還能清楚地看到每一步花費(fèi)的時間,從而找到最費(fèi)時間的那一步調(diào)用。
最后根據(jù)這個圖,我們發(fā)現(xiàn)原本只是去數(shù)據(jù)庫取一個值回來,卻由于實(shí)現(xiàn)方式不對,導(dǎo)致了和數(shù)據(jù)庫之間產(chǎn)生了200多個call。這才使得response time高達(dá)320ms。經(jīng)過重新編碼后,該API的response time降到了20ms,性能提升了15倍。
寫在最后
此次性能測試復(fù)雜度較高,非一兩人之力能夠完成,作為QA,我們可以主導(dǎo)事情的發(fā)生,并成為其中的主力承擔(dān)者,要及時提出問題和尋求幫助,通過團(tuán)隊(duì)的協(xié)作,讓問題盡快得到解決,最終順利完成性能測試任務(wù)。