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