当前位置: 首页 > news >正文

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            // URLForm      string            // http/webSocket/tcpMethod    string            // 方法 GET/POST/PUTHeaders   map[string]string // HeadersBody      string            // bodyVerify    string            // 验证的方法Timeout   time.Duration     // 请求超时时间Debug     bool              // 是否开启Debug模式MaxCon    int               // 每个连接的请求数HTTP2     bool              // 是否使用http2.0Keepalive 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 *CURLcurl, 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 stringform, url = getForm(url)if form == "" {err = fmt.Errorf("url:%s 不合法,必须是完整http、webSocket连接", url)return}var ok boolswitch form {case FormTypeHTTP:// verifyif verify == "" {verify = "statusCode"}key := fmt.Sprintf("%s.%s", form, verify)_, ok = verifyMapHTTP[key]if !ok {err = errors.New("验证器不存在:" + key)return}case FormTypeWebSocket:// verifyif 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 // 消息IDChanID        uint64 // 消息IDTime          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.ClientConnaddress string
}

conn和Address主要都是借助于两个类的成员函数来完成,解析地址和建立连接

其余模块可在代码中查看,这里不进行过多讲述

https://gitee.com/zhaobohan/stress-testing/blob/master/server/client/grpc_client.go

Websocket客户端处理

// WebSocket webSocket
type WebSocket struct {conn       *websocket.ConnURLLink    stringURL        *url.URLIsSsl      boolHTTPHeader 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 = falseerrCode   = 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/20
1000=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              float64averageTime      float64maxTimeFloat     float64minTimeFloat     float64requestTimeFloat 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) / 1e6minTimeFloat = float64(minTime) / 1e6requestTimeFloat = float64(requestTime) / 1e9// 打印的时长都为毫秒table(successNum, failureNum, errCode, qps, averageTime, maxTimeFloat, minTimeFloat, requestTimeFloat, chanIDLen,receivedBytes)
}
http://www.lryc.cn/news/527492.html

相关文章:

  • 算法-加油站问题
  • UART ,IIC 和SPI三种总线协议
  • Padas进行MongoDB数据库CRUD
  • 动手学图神经网络(6):利用图神经网络进行点云分类
  • C语言从入门到进阶
  • Python中容器类型的数据(下)
  • MySQL 用户相关的操作详解
  • 如何删除hugging face dowloaded的llm model?
  • Vue 封装http 请求
  • 恒源云云GPU服务器训练模型指南
  • Spring Boot应用中实现基于JWT的登录拦截器,以保证未登录用户无法访问指定的页面
  • MySQL 基础学习(1):数据类型与操作数据库和数据表
  • zyNo.19
  • Kafka生产者ACK参数与同步复制
  • IPhone14 Pro 设备详情
  • 【Linux】磁盘
  • Shell编程(for循环+并发问题+while循环+流程控制语句+函数传参+函数变量+函数返回值+反向破解MD5)
  • 强化学习数学原理(三)——值迭代
  • Day27-【13003】短文,什么是栈?栈为何用在递归调用中?顺序栈和链式栈是什么?
  • [JMCTF 2021]UploadHub
  • C++学习——认识和与C的区别
  • 为AI聊天工具添加一个知识系统 之63 详细设计 之4:AI操作系统 之2 智能合约
  • 基于SpringBoot的网上摄影工作室开发与实现 | 含论文、任务书、选题表
  • Flutter子页面向父组件传递数据方法
  • 回顾Maven
  • 除了layui.js还有什么比较好的纯JS组件WEB UI?在谷歌浏览上显示
  • 力扣111二叉树的最小深度(DFS)
  • c++学习第十三天
  • zookeeper-3.8.3-基于ACL的访问控制
  • Java定时任务实现方案(四)——Spring Task