当前位置: 代码网 > it编程>前端脚本>Golang > 基于Go语言实现一个压测工具

基于Go语言实现一个压测工具

2025年02月13日 Golang 我要评论
本篇主要是基于go来实现一个压测的工具,关于压测的内容可以参考其他的文章,这里默认了解压测的基本概念基于golang实现的压测工具整体架构整体系统架构比较简单通用数据处理模块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)
}

以上就是基于go语言实现一个压测工具的详细内容,更多关于go压测工具的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com