gRPC测试经验分享
记录一次gRPC测试经验,离测试结束已有一段时间了,一直没空更新,接假期梳理一下,希望对大家有帮助。
1 gRPC选型背景及业务交互
1.1选gRPC技术主要背景有
- 技术的迭代,在一个系统新业务做为试点,积累更多经验,为后续内部服务接口从HTTP转gRPC切换提供基础;
- 为满足未来对性能有极高要求的场景;
- gRPC清晰的 API 规范以及对流的良好支持,再加上有Nginx的加持,对于内部服务使用,gRPC是不二选择;
- 适配多语言,减少第三方调用成本。
1.2 业务交互
内部系统业务交互示意图:

2 gRPC
2.1 gRPC是什么
借用gRPC首页的一句话来理解它是什么。
A high performance, open source universal RPC framework
如下图,gRPC与RPC一样,也是使用了定义服务的思想,通过制定参数和返回类型实现的一种远程过程调用方法。Client可以直接调用Server端的接口,就想调用本地的方法一样。在服务器端,服务器实现了这个接口并运行一个 gRPC 服务器来处理客户端调用。

2.2 proto文件
说到proto文件(*.proto),不得不提协议缓冲区,它是一种用于序列化结构化数据的机制。gRPC使用协议缓冲区创建proto文件,定义结构化数据,protobuf 的术语中,结构化数据被称为 Message。通俗点说相当于确定数据协议,数据结构中存在哪些数据,数据类型是怎么样。如下例子:
# 定义语法类型
syntax = "proto3";
# 声明包名
package 包名;
# 声明服务
service TestServer {
# 以下 分别是 服务端 推送流, 客户端 推送流 ,双向流。
rpc CardList (cardListReq) returns (cardListRsp) {}
}
# 流请求结构
message StreamReqData {
string data = 1;
}
# 流返回结构
message StreamResData {
string data = 1;
}
# 定义请求结构体
message cardListReq {
map<string, string> extra = 1;
string orderid = 2;
string gen_id = 3;
int64 curpage = 4;
int64 pagesize = 5;
}
# 定义返回结构体
message cardListRsp {
map<string, string> extra = 1;
int32 code = 2; # 返回码
string msg = 3; # 返回信息
map<string, string> codes = 4;
}`
那我们拿到这个文件怎么用呢?可以使用protoc编译器把proto文件,但要确保你编译的proto文件是最新的,具体命令如下:
xxxxxxxxxx python -m grpc_tools.protoc -I ./ --python_out={py文件输出存放dir} --grpc_python_out={grpc文件输出存放dir} ./test.proto
/*输出4个文件:
test_pb2_grpc.py
test_pb2.py
test_pb2_grpc.pyc
test_pb2.pyc*/
2.3 环境配置
- Nginx配置
HTTP的Nginx配置和gRPC有点区别,下面列出了在配置Nginx时需要注意的地方:
server {
listen 80 http2; # 因为gRPC是基于HTTP2,所以这里要使用HTTP2。目前Nginx不支持HTTH1和HTTP2共用一个端口,要检查配置的port是否已使用。
# grpc prrof test inprovement,default 1000 解决高并发下请求连接数限制问题,在文章后面会有介绍
keepalive_requests 1000;
server_name domain;
.
.
.
# 下面"A.B"就是当初proto文件中定义的包名及服务名
location ~ /(test.TestServer|test.GameProduct)/ {
.
.
.
# 这里需要配置成grcp_pass,在HTTP中proxy_pass http
grpc_pass grpc://test_channel;
}
2.4 客户端用例实现
得到编译出来的gRPC文件,我们需要在Client中导入,下面是一个简单例子:
import grpc
import test_pb2 as pb
import test_pb2_grpc as pb_grpc
def test_get_card_list():
"""分页获取卡密"""
server = grpc.insecure_channel('{domain/server ip}:{port}')
cardListstub = pb_grpc.TestServerStub(server)
try:
respCardList = cardListstub.CardList(pb.cardListReq(
orderid="orderid_87534042742",
curpage=0,
pagesize=10,
gen_id="gen_87534042742",
))
# 设置断言
if respCardList.code == 1000:
assert 'ok' in respCardList.msg
print respCardList
else:
assert respCardList.code == 1000
except grpc.RpcError as e:
print '--------------- 返回未知状态,请查看日志排查问题 --------------'
assert 'UNKNOWN' in e
# print e
except Exception as e:
print "get except: %s" % str(e)
对于我们测试同学来说,了解proto内容含义、原理及怎么使用,已经满足工作需要了。
3 ghz压测
3.1 环境搭建
在网上搜了一波,对于gRPC压测基本都是使用ghz工具,具体介绍就不展开了,请看ghz介绍。官网有安装方法和一些用法介绍,像ab一样,易上手。
3.2 压测脚本
下面截取gRPC其中一个接口的代码,写脚本的目的是通过传参的方式执行,在终端执行方便些。
#coding:utf-8
import subprocess
import json
import sys
import time
import uuid
class TESTAPI():
def __init__(self):
pass
# args一般为列表
def exeCmd(self,args):
sub=subprocess.Popen(args,shell=False,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
out,err=sub.communicate()
return out.decode("utf-8")
# 获取传入参数
def getParas(self):
concurrency = sys.argv[1]
total = sys.argv[2]
return concurrency,total
def cardList(self):
getParas = self.getParas()
reqData={"orderid":"orderid_87534042742","gen_id":"gen_87534042742"}
reqData=json.dumps(reqData)
# 拼接cmd 参数可自由组合 具体用法可参考官网使用说明 比较简单 一看就会
args=["./ghz","--insecure","--proto","./test.proto",
"--call","test.TestServer/CardList","-d",reqData,"-c",getParas[0],"-n",getParas[1],"{domain/server ip}:{port}"]
# 执行指令并输出结果
print(self.exeCmd(args))
if __name__ == '__main__':
runAPI = TESTAPI()
runAPI.cardList()
因为是单接口,HTTP和HTTPS请求就选了ab作为压测工具,考虑到和Jmeter是否会有误差,也顺便对别了一下这两个工具在单个接口情况下,压测结果无差异。ab压测脚本如下:
#!/bin/bash
# 可传多组
totalReqs=(10 20)
# 可传多组
concurrency=(3 5)
path="https://{domain}/server/test/list?orderid=orderid_20060069834&curpage=&pagesize=10&gen_id=gen_20060069834"
header="X-Server-Sign:61394770ee4afadfe7dfed29edd0ec466e3b3b5b857fc705dcd31baf518f2719"
startTest() {
if [ $3 -eq 0 ];then
# 借助 awk 分析
ab -n $1 -c $2 -H $header $path 2>&1 | awk 'BEGIN {printf "%-10s %-15s %-8s %-8s %-8s %-8s\n", "Requests", "Concurrency", "Min(ms)", "Max(ms)", "Mean(ms)","QPS(s)";printf "------------------------------------------------------ \n"} /Total:/ {printf "%-10s %-15s %-8s %-8s %-8s", "'$1'", "'$2'", $2, $6, $3}'
else
ab -n $1 -c $2 $path 2>&1 | awk '/Total:/ {printf "%-10s %-15s %-8s %-8s %-8s\n", "'$1'", "'$2'", $2, $6, $3}'
fi
}
index=0
for request in ${totalReqs[@]}
do
for con in ${concurrency[@]}
do
startTest $request $con $index
let 'index++'
done
done
------------------- 输出结果 -------------------------
fee@mgb-haproxy01-pressure:~/pressureTest/test$ sh cardList_ab.sh
Requests Concurrency Min(ms) Max(ms) Mean(ms)
------------------------------------------------------
10 3 7 10 8
10 5 9 11 10
20 3 6 9 8
20 5 7 14 9
3.3 执行压测及数据
python cardList.py 5 5 # 如果需要持续压,加个循环即可,重定向输出信息。
ghz压测数据默认输出如下:

ghz还提供了输出html文件的报告,需要加“-o”、“-O”,两个参数,大家可以尝试一下,看起来很直观。当然像我截图中存在异常请求,可以通过输出Debug日志,添加“-D”参数即可。html文件报告如下:

gRPC、HTTP以及HTTPS三种请求方式压测数据对比如下:

4 躺过的坑和解决方案
4.1 域名的使用
调用gRPC服务接口一定要加监听端口,使用{domain/ip}:{port}的形式,不需要加协议前缀和Path。是因为已经通过协议封装,协议里面有service和interface的信息,序列化了,不需要通过path来分发到具体的接口。
是通过在代码中带一个参数mehod,如method="/server/list",然后程序通过 if method==xxx判断走什么逻辑。具体的接口报错信息如下:
# 接口加了HTTP/HTTPS协议报错信息
rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing dial tcp: address http://127.0.0.1:11281: too many colons in address
# 没有加port报错信息
rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing dial tcp: address xxx.xxx.com: missing port in address"
4.2 压测时出现连接数受限
gRPC压测出现请求连接数到达一定数量之后会出现大量的连接失败的请求,导致大量TPC连接出现TIME_WAIT。
# 日志提示
"message":"Received RPC Stats","statsID":0,"code":"Unavailable","error":"rpc error: code = Unavailable desc = the connection is draining"
后来查资料得知是因为keepalive_requests设置比较小,高并发下超过此值后nginx会强制关闭和客户端保持的keepalive长连接,关闭连接后导致Nginx出现TIME_WAIT。
尝试解决方案:
1) 程序在AllySDK客户端,添加retry机制,运行服务前在设置环境变量:GRPC_GO_RETRY=on;
2)Nginx配置调整upstream配置,增大fail_timeout和keepalive;
3)Nginx 版本从 1.14.0 升级到 1.20.0;
4)在用ghz压测时加入参数: --keepalive=60s;
5)尝试server模块下设置这些参数:
large_client_header_buffers,不对
client_header_timeout,不对
keepalive_timeout,不对
keepalive_requests,bingo!!!,终于看到了胜利在招手。
需要注意的是,Nginx版本低于1.19.7,这个指令叫:http2_max_requests,默认值1000,Nginx官网文档有说明。你并发越大,需要设置的值就越大,但线上环境要把该情况同步给SA,评估这个值太大,是否会带来风险。

5 总结
本次gRPC的测试经历了一段时间,也是相对较新的技术,在前面测试过程中也记录了一些问题,感兴趣可以翻一下。基于在测试时碰到问题,爬网上也没有很全的分享文章,所以记录了下,也算是对本次测试的复盘。借助此次机会,掌握了gRPC、ghz原理和使用,不是很了解的同学可以通过超链传送学习。
如果大家对gRPC有更好的测试、压测方法,者对我本次经验总结有任何建议,欢迎随时骚扰!

1万+

被折叠的 条评论
为什么被折叠?



