go实现的简单压测工具

news2024/11/17 19:48:08

1、参数概览

依赖github.com/ddliu/go-httpclient进行http接口请求
依赖github.com/antlabs/pcurl解析curl

输入参数:

  • -c,concurrency,并发数,启动协程数
  • -n, totalNumber,单个协程发送的请求个数
  • -u,curl字符串
  • -p,如果不使用-u,可以将curl地址放在文件中,使用-p传入curl文件地址
  • -e,expectCode,期望response返回的状态码

2、核心代码

1、网络请求server/dispose.go

func init() {
	// 注册verify校验器
	verify.RegisterVerifyHttp(verify.GetVerifyKey("statusCode"), verify.VerifyHttpByStatusCode)
}

func Dispose(ctx context.Context, req *request.Request, concurrency, totalNumber uint64) {
	ch := make(chan *response.ResponseResult, 1000)
	wg := sync.WaitGroup{}
	wgReceiving := sync.WaitGroup{}

	wgReceiving.Add(1)

    // 统计数据详情
	go func() {
		defer wgReceiving.Done()
		statistics.HandleStatics(concurrency, ch)
	}()

	// 传递的-c参数,为每个协程创建-n次请求
	for i := uint64(0); i < concurrency; i++ {
		wg.Add(1)
		chanId := i

		go func() {
			defer wg.Done()
			serveHTTP(ctx, chanId, totalNumber, ch, req)
		}()
	}

	wg.Wait()
	time.Sleep(time.Millisecond)
	close(ch)
	wgReceiving.Wait()
}

// 真正发送请求的方法
// chanId 每个协程的身份Id
// ch 用于接受http接口响应数据
// req 根据curl解析出来的request结构体
func serveHTTP(ctx context.Context, chanId, totalNumber uint64, ch chan<- *response.ResponseResult, req *request.Request) {
	for i := uint64(0); i < totalNumber; i++ {
		if ctx.Err() != nil {
			fmt.Printf("ctx.Err err: %v \n", ctx.Err())
			break
		}
		header := make(map[string]string)
		for k := range req.Header {
			header[k] = req.Header.Get(k)
		}

		respStatusCode := constants.Success
		isSucceed := false

		start := time.Now()
		resp, err := httpclient.Do(req.Method, req.URL.String(), header, nil)
		cost := uint64(time.Since(start).Nanoseconds()) //统计耗时
		if err != nil || resp == nil {
			respStatusCode = constants.RequestFailed
		} else {
		    // 校验response code与-e是否相同
			respStatusCode, isSucceed = verify.GetVerify(verify.GetVerifyKey("statusCode"))(req, resp)
		}

		result := &response.ResponseResult{
			Id:         fmt.Sprintf("%d-%d", chanId, i),
			ChanId:     chanId,
			Cost:       cost,
			IsSucceed:  isSucceed,
			StatusCode: respStatusCode,
		}
		// 写数据
		ch <- result
	}
}

2、校验器verify/verify.go

type Verify func(*request.Request, *httpclient.Response) (constants.ErrCode, bool)

var (
	verifyMap = make(map[string]Verify)
	mutex     sync.RWMutex
)

func RegisterVerifyHttp(key string, verifyFunc Verify) {
	mutex.Lock()
	defer mutex.Unlock()
	verifyMap[key] = verifyFunc
}

// request 解析curl所得
// response http请求结果
func VerifyHttpByStatusCode(request *request.Request, response *httpclient.Response) (constants.ErrCode, bool) {
	responseCode := response.StatusCode
	if responseCode == request.ExpectedCode {
		return constants.ErrCode(responseCode), true
	}
	return constants.ErrCode(responseCode), false
}

func GetVerifyKey(t string) string {
	return fmt.Sprintf("http.%s", t)
}

func GetVerify(key string) Verify {
	verify, ok := verifyMap[key]
	if !ok {
		panic("verify方法不存在")
	}
	return verify
}

3、解析curl request/request.go

type Request struct {
	*http.Request
	ExpectedCode int //-e参数输入
}

func NewRequest(curl, path string, expectedCode int) (*Request, error) {
	// 优先使用文件解析curl
	if path != "" {
		file, err := os.Open(path)
		if err != nil {
			fmt.Printf("open curl file %s failed, err: %+v\n", path, err)
			return nil, err
		}
		defer file.Close()

		buf, err := io.ReadAll(file)
		if err != nil {
			fmt.Printf("read curl file %s failed, err: %+v\n", path, err)
			return nil, err
		}
		curl = string(buf)
	}

	req, err := pcurl.ParseAndRequest(curl)
	if err != nil {
		fmt.Printf("parse curl file %s failed, err: %+v\n", path, err)
		return nil, err
	}
	return &Request{
		Request:      req,
		ExpectedCode: expectedCode,
	}, nil
}

4、数据统计statstics/statistics.go

// HandleStatics 所有耗时变量均为纳秒
func HandleStatics(concurrency uint64, ch <-chan *response.ResponseResult) {
	var (
		requestCostTimeList []uint64     // 耗时数组
		processingTime      uint64   = 0 // processingTime 处理总耗时
		requestCostTime     uint64   = 0 // requestCostTime 请求总时间
		maxTime             uint64   = 0 // maxTime 至今为止单个请求最大耗时
		minTime             uint64   = 0 // minTime 至今为止单个请求最小耗时
		successNum          uint64   = 0
		failureNum          uint64   = 0
		chanIdLen           uint64   = 0 // chanIdLen 并发数
		stopChan                     = make(chan bool)
		mutex                        = sync.RWMutex{}
		chanIds                      = make(map[int]bool)
	)

	startTime := uint64(time.Now().UnixNano())
	respCodeMap := sync.Map{}
	ticker := time.NewTicker(time.Second)
	go func() {
		for {
			select {
			case <-ticker.C:
				endTime := uint64(time.Now().UnixNano())
				mutex.Lock()
				go calculateData(concurrency, processingTime, endTime-startTime, maxTime, minTime, successNum, failureNum, chanIdLen, &respCodeMap)
				mutex.Unlock()
			case <-stopChan:
				return
			}
		}
	}()
	printHeader()
	for respRes := range ch {
		mutex.Lock()
		processingTime = processingTime + respRes.Cost
		if maxTime <= respRes.Cost {
			maxTime = respRes.Cost
		}
		if minTime == 0 {
			minTime = respRes.Cost
		} else if minTime > respRes.Cost {
			minTime = respRes.Cost
		}
		if respRes.IsSucceed {
			successNum = successNum + 1
		} else {
			failureNum = failureNum + 1
		}

		// 统计response状态码
		if value, ok := respCodeMap.Load(respRes.StatusCode); ok {
			total, _ := value.(int)
			respCodeMap.Store(respRes.StatusCode, total+1)
		} else {
			respCodeMap.Store(respRes.StatusCode, 1)
		}

		// 统计并发数
		if _, ok := chanIds[int(respRes.ChanId)]; !ok {
			chanIds[int(respRes.ChanId)] = true
			chanIdLen = uint64(len(chanIds))
		}
		requestCostTimeList = append(requestCostTimeList, respRes.Cost)
		mutex.Unlock()
	}
	// 数据全部接受完成,停止定时输出统计数据
	stopChan <- true
	endTime := uint64(time.Now().UnixNano())
	requestCostTime = endTime - startTime
	calculateData(concurrency, processingTime, requestCostTime, maxTime, minTime, successNum, failureNum, chanIdLen, &respCodeMap)

	fmt.Printf("\n\n")
	fmt.Println("*************************  结果 stat  ****************************")
	fmt.Println("处理协程数量:", concurrency)
	fmt.Println("请求总数(并发数*请求数 -c * -n):", successNum+failureNum, "总请求时间:",
		fmt.Sprintf("%.3f", float64(requestCostTime)/1e9),
		"秒", "successNum:", successNum, "failureNum:", failureNum)
	printTop(requestCostTimeList)
	fmt.Println("*************************  结果 end   ****************************")
	fmt.Printf("\n\n")
}

func calculateData(concurrent, processingTime, costTime, maxTime, minTime, successNum, failureNum, chanIdLen uint64, respCodeMap *sync.Map) {
	if processingTime == 0 || chanIdLen == 0 {
		return
	}

	var qps, averageTime, maxTimeFloat, minTimeFloat, requestCostTimeFloat float64

	// 平均 QPS 成功数*总协程数/总耗时 (每秒)
	qps = float64(successNum*1e9*concurrent) / float64(processingTime)

	// 平均耗时 总耗时/总请求数/并发数 纳秒=>毫秒
	if successNum != 0 && concurrent != 0 {
		averageTime = float64(processingTime) / float64(successNum*1e6)
	}
	maxTimeFloat = float64(maxTime) / 1e6
	minTimeFloat = float64(minTime) / 1e6
	requestCostTimeFloat = float64(costTime) / 1e9

	result := fmt.Sprintf("%4.0fs│%7d│%7d│%7d│%8.2f│%11.2f│%11.2f│%11.2f│%v",
		requestCostTimeFloat, chanIdLen, successNum, failureNum, qps, maxTimeFloat, minTimeFloat, averageTime, printMap(respCodeMap))
	fmt.Println(result)
}

func printHeader() {
	fmt.Printf("\n\n")
	fmt.Println("─────┬───────┬───────┬───────┬────────┬───────────┬───────────┬───────────┬────────")
	fmt.Println(" 耗时│ 并发数│ 成功数│ 失败数│   qps  │最长耗时/ms│最短耗时/ms│平均耗时/ms│ 状态码")
	fmt.Println("─────┼───────┼───────┼───────┼────────┼───────────┼───────────┼───────────┼────────")
	return
}

// 打印响应状态码及数量, 如 200:5
func printMap(respCodeMap *sync.Map) (mapStr string) {
	var mapArr []string

	respCodeMap.Range(func(key, value interface{}) bool {
		mapArr = append(mapArr, fmt.Sprintf("%v:%v", key, value))
		return true
	})
	sort.Strings(mapArr)
	mapStr = strings.Join(mapArr, ";")
	return
}

// printTop 排序后计算 top 90 95 99
func printTop(requestCostTimeList []uint64) {
	if len(requestCostTimeList) == 0 {
		return
	}
	all := uint64Array{}
	all = requestCostTimeList
	sort.Sort(all)
	fmt.Println("tp90:", fmt.Sprintf("%.3fms", float64(all[int(float64(len(all))*0.90)]/1e6)))
	fmt.Println("tp95:", fmt.Sprintf("%.3fms", float64(all[int(float64(len(all))*0.95)]/1e6)))
	fmt.Println("tp99:", fmt.Sprintf("%.3fms", float64(all[int(float64(len(all))*0.99)]/1e6)))
}

type uint64Array []uint64

func (array uint64Array) Len() int           { return len(array) }
func (array uint64Array) Swap(i, j int)      { array[i], array[j] = array[j], array[i] }
func (array uint64Array) Less(i, j int) bool { return array[i] < array[j] }

5、main.go

var (
	concurrency  uint64 = 1 // 并发数
	totalNumber  uint64 = 1 // 请求个数
	curl                = ""
	curlPath            = "" // curl文件路径
	expectedCode        = 200
)

func init() {
	flag.Uint64Var(&concurrency, "c", concurrency, "并发数")
	flag.Uint64Var(&totalNumber, "n", totalNumber, "请求数(单个并发)")
	flag.StringVar(&curl, "u", curl, "压测地址")
	flag.StringVar(&curlPath, "p", curlPath, "curl文件地址")
	flag.IntVar(&expectedCode, "e", expectedCode, "期望请求结果的状态码")

	flag.Parse()
}

func main() {
	runtime.GOMAXPROCS(getCPUNum())

	if len(curl) == 0 && len(curlPath) == 0 {
		fmt.Printf("示例: go run main.go -c 1 -n 1 -u https://www.baidu.com/ \n")
		return
	}

	req, err := request.NewRequest(curl, curlPath, expectedCode)
	if err != nil {
		fmt.Println(err)
		return
	}
	ctx := context.Background()
	server.Dispose(ctx, req, concurrency, totalNumber)
}
func getCPUNum() int {
	if runtime.NumCPU()/4 < 1 {
		return 1
	}
	return runtime.NumCPU() / 4
}

3、验证猜想

  • 启动
go run main.go -c 1000 -n 5000 -p D:\go\go-demo\gostress\test-stress.curl 
  • qps、耗时等统计如下
    在这里插入图片描述
    在这里插入图片描述

为验证工具统计正确性,配置prometheus进行对照
在这里插入图片描述
可以看到prometheus在http server端统计到的数据qps、tp99、tp90、tp95基本上是符合的,由此验证工具正确性

4、工具http接口&监控

1、server端监控代码monitor/monitor.go

// 统计qps
var HttpRequestCount = prometheus.NewCounterVec(
	prometheus.CounterOpts{
		Name: "http_request_count",
		Help: "http request count",
	},
	[]string{"endpoint", "port"},
)

var Histogram = prometheus.NewHistogram(prometheus.HistogramOpts{
	Name:    "histogram_showcase_metric",
	Buckets: []float64{40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 280, 290, 300}, // 根据场景需求配置bucket的范围
})

2、main.go

func init() {
	prometheus.MustRegister(monitor.HttpRequestCount)
	prometheus.MustRegister(monitor.Histogram)
}

func main() {
	r := gin.Default()
	r.GET("/metrics", PromHandler(promhttp.Handler()))
	r.GET("/stress", func(c *gin.Context) {
		start := time.Now()
		c.JSON(http.StatusOK, "gin demo")
		monitor.HttpRequestCount.WithLabelValues(c.Request.URL.Path, "8888").Inc()

		n := rand.Intn(100)
		if n >= 95 {
			time.Sleep(100 * time.Millisecond)
		} else {
			time.Sleep(50 * time.Millisecond)
		}

		monitor.Histogram.Observe((float64)(time.Since(start) / time.Millisecond))
	})

	r.Run(":8888")
}

func PromHandler(handler http.Handler) gin.HandlerFunc {
	return func(c *gin.Context) {
		handler.ServeHTTP(c.Writer, c.Request)
	}
}

5、prometheus与grafana在windows安装步骤

  • prometheus安装
    直接google,改配置如下
  - job_name: "prometheus"

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
      - targets: ["localhost:9090"]
  # 此次测试监控
  - job_name: "go-stress"
    static_configs:
      - targets: ["localhost:8888"]
  - job_name: "nginx"
    static_configs:
      - targets: ["localhost:8889"]
  # 监控windows   
  - job_name: "windows"
    static_configs:
      - targets: ["localhost:9182"]

启动后打开localhost:9090查看

  • grafana安装
    官网下载zip安装包,解压启动即可

  • grafana面板配置
    qps

sum(rate(http_request_duration_count{}[1m])) by (endpoint)

TP90

histogram_quantile(0.90, rate(histogram_showcase_metric_bucket{instance="localhost:8888"}[1m]))

TP99、TP95修改对应值即可

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/687541.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Zookeeper源码解析(上)

序&#xff1a; Zookeeper最早起源于雅虎研究院的一个研究小组&#xff0c; 在当时&#xff0c; 研究人员发现&#xff0c;在雅虎内部有很大大型的系统都是需要一个依赖一个类似的系统来进行分布式协调&#xff0c;但是在系统往往都存在分布式单点问题&#xff0c;所以雅虎的开…

佩戴舒适度极好的蓝牙耳机推荐,五款佩戴舒适的蓝牙耳机推荐

​说到真无线蓝牙耳机&#xff0c;很多人会问音质表现好不好&#xff0c;佩戴上耳舒适性怎么样&#xff1f;等等问题。面对这些常会问的问题&#xff0c;我总结出来&#xff0c;也整理出来几款质量好、佩戴舒适的蓝牙耳机给大家&#xff0c;来看看有哪些。 一、南卡OE PRO开放…

安装SSL证书会拖慢网站访问速度吗?

&#x1f482; 个人网站:【海拥】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 目录 前言什么是SSL证书SSL证…

Python程序设计基础:程序流程控制(一)

文章目录 一、条件表达式1、关系运算符2、逻辑运算符3、条件表达式 二、选择结构1、单分支结构if语句2、双分支结构if-else语句3、多分支结构if-elif-else语句4、嵌套的if结构 一、条件表达式 程序流程的基本结构主要有三种&#xff0c;顺序结构、选择结构和循环结构&#xff…

5.3.3 绝对路径与相对路径

除了需要特别注意的FHS目录配置外&#xff0c;在文件名部分我们也要特别注意。因为根据文件名写法的不同&#xff0c;也可将所谓的路径&#xff08;path&#xff09;定义为绝对路径&#xff08;absolute&#xff09;与相对路径&#xff08;relative&#xff09;。 这两种文件名…

Java IO 学习总结(五)OutputStreamWriter

Java IO 学习总结&#xff08;一&#xff09;输入流/输出流 Java IO 学习总结&#xff08;二&#xff09;File 类 Java IO 学习总结&#xff08;三&#xff09;BufferedInputStream Java IO 学习总结&#xff08;四&#xff09;BufferedReader 缓冲字符流 Java IO 学习总结&…

JDK HTTPS 400错误 微软数据湖数据拉取(DataLake Landing Zone API)避坑指南

文章目录 坑1&#xff1a;微软Azure数据湖landing zone API不支持TLSv1.1协议注意JDK1.8高版本 坑2&#xff1a;拉取的文件内容开头带BOM 数据湖号称新一代数据仓库产品。数据被写进数据湖文件之后会被自动同步到landing zone&#xff0c;可以通过landing zone API读取文件内容…

一文梳理清楚 Python OpenCV 的知识体系

本篇文章为你详细罗列 Python OpenCV 的学习路线与重要知识点。核心分成 24 个小节点&#xff0c;全部掌握&#xff0c;OpenCV 入门阶段就顺利通过了。 1. OpenCV 初识与安装 本部分要了解 OpenCV &#xff08;Open Source Computer Vision Library&#xff09;的相关简介&…

常见的性能测试缺陷

目录 前言&#xff1a; 性能测试缺陷分类 一、硬件 二、网络 三、应用 四、配置 五、数据库 六、中间件 前言&#xff1a; 性能测试是测试系统在特定条件下的响应时间、并发用户数、吞吐量、内存使用率、CPU利用率、网络延迟等各项指标&#xff0c;以验证其性能是否符…

ActiveMQ消息队列的介绍以及部署

文章目录 1.ActiveMQ消息队列中间件1.1.什么是ActiveMQ1.2.ActiveMQ支持的消息传递类型 2.部署ActiveMQ消息队列2.1.安装JDK环境2.2.部署ActiveMQ消息队列2.3.启动ActiveMQ消息队列2.4.ActiveMQ的端口号2.5.使用ActiveMQ的后台管理系统 ActiveMQ官网&#xff1a;https://active…

官宣!硬核学科“集成电路与机器人应用开发”正式入驻新校区!

好消息&#xff01;好消息&#xff01; 集成电路与机器人应用开发学科 强势入驻黑马武汉校区 现在报名&#xff08;7月1日前&#xff09;首期班 限时优惠1000元 再送价值千元硬件装备1套&#xff01; 上周&#xff0c;播妞采访了几位2023届毕业生的就业现状&#xff08;点击…

11-12 - 信号发送与处理

---- 整理自狄泰软件唐佐林老师课程 查看所有文章链接&#xff1a;&#xff08;更新中&#xff09;Linux系统编程训练营 - 目录 文章目录 1. 信号的概念及分类1.1 问题1.2 什么是信号1.3 信号的分类1.3.1 硬件异常信号1.3.2 终端相关信号1.3.3 软件相关信号 1.4 内核与信号1.5 …

组合模式(Composite)

别名 对象树&#xff08;Object Tree&#xff09;。 定义 组合是一种结构型设计模式&#xff0c;你可以使用它将对象组合成树状结构&#xff0c;并且能像使用独立对象一样使用它们。 前言 1. 问题 如果应用的核心模型能用树状结构表示&#xff0c;在应用中使用组合模式才…

彭博:为完善Vision Pro体验,苹果扩招数千名新员工

彭博社记者Mark Gurman在最新一期Power On栏目中表示&#xff0c;苹果在WWDC 2023上公布Vision Pro头显&#xff0c;只是该公司进入XR市场的第一步&#xff0c;实际上该设备在明年才会推出完整版。而且据项目相关人士透露&#xff0c;Vision Pro的软件生态还需要很长时间发展。…

软件工程——第6章详细设计知识点整理

本专栏是博主个人笔记&#xff0c;主要目的是利用碎片化的时间来记忆软工知识点&#xff0c;特此声明&#xff01; 文章目录 1.详细设计阶段的根本目的是&#xff1f; 2.详细设计的任务&#xff1f; 3.详细设计的结果地位&#xff1f;如何衡量程序质量&#xff1f; 4.结构程…

在GitHub上爆火!跳槽必看《Java 面试突击核心讲》知识点笔记整理

不知道大家在面试中有没有这种感觉&#xff1a;面试官通常会在短短两小时内对面试者的知识结构进行全面了解&#xff0c;面试者在回答问题时如果拖泥带水且不能直击问题的本质&#xff0c;则很难充分表现自己&#xff0c;最终影响面试结果。 所以针对这种情况&#xff0c;这份…

从0到1精通自动化测试,pytest自动化测试框架,使用自定义标记mark(十一)

一、前言 pytest可以支持自定义标记&#xff0c;自定义标记可以把一个web项目划分多个模块&#xff0c;然后指定模块名称执行 app自动化的时候&#xff0c;如果想android和ios公用一套代码时&#xff0c;也可以使用标记功能&#xff0c;标明哪些是ios用例&#xff0c;哪些是a…

除静电设备给我们的生产带来怎样的便利

一般来说&#xff0c;我们需要根据具体的生产工艺和场景选择适当的静电设备&#xff0c;并按照厂商提供的操作规范正确使用&#xff0c;以确保除静电设备有效发挥作用。 1. 静电消除&#xff1a;静电设备可以帮助消除物体表面的静电电荷&#xff0c;防止静电积聚。静电积聚可能…

UML类图设计

1.普通类&#xff0c;抽象类&#xff0c;接口 普通类 抽象类 接口 1 关联关系 依赖关系 关联&#xff1a;对象之间的引用关系 依赖&#xff1a;耦合性最低&#xff0c;一些静态方法等 2 聚合关系 组合关系 聚合&#xff1a;整体与部分的关系&#xff0c;但是部分可以脱…

英特尔 oneAPI 2023 黑客松大赛:赛道二机器学习:预测淡水质量 实践分享

目录 一、问题描述二、解决方案1、方案简述2、数据分析预处理特征类型处理特征分布分析 3、特征构造4、特征选择过滤法重要性排序 5、模型训练 总结未来工作 一、问题描述 淡水是我们最重要和最稀缺的自然资源之一&#xff0c;仅占地球总水量的 3%。它几乎触及我们日常生活的方…