讓我們一起編寫一個(gè)接口壓測(cè)工具
前言
前段時(shí)間有個(gè)項(xiàng)目即將上線,需要對(duì)其中的核心接口進(jìn)行壓測(cè);由于我們的接口是 gRPC 協(xié)議,找了一圈發(fā)現(xiàn)壓測(cè)工具并不像 HTTP 那么多。
最終發(fā)現(xiàn)了 ghz 這個(gè)工具,功能也非常齊全。
事后我在想為啥做 gRPC 壓測(cè)的工具這么少,是有什么難點(diǎn)嘛?為了驗(yàn)證這個(gè)問題于是我準(zhǔn)備自己寫一個(gè)工具。
特性
前前后后大概花了個(gè)周末的時(shí)間完成了相關(guān)功能。
https://github.com/crossoverJie/ptg/
也是一個(gè)命令行工具,使用起來(lái)效果如上圖;完整的命令如下:
- NAME:
- ptg - Performance testing tool (Go)
- USAGE:
- ptg [global options] command [command options] [arguments...]
- COMMANDS:
- help, h Shows a list of commands or help for one command
- GLOBAL OPTIONS:
- --thread value, -t value -t 10 (default: 1 thread)
- --Request value, --proto value -proto http/grpc (default: http)
- --protocol value, --pf value -pf /file/order.proto
- --fully-qualified value, --fqn value -fqn package.Service.Method
- --duration value, -d value -d 10s (default: Duration of test in seconds, Default 10s)
- --request value, -c value -c 100 (default: 100)
- --HTTP value, -M value -m GET (default: GET)
- --bodyPath value, --body value -body bodyPath.json
- --header value, -H value HTTP header to add to request, e.g. "-H Content-Type: application/json"
- --target value, --tg value http://gobyexample.com/grpc:127.0.0.1:5000
- --help, -h show help (default: false)
考慮到受眾,所以同時(shí)支持 HTTP 與 gRPC 接口的壓測(cè)。
做 gRPC 壓測(cè)時(shí)所需的參數(shù)要多一些:
- ptg -t 10 -c 100 -proto grpc -pf /xx/xx.proto -fqn hello.Hi.Say -body test.json -tg "127.0.0.1:5000"
比如需要提供 proto 文件的路徑、具體的請(qǐng)求參數(shù)還有請(qǐng)求接口的全路徑名稱。
目前只支持最常見的 unary call 調(diào)用,后續(xù)如果有需要的話也可以 stream。
同時(shí)也支持壓測(cè)時(shí)間、次數(shù)兩種壓測(cè)方式。
安裝
想體驗(yàn)度朋友如果本地有 go 環(huán)境那直接運(yùn)行:
- go get github.com/crossoverJie/ptg
沒有環(huán)境也沒關(guān)系,可以再 release 頁(yè)面下載與自己環(huán)境對(duì)應(yīng)的版本解壓使用。
https://github.com/crossoverJie/ptg/releases
設(shè)計(jì)模式
整個(gè)開發(fā)過程中還是有幾個(gè)點(diǎn)想和大家分享,首先是設(shè)計(jì)模式。
因?yàn)橐婚_始設(shè)計(jì)時(shí)就考慮到需要支持不同的壓測(cè)模式(次數(shù)、時(shí)間;后續(xù)也可以新增其他的模式)。
所以我便根據(jù)壓測(cè)的生命周期定義了一套接口:
- type (
- Model interface {
- Init()
- Run()
- Finish()
- PrintSate()
- Shutdown()
- }
- )
從名字也能看出來(lái),分別對(duì)應(yīng):
- 壓測(cè)初始化
- 運(yùn)行壓測(cè)
- 停止壓測(cè)
- 打印壓測(cè)信息
- 關(guān)閉程序、釋放資源
然后在兩個(gè)不同的模式中進(jìn)行實(shí)現(xiàn)。
這其實(shí)就是一個(gè)典型的依賴倒置原則。
程序員要依賴于抽象接口編程、不要依賴具體的實(shí)現(xiàn)。
其實(shí)大白話就是咱們 Java 里常說(shuō)的面向接口編程;這個(gè)編程技巧在開發(fā)框架、SDK或是多種實(shí)現(xiàn)的業(yè)務(wù)中常用。
好處當(dāng)然是顯而易見:當(dāng)接口定義好之后,不同的業(yè)務(wù)只需要根據(jù)接口實(shí)現(xiàn)自己的業(yè)務(wù)就好,完全不會(huì)互相影響;維護(hù)、擴(kuò)展都很方便。
支持 HTTP 和 gRPC 也是同理實(shí)現(xiàn)的:
- type (
- Client interface {
- Request() (*Response, error)
- }
- )
當(dāng)然前提得是前期的接口定義需要考慮周全、不能之后頻繁修改接口定義,這樣的接口就沒有意義了。
goroutine
另外一點(diǎn)則是不得不感嘆 goroutine+select+channel 這套并發(fā)編程模型真的好用,并且也非常容易理解。
很容易就能寫出一套并發(fā)代碼:
- func (c *CountModel) Init() {
- c.wait.Add(c.count)
- c.workCh = make(chan *Job, c.count)
- for i := 0; i < c.count; i++ {
- go func() {
- c.workCh <- &Job{
- thread: thread,
- duration: duration,
- count: c.count,
- target: target,
- }
- }()
- }
- }
比如這里需要初始化 N 個(gè) goroutine 執(zhí)行任務(wù),只需要使用 go 關(guān)鍵字,然后利用 channel 將任務(wù)寫入。
當(dāng)然在使用 goroutine+channel 配合使用時(shí)也得小心 goroutine 泄露的問題;簡(jiǎn)單來(lái)說(shuō)就是在程序員退出時(shí)還有 goroutine 沒有退出。
比較常見的例子就是向一個(gè)無(wú)緩沖的 channel 中寫數(shù)據(jù),當(dāng)沒有其他 goroutine 來(lái)讀取數(shù)時(shí),寫入的 goroutine 就會(huì)被一直阻塞,最終導(dǎo)致泄露。
總結(jié)
有 gRPC 接口壓測(cè)需求的朋友歡迎試用,提出寶貴意見;當(dāng)然 HTTP 接口也可以。
源碼地址:https://github.com/crossoverJie/ptg/