avatar

gRPC效能比較

gPRC壓力測試

gRPC 有支援的程式語言很多,有 Python、golang、js、java 等等,所以以下會比較gopython兩個language的implementation performance

ghz-壓力測試工具

Simple gRPC benchmarking and load testing tool inspired by hey and grpcurl.
github
documentation

Installation

  • window
    這裡下載解壓縮就可以了
  • linux
    wget https://github.com/bojand/ghz/releases/download/v0.52.0/ghz_0.52.0_Linux_x86_64.tar.gz
    sudo tar -xvf ghz_0.52.0_Linux_x86_64.tar.gz
    Additionally consider adding the location directory in the PATH variable if you would like the ghz command to be available everywhere.

Usage

usage: ghz [<flags>] [<host>]

Flags:
-h, --help Show context-sensitive help (also try --help-long and --help-man).
--config= Path to the JSON or TOML config file that specifies all the test run settings.
--proto= The Protocol Buffer .proto file.
--protoset= The compiled protoset file. Alternative to proto. -proto takes precedence.
--call= A fully-qualified method name in 'package.Service/method' or 'package.Service.Method' format.
-i, --import-paths= Comma separated list of proto import paths. The current working directory and the directory of the protocol buffer file are automatically added to the import list.
--cacert= File containing trusted root certificates for verifying the server.
--cert= File containing client certificate (public key), to present to the server. Must also provide -key option.
--key= File containing client private key, to present to the server. Must also provide -cert option.
--cname= Server name override when validating TLS certificate - useful for self signed certs.
--skipTLS Skip TLS client verification of the server's certificate chain and host name.
--insecure Use plaintext and insecure connection.
--authority= Value to be used as the :authority pseudo-header. Only works if -insecure is used.
-c, --concurrency=50 Number of requests to run concurrently. Total number of requests cannot be smaller than the concurrency level. Default is 50.
-n, --total=200 Number of requests to run. Default is 200.
-q, --qps=0 Rate limit, in queries per second (QPS). Default is no rate limit.
-t, --timeout=20s Timeout for each request. Default is 20s, use 0 for infinite.
-z, --duration=0 Duration of application to send requests. When duration is reached, application stops and exits. If duration is specified, n is ignored. Examples: -z 10s -z 3m.
-x, --max-duration=0 Maximum duration of application to send requests with n setting respected. If duration is reached before n requests are completed, application stops and exits. Examples: -x 10s -x 3m.
--duration-stop="close" Specifies how duration stop is reported. Options are close, wait or ignore.
-d, --data= The call data as stringified JSON. If the value is '@' then the request contents are read from stdin.
-D, --data-file= File path for call data JSON file. Examples: /home/user/file.json or ./file.json.
-b, --binary The call data comes as serialized binary message or multiple count-prefixed messages read from stdin.
-B, --binary-file= File path for the call data as serialized binary message or multiple count-prefixed messages.
-m, --metadata= Request metadata as stringified JSON.
-M, --metadata-file= File path for call metadata JSON file. Examples: /home/user/metadata.json or ./metadata.json.
--stream-interval=0 Interval for stream requests between message sends.
--reflect-metadata= Reflect metadata as stringified JSON used only for reflection request.
-o, --output= Output path. If none provided stdout is used.
-O, --format= Output format. One of: summary, csv, json, pretty, html, influx-summary, influx-details. Default is summary.
--connections=1 Number of connections to use. Concurrency is distributed evenly among all the connections. Default is 1.
--connect-timeout=10s Connection timeout for the initial connection dial. Default is 10s.
--keepalive=0 Keepalive time duration. Only used if present and above 0.
--name= User specified name for the test.
--tags= JSON representation of user-defined string tags.
--cpus=12 Number of cpu cores to use.
--debug= The path to debug log file.
-e, --enable-compression Enable Gzip compression on requests.
-v, --version Show application version.

Args:
[<host>] Host and port to test.

Example proto :

syntax = "proto3";  

package pb;

service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
string greeting = 1;
}

message HelloResponse {
string reply = 1;
}

We can test the call:

ghz --insecure \
--proto ./hello.proto \
--call pb.HelloService.SayHello \
-d '{"greeting":"hello world"}' \
127.0.0.1:9999

--insecure : Use plaintext and insecure connection
proto: proto檔案路徑
call : proto 定義的RPC服務 <package.Service.Method>
-d : 傳入的參數
127.0.0.1:9999 : host gRPC service

在window 下實測後無法直在terminal用-d帶入參數,必須透過-D另外帶上json檔案路徑的方式不然會報錯

json example:

{"greeting":"hello world"}

command :

ghz --insecure \
--proto ./hello.proto \
--call pb.HelloService.SayHello \
-D task_data.json \
127.0.0.1:9999

Advanced usage

  • 可以將所有的設定寫成json格式,方便管理

json example:

{
"total": 5000,
"concurrency": 50,
"proto": "./hello.proto",
"call": "pb.HelloService.SayHello",
"data": {
"greeting": "hello world"
},
"host": "127.0.0.1:9999",
"insecure": true
}

command :

ghz.exe --config .\config.json
  • 支援多種輸出格式,如csv, json, html, InfluxDB

export html report command :

ghz.exe --insecure \
--proto ./hello.proto \
--call pb.HelloService.SayHello \
-D task_data.json \
-c 1000 -n 10000 -O html \
127.0.0.1:9998 >> report.html

Experiment

python gRPC server:127.0.0.1:9999

from concurrent import futures
import time

import grpc
import hello_pb2
import hello_pb2_grpc


# 創建一個 HelloServiceServicer,要繼承自 hello_pb2_grpc.HelloServiceServicer
class HelloServiceServicer(hello_pb2_grpc.HelloServiceServicer):

# 由於我們 service 定義了 SayHello 這個 rpc,所以要實作 SayHello 這個 method
def SayHello(self, request, context):
# response 是個 HelloResponse 形態的 message
response = hello_pb2.HelloResponse()
response.reply = f'Hello, {request.greeting}'
return response


def serve():
# 創建一個 gRPC server
server = grpc.server(futures.ThreadPoolExecutor())

# 利用 add_HelloServiceServicer_to_server 這個 method 把上面定義的 HelloServiceServicer 加到 server 當中
hello_pb2_grpc.add_HelloServiceServicer_to_server(HelloServiceServicer(), server)

# 讓 server 跑在 port 9999 中
server.add_insecure_port('[::]:9999')
server.start()
try:
while True:
time.sleep(86400)
except KeyboardInterrupt:
server.stop(0)


if __name__ == '__main__':
serve()

go gRPC server:127.0.0.1:9998

package main

import (
"context"
"log"
"net"

pb "../pb"
"google.golang.org/grpc"
)

type service struct {
pb.UnimplementedHelloServiceServer
}

func (s *service) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
// log.Printf("Received: %v", in.GetGreeting())
return &pb.HelloResponse{Reply: "Hello, " + in.GetGreeting()}, nil
}

func main() {
addr := "127.0.0.1:9998"
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

// log.Println("Server listening on", addr)
gRPCServer := grpc.NewServer()
pb.RegisterHelloServiceServer(gRPCServer, &service{})
if err := gRPCServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

上面的code都放在這邊

透過ghz tool 對python server進行1000000個request,同時1000個連線,且每次都傳1kB的資料

ghz.exe --insecure \
--import-paths ./ \
--proto ./hello.proto \
--call pb.HelloService.SayHello \
--keepalive 3s \
-D task_data.json \
-c 1000 -n 1000000 \
127.0.0.1:9999

透過ghz tool 對go server進行1000000個request,同時1000個連線,且每次都傳1kB的資料

ghz.exe --insecure \
--import-paths ./ \
--proto ./hello.proto \
--call pb.HelloService.SayHello \
--keepalive 3s \
-D task_data.json \
-c 1000 -n 1000000 \
127.0.0.1:9998

可以發現 Reqursts/sec go為63200.71 而python 則比較少為5963.62,所以go的Requrests/sec大約為python的10倍

Author: Alen Chen
Link: https://alenweiru.gitlab.io/posts/55ea54a0/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.