RESTful API 時代,我們有許多簡單好用的測試工具:有酷炫的 Postman,有命令列控愛用的 HTTPie,當然也有硬漢必備的萬用瑞士刀 curl。
那麼,gRPC 呢?
這篇文章介紹兩個好用的小工具:gRPCurl 及 ghz,一個是輸入輸出介面測試工具,另一個是壓測工具,也順便介紹一些簡化測試的技巧。
待測程式
待測程式放在 https://github.com/William-Yeh/grpcurl-and-ghz-demo
檔案簡介:
.
├── README.md
├── build.sh ← 編譯命令
├── go.mod
├── go.sum
├── out ← 編譯後的可執行檔
│ ├── server
│ └── server-new
├── routeguide ← gRPC 介面定義;原封不動取自 "gRPC-Go" 專案
│ ├── route_guide.pb.go
│ └── route_guide.proto
├── server ← server 程式;原封不動取自 "gRPC-Go" 專案
│ └── server.go
├── server-new ← 由我修改過的新版 server 程式
│ └── server-new.go
├── testdata.dat ← 給 gRPCurl 的測試資料
└── testdata.json ← 給 ghz 的測試資料
為了方便起見,我挑選 “gRPC-Go” 專案裡面的 “the route guide server and client” 範例做為待測程式。
這隻 server 程式透過 gRPC 提供 RouteGuide
服務,我們這次只會測試其中的 RecordRoute
呼叫。以下是從 gRPC 介面定義檔 route_guide.proto
摘錄我們會用到的部份:
package routeguide;
service RouteGuide {
//...
// A client-to-server streaming RPC.
//
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
}
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
message RouteSummary {
int32 point_count = 1;
int32 feature_count = 2;
int32 distance = 3;
int32 elapsed_time = 4;
}
//...
我也針對 server 程式小改三個地方,弄出另一個 server-new 程式:
$ diff server/server.go server-new/server-new.go
42a43
> "google.golang.org/grpc/reflection"
55c56
< port = flag.Int("port", 10000, "The server port")
---
> port = flag.Int("port", 20000, "The server port")
243a245,248
>
> // Register reflection service on gRPC server.
> reflection.Register(grpcServer)
>
簡單來說,新舊兩版的差別是:
- 舊版 server 的 gRPC port = 10000,新版 server-new 則是 20000。
- 新版 server-new 支援 server reflection 功能。稍後會再說明這是什麼。
實驗環境
實驗所需環境:
先來編譯程式:
$ ./build.sh
成功後,會在 out
目錄拿到兩份執行檔:
-
out/server
:原封不動來自 “gRPC-Go” 專案的 server 程式。 -
out/server-new
:由我小小修改過的新程式。
進行實驗時,建議將你的終端機配置成這樣:
然後,請分別啟動 out/server
及 out/server-new
程式:
# 舊程式,跑在 10000 port
$ out/server
# 新程式,跑在 20000 port
$ out/server-new
一切就緒,準備要來測試它們了。
實驗一:搭配 proto 檔
先針對舊版的 server 來實驗。
gRPCurl
gRPCurl,顧名思義,是在向硬漢必備的萬用瑞士刀 curl 致敬。不妨將它視為 gRPC 版的 curl。
gRPCurl 使用上最主要的差別是,因應 gRPC 的特性,必須餵給它 proto 檔案,才會知道該如何封裝訊息格式。
譬如說,我們可將 proto 檔案的路徑寫在 -import-path
中、將 proto 檔案名稱寫在 -proto
中、將參數寫在 -d
中,再呼叫 server 的遠端程序:
$ grpcurl -plaintext \
-d '{"latitude":-460000000,"longitude":-1160000000} {"latitude":720000000,"longitude":-540000000}' \
-import-path ./routeguide \
-proto route_guide.proto \
127.0.0.1:10000 \
routeguide.RouteGuide.RecordRoute
{
"pointCount": 2,
"distance": 13975745
}
我們也可將事先備妥的資料檔餵給 gRPCurl。像是含有 100 筆資料的 testdata.dat
:
$ grpcurl -plaintext -d '@' \
-import-path ./routeguide \
-proto route_guide.proto \
127.0.0.1:10000 \
routeguide.RouteGuide.RecordRoute \
< testdata.dat
{
"pointCount": 100,
"distance": 1003784333
}
ghz
ghz 壓測工具,是這麼自我介紹的:
Simple gRPC benchmarking and load testing tool inspired by hey and grpcurl.
所以,從命令列參數及統計結果上,都可看出它們的影響。
譬如說,我們可將 proto 檔案的路徑寫在 --import-path
中、將 proto 檔案名稱寫在 --proto
中、將參數寫在 --data
中,再呼叫 server 的遠端程序:
$ ghz --insecure -z 20s \
--data '[{"latitude":-460000000,"longitude":-1160000000},{"latitude":720000000,"longitude":-540000000}]' \
--import-paths ./routeguide \
--proto route_guide.proto \
--call routeguide.RouteGuide.RecordRoute \
127.0.0.1:10000
壓測 20 秒,結果如下:
Summary:
Count: 548082
Total: 20.00 s
Slowest: 50.36 ms
Fastest: 0.12 ms
Average: 1.74 ms
Requests/sec: 27403.72
Response time histogram:
0.123 [1] |
5.147 [543463] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
10.170 [4297] |
15.194 [271] |
20.217 [8] |
25.241 [3] |
30.264 [1] |
35.288 [0] |
40.311 [0] |
45.335 [0] |
50.358 [1] |
Latency distribution:
10 % in 0.90 ms
25 % in 1.21 ms
50 % in 1.58 ms
75 % in 2.04 ms
90 % in 2.66 ms
95 % in 3.24 ms
99 % in 4.93 ms
Status code distribution:
[Canceled] 33 responses
[Unavailable] 4 responses
[OK] 548045 responses
Error distribution:
[33] rpc error: code = Canceled desc = grpc: the client connection is closing
[4] rpc error: code = Unavailable desc = transport is closing
同樣的,我們也可將事先備妥的資料檔餵給 ghz。像是含有 100 筆資料的 testdata.json
:
$ ghz --insecure --data=@ -z 20s \
--import-paths ./routeguide \
--proto route_guide.proto \
--call routeguide.RouteGuide.RecordRoute \
127.0.0.1:10000 \
< testdata.json
壓測 20 秒,結果如下:
Summary:
Count: 788680
Total: 20.00 s
Slowest: 28.02 ms
Fastest: 0.13 ms
Average: 1.23 ms
Requests/sec: 39432.62
Response time histogram:
0.126 [1] |
2.915 [772021] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
5.705 [15003] |∎
8.495 [1000] |
11.285 [458] |
14.074 [95] |
16.864 [10] |
19.654 [12] |
22.444 [3] |
25.233 [5] |
28.023 [40] |
Latency distribution:
10 % in 0.65 ms
25 % in 0.86 ms
50 % in 1.11 ms
75 % in 1.42 ms
90 % in 1.88 ms
95 % in 2.30 ms
99 % in 3.57 ms
Status code distribution:
[OK] 788648 responses
[Canceled] 32 responses
Error distribution:
[32] rpc error: code = Canceled desc = grpc: the client connection is closing
實驗二:不需搭配 proto 檔
使用前,每次都要先備妥待測程式的 proto 檔,其實也滿麻煩的。萬一複雜的 proto 檔案又去 import 其他 proto 檔 1,可能就得條列一堆 -import-path
或 --import-path
命令列參數給 gRPCurl 及 ghz。
有沒有省力一點的方法?
有的,就是透過 server reflection 功能。
Server reflection
我以這次的範例程式 server-new 為例,說明如何加上 server reflection 功能。
首先,請加上 google.golang.org/grpc/reflection
套件:
import "google.golang.org/grpc/reflection"
接著,在 grpcServer.Serve
之前,呼叫 reflection.Register
:
grpcServer := grpc.NewServer(opts...)
//...
// Register reflection service on gRPC server.
reflection.Register(grpcServer)
grpcServer.Serve(lis)
只需要這兩個步驟,你的 gRPC 程式本身就具有 server reflection 功能,對方不再需要 proto 檔案就能直接進行遠端呼叫。
針對新版的 server-new 來實驗看看吧。
gRPCurl
我們可用 list
指令查詢 server-new 提供哪些服務:
$ grpcurl -plaintext 127.0.0.1:20000 list
grpc.reflection.v1alpha.ServerReflection
routeguide.RouteGuide
可用 describe
指令查詢 server-new 提供服務的介面:
$ grpcurl -plaintext 127.0.0.1:20000 describe
grpc.reflection.v1alpha.ServerReflection is a service:
service ServerReflection {
rpc ServerReflectionInfo ( stream .grpc.reflection.v1alpha.ServerReflectionRequest ) returns ( stream .grpc.reflection.v1alpha.ServerReflectionResponse );
}
routeguide.RouteGuide is a service:
service RouteGuide {
rpc GetFeature ( .routeguide.Point ) returns ( .routeguide.Feature );
rpc ListFeatures ( .routeguide.Rectangle ) returns ( stream .routeguide.Feature );
rpc RecordRoute ( stream .routeguide.Point ) returns ( .routeguide.RouteSummary );
rpc RouteChat ( stream .routeguide.RouteNote ) returns ( stream .routeguide.RouteNote );
}
可用 describe
指令進一步查詢某參數的具體格式:
$ grpcurl -plaintext 127.0.0.1:20000 describe .routeguide.Point
routeguide.Point is a message:
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
可看出,gRPCurl 不需要 proto 檔案,就能直接向 server-new 查詢遠端呼叫所需知道的介面細節。
最後,讓我們將含有 100 筆資料的 testdata.dat
餵給 gRPCurl 來測測看:
$ grpcurl -plaintext -d '@' \
127.0.0.1:20000 \
routeguide.RouteGuide.RecordRoute \
< testdata.dat
{
"pointCount": 100,
"distance": 1003784333
}
有了 server reflection 功能,是不是方便多了?如果你有權修改原始程式,這是值得好好考慮的,可讓測試工作輕鬆一點。
當然啦,你可以考慮在 production 環境關掉這功能;但在測試環境中,這真的很方便。
ghz
儘管 server reflection 通常不會在 production 環境啟用,不過我還是很好奇:server reflection 對執行效率的影響有多少?尤其是涉及 marshalling。
用 ghz 試試看吧!
$ ghz --insecure --data=@ -z 20s \
--call routeguide.RouteGuide.RecordRoute \
127.0.0.1:20000 \
< testdata.json
壓測 20 秒,結果如下:
Summary:
Count: 826524
Total: 20.00 s
Slowest: 19.79 ms
Fastest: 0.12 ms
Average: 1.17 ms
Requests/sec: 41317.73
Response time histogram:
0.118 [1] |
2.085 [776730] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
4.052 [45892] |∎∎
6.019 [3018] |
7.986 [550] |
9.953 [112] |
11.920 [74] |
13.887 [51] |
15.854 [37] |
17.821 [17] |
19.788 [1] |
Latency distribution:
10 % in 0.62 ms
25 % in 0.82 ms
50 % in 1.06 ms
75 % in 1.36 ms
90 % in 1.80 ms
95 % in 2.20 ms
99 % in 3.38 ms
Status code distribution:
[OK] 826483 responses
[Canceled] 40 responses
[Unavailable] 1 responses
Error distribution:
[40] rpc error: code = Canceled desc = grpc: the client connection is closing
[1] rpc error: code = Unavailable desc = transport is closing
雖然這還不算非常嚴謹的實驗,但可看出,沒有 server reflection 功能的 server 版本,與有此功能的 server-new 版本,執行效率沒有顯著差異。
因此,server reflection 功能,值得嘗試。
總結
本篇文章,介紹兩個好用的 gRPC 測試小工具:輸入輸出介面測試工具 gRPCurl,以及壓測工具 ghz。最後並推薦 server reflection 功能來簡化 gRPC 測試工作。
ghz 也有 web 介面,目前是 beta 狀態。有興趣的,請去 ghz 官網看看。
-
Proto 檔案也可以 import 其他 proto 檔。詳見 https://developers.google.com/protocol-buffers/docs/proto3#importing-definitions ↩︎