原创 使用boomer压测grpc协议

2020/04/23 性能测试

由于工作原因,需要对grpc协议进行压测,在网上找了ghz,但是它只是一个命令行工具并且不支持分布式,于是结合Locust+boomer实现了grpc协议的压测。

grpc服务

gRPC是一个高性能、通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。

gRPC具有以下重要特征:

  • 强大的IDL特性 RPC使用ProtoBuf来定义服务,ProtoBuf是由Google开发的一种数据序列化协议,性能出众,得到了广泛的应用。
  • 支持多种语言 支持C++、Java、Go、Python、Ruby、C#、Node.js、Android Java、Objective-C、PHP等编程语言。
  • 基于HTTP/2标准设计

官网上有非常多语言的快速入门,为了演示跨语言调用,且boomer是基于go语言的,所以我演示的案例是go->python。首先根据官方文档的指引,起一个helloworld的grpc服务。

根据python 的快速入门,起一个基于Python的grpc服务。

-> % python greeter_server.py                                                                                                                                          

为了验证服务是否正常启动了,先直接使用greeter_client.py验证一下:

-> % python greeter_client.py
Greeter client received: Hello, you!

序列化和反序列化

为了从boomer侧发起请求,首先需要对请求和响应做序列化与反序列化,在阅读grpc的源码后,定义一个结构体ProtoCodec:

// ProtoCodec ...
type ProtoCodec struct{}

// Marshal ...
func (s *ProtoCodec) Marshal(v interface{}) ([]byte, error) {
	return proto.Marshal(v.(proto.Message))
}

// Unmarshal ...
func (s *ProtoCodec) Unmarshal(data []byte, v interface{}) error {
	return proto.Unmarshal(data, v.(proto.Message))
}

// Name ...
func (s *ProtoCodec) Name() string {
	return "ProtoCodec"
}

服务调用

我的想法是提供一套调用grpc服务的通用client,所以调用服务+方法时需要是动态的,正好grpc提供了Invoke方法可以满足这一点,接下来定义一个Requester结构体。

// Requester ...
type Requester struct {
	addr      string
	service   string
	method    string
	timeoutMs uint
	pool      pool.Pool
}

Requester中定义两个方法,一个是获取真实的调用方法getRealMethodName,一个是发起请求的方法Call,其中Call是暴露给外层调用的。

// getRealMethodName
func (r *Requester) getRealMethodName() string {
	return fmt.Sprintf("/%s/%s", r.service, r.method)
}

Call方法核心代码

if err = cc.(*grpc.ClientConn).Invoke(ctx, r.getRealMethodName(), req, resp, grpc.ForceCodec(&ProtoCodec{})); err != nil {
		fmt.Fprintf(os.Stderr, err.Error())
		return err
	}

连接池

如http1.1的Keep-Alive,在高并发下需要保持grpc连接以提高性能,所以需要实现一个grpc的连接池管理,这也是Requester结构体中pool的职责。

Requester实例化时初始化连接池:

// NewRequester ...
func NewRequester(addr string, service string, method string, timeoutMs uint, poolsize int) *Requester {
	//factory 创建连接的方法
	factory := func() (interface{}, error) { return grpc.Dial(addr, grpc.WithInsecure()) }

	//close 关闭连接的方法
	closef := func(v interface{}) error { return v.(*grpc.ClientConn).Close() }

	//创建一个连接池: 初始化5,最大连接200,最大空闲10
	poolConfig := &pool.Config{
		InitialCap: 5,
		MaxCap:     poolsize,
		MaxIdle:    10,
		Factory:    factory,
		Close:      closef,
		//连接最大空闲时间,超过该时间的连接 将会关闭,可避免空闲时连接EOF,自动失效的问题
		IdleTimeout: 15 * time.Second,
	}
	apool, _ := pool.NewChannelPool(poolConfig)
	return &Requester{addr: addr, service: service, method: method, timeoutMs: timeoutMs, pool: apool}
}

这里使用了开源库 pool 来做grpc的连接管理。在Call方法中每次发起请求前在连接池中获取一个连接,调用完成后放回连接池中。

脚本编写

接下来就是编写boomer脚本了,我们需要两个文件,一个定义pb结构的请求和响应,一个是执行逻辑main.go

a、基于.proto生成供go使用的pb.go文件

grpc使用PB结构传输消息,.proto文件定义了PB数据,使用protoc工具可以生成直接给不同语言使用的数据结构和接口定义文件,如下

-> % protoc helloworld.proto --go_out=./

执行成功后生成helloworld.pb.go文件,供main.go引用。

b、编写压测脚本main.go

在helloworld例子中存在两个PB对象,分别是HelloRequest、HelloReply,python暴露的rpc服务和接口分别为helloworld.Greeter和SayHello,所以调用方式如下:


// 修改为要压测的服务接口
var service = "helloworld.Greeter"
var method = "SayHello"
...

client = grequester.NewRequester(addr, service, method, timeout, poolsize)
...

startTime := time.Now()

// 构建请求对象
request := &HelloRequest{}
request.Name = req.Name

// 初始化响应对象
resp := new(HelloReply)
err := client.Call(request, resp)

elapsed := time.Since(startTime)

完整的文件地址请看 main.go

c、调试脚本

使用boomer的 –run-tasks 调试脚本

-> % cd examples/rpc
-> % go run *.go -a localhost:50051 -r '{"name":"bugVanisher"}' --run-tasks rpcReq
2020/04/21 21:31:11 {"name":"bugVanisher"}
2020/04/21 21:31:11 Running rpcReq
2020/04/21 21:31:11 Resp Length: 29
2020/04/21 21:31:11 message:"Hello, bugVanisher!"

至此,基于boomer的grpc压测脚本已经完成了,剩下的就是结合Locust对被测系统进行压测了,我这里就不赘述了。

总结

本文展示了如何压测一个官方Demo的grpc服务接口,这仅是一个演示,真实的业务一般会针对grpc框架做封装,也许不同的语言有各自完整的一套开源框架了。需要注意的地方是,不同的框架下,我们Invoke时,真实的method可能有所不同,要根据实际情况做修改。

结合连接池和boomer,压测脚本能提供非常好的施压能力,这也体现了go语言的强大。如果想实现非http协议的压测同时又想拥有不错的并发施压能力并支持分布式,boomer是一个不错的选择。

Search

    Table of Contents