Go:基于Go实现一个压测工具
文章目录
- 写在前面
- 整体架构
- 通用数据处理模块
- Http请求响应数据处理
- Curl参数解析处理
- 客户端模块
- Http客户端处理
- Grpc客户端处理
- Websocket客户端处理
- 连接处理模块
- Grpc
- Http
- 统计数据模块
- 统计原理
- 实现过程
写在前面
本篇主要是基于Go来实现一个压测的工具,关于压测的内容可以参考其他的文章,这里默认了解压测的基本概念
基于Golang实现的压测工具
整体架构
整体系统架构比较简单
通用数据处理模块
Http请求响应数据处理
本项目支持http协议、websocket协议、grpc协议、Remote Authentication Dial-In User Service协议,因此需要构造出一个通用的http请求和响应的结构体,进行一个通用的封装:
// Request 请求数据
type Request struct {
URL string // URL
Form string // http/webSocket/tcp
Method string // 方法 GET/POST/PUT
Headers map[string]string // Headers
Body string // body
Verify string // 验证的方法
Timeout time.Duration // 请求超时时间
Debug bool // 是否开启Debug模式
MaxCon int // 每个连接的请求数
HTTP2 bool // 是否使用http2.0
Keepalive bool // 是否开启长连接
Code int // 验证的状态码
Redirect bool // 是否重定向
}
这当中值得注意的是验证的方法,这里是因为在进行压测中,要判断返回的响应是否是正确的响应,因此要进行判断响应是否正确,所以要进行相应的函数的注册,因此对于一个请求,是有必要找到一个对应的请求方法来判断这个请求正确,之后进行记录
这个model的核心功能,就是生成一个http请求的结构体,来帮助进行存储
// NewRequest 生成请求结构体
// url 压测的url
// verify 验证方法 在server/verify中 http 支持:statusCode、json webSocket支持:json
// timeout 请求超时时间
// debug 是否开启debug
// path curl文件路径 http接口压测,自定义参数设置
func NewRequest(url string, verify string, code int, timeout time.Duration, debug bool, path string,
reqHeaders []string, reqBody string, maxCon int, http2, keepalive, redirect bool) (request *Request, err error) {
var (
method = "GET"
headers = make(map[string]string)
body string
)
if path != "" {
var curl *CURL
curl, err = ParseTheFile(path)
if err != nil {
return nil, err
}
if url == "" {
url = curl.GetURL()
}
method = curl.GetMethod()
headers = curl.GetHeaders()
body = curl.GetBody()
} else {
if reqBody != "" {
method = "POST"
body = reqBody
}
for _, v := range reqHeaders {
getHeaderValue(v, headers)
}
if _, ok := headers["Content-Type"]; !ok {
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
}
}
var form string
form, url = getForm(url)
if form == "" {
err = fmt.Errorf("url:%s 不合法,必须是完整http、webSocket连接", url)
return
}
var ok bool
switch form {
case FormTypeHTTP:
// verify
if verify == "" {
verify = "statusCode"
}
key := fmt.Sprintf("%s.%s", form, verify)
_, ok = verifyMapHTTP[key]
if !ok {
err = errors.New("验证器不存在:" + key)
return
}
case FormTypeWebSocket:
// verify
if verify == "" {
verify = "json"
}
key := fmt.Sprintf("%s.%s", form, verify)
_, ok = verifyMapWebSocket[key]
if !ok {
err = errors.New("验证器不存在:" + key)
return
}
}
if timeout == 0 {
timeout = 30 * time.Second
}
request = &Request{
URL: url,
Form: form,
Method: strings.ToUpper(method),
Headers: headers,
Body: body,
Verify: verify,
Timeout: timeout,
Debug: debug,
MaxCon: maxCon,
HTTP2: http2,
Keepalive: keepalive,
Code: code,
Redirect: redirect,
}
return
}
之后是对于对应的响应的封装,结构体定义为:
// RequestResults 请求结果
type RequestResults struct {
ID string // 消息ID
ChanID uint64 // 消息ID
Time uint64 // 请求时间 纳秒
IsSucceed bool // 是否请求成功
ErrCode int // 错误码
ReceivedBytes int64
}
Curl参数解析处理
对于这个模块,本项目中实现的逻辑是根据一个指定的Curl的文件,对于文件中的Curl进行解析,即可解析出对应的Http请求的参数,具体代码链接如下
https://gitee.com/zhaobohan/stress-testing/blob/master/model/curl_model.go
客户端模块
Http客户端处理
在该模块中主要是对于Http客户端进行处理,对于普通请求和Http2.0请求进行了特化处理,支持根据客户端ID来获取到指定的客户端,建立映射关系
具体的核心成员为:
var (
mutex sync.RWMutex
// clients 客户端
// key 客户端id - value 客户端
clients = make(map[uint64]*http.Client)
)
再具体的,对于客户端的封装,主要操作是,对于Client的构造
// createLangHTTPClient 初始化长连接客户端参数
// 创建了一个配置了长连接的 HTTP 客户端传输对象
func createLangHTTPClient(request *model.Request) *http.Client {
tr := &http.Transport{
// 使用 net.Dialer 来建立 TCP 连接
// Timeout 设置为 30 秒,表示如果连接在 30 秒内没有建立成功,则超时
// KeepAlive 设置为 30 秒,表示连接建立后,如果 30 秒内没有数据传输,则发送一个 keep-alive 探测包以保持连接
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 0, // 最大连接数,默认0无穷大
MaxIdleConnsPerHost: request.MaxCon, // 对每个host的最大连接数量(MaxIdleConnsPerHost<=MaxIdleConns)
IdleConnTimeout: 90 * time.Second, // 多长时间未使用自动关闭连接
// InsecureSkipVerify 设置为 true,表示不验证服务器的 SSL 证书
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
if request.HTTP2 {
// 使用真实证书 验证证书 模拟真实请求
tr = &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 0, // 最大连接数,默认0无穷大
MaxIdleConnsPerHost: request.MaxCon, // 对每个host的最大连接数量(MaxIdleConnsPerHost<=MaxIdleConns)
IdleConnTimeout: 90 * time.Second, // 多长时间未使用自动关闭连接
// 配置 TLS 客户端设置,InsecureSkipVerify 设置为 false,表示验证服务器的 SSL 证书
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
}
// 将 tr 配置为支持 HTTP/2 协议
_ = http2.ConfigureTransport(tr)
}
client := &http.Client{
Transport: tr,
}
// 禁止 HTTP 客户端自动重定向,而是让客户端在遇到重定向时停止并返回最后一个响应
if !request.Redirect {
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
}
return client
}
https://gitee.com/zhaobohan/stress-testing/blob/master/server/client/http_client.go
Grpc客户端处理
对于Grpc的构造来说,主要实现的功能是建立连接等,这些操作是较为简单的操作,因此这里不具体讲述
// GrpcSocket grpc
type GrpcSocket struct {
conn *grpc.ClientConn
address string
}
conn和Address主要都是借助于两个类的成员函数来完成,解析地址和建立连接
其余模块可在代码中查看,这里不进行过多讲述
https://gitee.com/zhaobohan/stress-testing/blob/master/server/client/grpc_client.go
Websocket客户端处理
// WebSocket webSocket
type WebSocket struct {
conn *websocket.Conn
URLLink string
URL *url.URL
IsSsl bool
HTTPHeader map[string]string
}
其余模块可在代码中查看,这里不进行过多讲述
https://gitee.com/zhaobohan/stress-testing/blob/master/server/client/websocket_client.go
连接处理模块
Grpc
对于Grpc的测试,这里模拟了一个rpc调用,执行了一个Hello World的函数,之后填充相应的数据作为请求的响应,最后将结果返回
// grpcRequest 请求
func grpcRequest(chanID uint64, ch chan<- *model.RequestResults, i uint64, request *model.Request,
ws *client.GrpcSocket) {
var (
startTime = time.Now()
isSucceed = false
errCode = model.HTTPOk
)
// 获取连接
conn := ws.GetConn()
if conn == nil {
errCode = model.RequestErr
} else {
c := pb.NewApiServerClient(conn)
var (
ctx = context.Background()
req = &pb.Request{
UserName: request.Body,
}
)
// 发送请求,获得响应
rsp, err := c.HelloWorld(ctx, req)
if err != nil {
errCode = model.RequestErr
} else {
// 200 为成功
if rsp.Code != 200 {
errCode = model.RequestErr
} else {
isSucceed = true
}
}
}
requestTime := uint64(helper.DiffNano(startTime))
requestResults := &model.RequestResults{
Time: requestTime,
IsSucceed: isSucceed,
ErrCode: errCode,
}
requestResults.SetID(chanID, i)
ch <- requestResults
}
Http
对于Http的测试,效果也基本类似,原理也基本相同
// HTTP 请求
func HTTP(ctx context.Context, chanID uint64, ch chan<- *model.RequestResults, totalNumber uint64, wg *sync.WaitGroup,
request *model.Request) {
defer func() {
wg.Done()
}()
for i := uint64(0); i < totalNumber; i++ {
if ctx.Err() != nil {
break
}
list := getRequestList(request)
isSucceed, errCode, requestTime, contentLength := sendList(chanID, list)
requestResults := &model.RequestResults{
Time: requestTime,
IsSucceed: isSucceed,
ErrCode: errCode,
ReceivedBytes: contentLength,
}
requestResults.SetID(chanID, i)
ch <- requestResults
}
return
}
统计数据模块
下面来看计算统计数据模块
统计原理
这里需要统计的数据有以下:
耗时、并发数、成功数、失败数、qps、最长耗时、最短耗时、平均耗时、下载字节、字节每秒、状态码
其中这里需要注意的,计算的数据有QPS,其他基本都可以经过简单的计算得出
那QPS该如何进行计算呢?这里来这样进行计算:
QPS = 服务器每秒钟处理请求数量 (req/sec 请求数/秒)
定义:单个协程耗时T, 所有协程压测总时间 sumT,协程数 n
如果:只有一个协程,假设接口耗时为 2毫秒,每个协程请求了10次接口,每个协程耗总耗时210=20毫秒,sumT=20
QPS = 10/201000=500
如果:只有十个协程,假设接口耗时为 2毫秒,每个协程请求了10次接口,每个协程耗总耗时210=20毫秒,sumT=2010=200
QPS = 100/(200/10)*1000=5000
上诉两个示例现实中总耗时都是20毫秒,示例二 请求了100次接口,QPS应该为 示例一 的10倍,所以示例二的实际总QPS为5000
除以协程数的意义是,sumT是所有协程耗时总和
实现过程
这个模块主要是定时进行一个统计压测的结论并进行打印的工作,依赖的函数是
// calculateData 计算数据
func calculateData(concurrent, processingTime, requestTime, maxTime, minTime, successNum, failureNum uint64,
chanIDLen int, errCode *sync.Map, receivedBytes int64) {
if processingTime == 0 {
processingTime = 1
}
var (
qps float64
averageTime float64
maxTimeFloat float64
minTimeFloat float64
requestTimeFloat float64
)
// 平均 QPS 成功数*总协程数/总耗时 (每秒)
if processingTime != 0 {
qps = float64(successNum*concurrent) * (1e9 / float64(processingTime))
}
// 平均时长 总耗时/总请求数/并发数 纳秒=>毫秒
if successNum != 0 && concurrent != 0 {
averageTime = float64(processingTime) / float64(successNum*1e6)
}
// 纳秒=>毫秒
maxTimeFloat = float64(maxTime) / 1e6
minTimeFloat = float64(minTime) / 1e6
requestTimeFloat = float64(requestTime) / 1e9
// 打印的时长都为毫秒
table(successNum, failureNum, errCode, qps, averageTime, maxTimeFloat, minTimeFloat, requestTimeFloat, chanIDLen,
receivedBytes)
}
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/522987.html 如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!