go实现的简单压测工具

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修改对应值即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值