RESTful API 時代,我們有許多簡單好用的測試工具:有酷炫的 Postman,有命令列控愛用的 HTTPie,當然也有硬漢必備的萬用瑞士刀 curl

那麼,gRPC 呢?

這篇文章介紹兩個好用的小工具:gRPCurlghz,一個是輸入輸出介面測試工具,另一個是壓測工具,也順便介紹一些簡化測試的技巧。

待測程式

待測程式放在 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 功能。稍後會再說明這是什麼。

實驗環境

實驗所需環境:

  • Go 1.14 以上。
  • gRPCurl 1.5.0 以上。
  • ghz 0.51.0 以上。

先來編譯程式:

$ ./build.sh

成功後,會在 out 目錄拿到兩份執行檔:

  • out/server:原封不動來自 “gRPC-Go” 專案的 server 程式。

  • out/server-new:由我小小修改過的新程式。

進行實驗時,建議將你的終端機配置成這樣:

終端機建議配置

終端機建議配置

然後,請分別啟動 out/serverout/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 官網看看。


  1. Proto 檔案也可以 import 其他 proto 檔。詳見 https://developers.google.com/protocol-buffers/docs/proto3#importing-definitions ↩︎