本文主要是介绍基于Go语言实现一个压测工具,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《基于Go语言实现一个压测工具》这篇文章主要为大家详细介绍了基于Go语言实现一个简单的压测工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下...
本篇主要是基于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 //China编程 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.www.chinasem.cnRequest) 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 := sendLiphpst(chanID, list) requestResults := &model.RequestResults{ Time: requestTime, IsSucceed: isSucceed, ErrCode: errCode, ReceivedBytes: contentLength, } requestRejavascriptsults.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 mandroidinTimeFloat = float64(minTime) / 1e6 requestTimeFloat = float64(requestTime) / 1e9 // 打印的时长都为毫秒 table(successNum, failureNum, errCode, qps, averageTime, maxTimeFloat, minTimeFloat, requestTimeFloat, chanIDLen, receivedBytes) }
以上就是基于Go语言实现一个压测工具的详细内容,更多关于Go压测工具的资料请关注编程China编程(www.chinasem.cn)其它相关文章!
这篇关于基于Go语言实现一个压测工具的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!