前言
- 前一篇文已经简单讲解怎么通过goroutines的能力编写并发http压测脚本,但前文有提到过,主线程为了等待goroutine都运行完毕,不得不在程序的末尾使用time.Sleep() 来睡眠一段时间,等待其他线程充分运行。对于简单的代码,100个for循环可以在1秒之内运行完毕,time.Sleep() 也可以达到想要的效果,但是对于大多数真实业务和工作场景来说,1秒肯定会不够的,并且大部分时候我们都无法预知for循环内代码运行时间的长短。这时候就不能使用time.Sleep() 来完成等待操作了,这里我们就可以使用golang的一个类似于计数器的组件sync.WaitGroup
sync.WaitGroup模块
- WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为n ,Done() 每次把计数器-1 ,wait() 会阻塞代码的运行,直到计数器地值减为0,结合goroutines使用
import (
"fmt"
"sync"
)
func main() {
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 20; i++ {
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
可以执行查看一些输出
go run synctest.go
1
5
2
3
4
7
6
19
15
0
10
14
18
16
12
8
11
13
17
9
- 这里首先把wg 计数设置为20, 每个for循环运行完毕都把计数器减一,主函数中使用Wait() 一直阻塞,直到wg为0,也就是所有的20个for循环都运行完毕,WaitGroup 通过计数器的方式很好地控制了gorountines和主线程的执行关系
sync.WaitGroup模块编写场景化的压测脚本
- 有了sync.WaitGroup提供的能力,我们可以更好的基于一些场景去编写压测脚本
- 有这样子的一个服务器
#! /usr/bin/env
#coding=utf-8
import socket
import json
import requests
from flask import Flask, request,jsonify,g
app = Flask(__name__)
filename='demo.txt'
@app.route('/apisetdata', methods=['POST'])
def setdata():
msg=request.json["msg"]
print(msg)
with open(filename, 'w') as f:
f.write(str(msg))
f.close()
resp=jsonify({"code":200,"state":"set msg ok","msg":msg})
resp.status_code=200
return resp
@app.route('/apigetdata', methods=['GET'])
def getdata():
f = open(filename, 'r')
msg=f.read()
resp=jsonify({"code":200,"state":"get msg ok","msg":msg})
resp.status_code=200
return resp
if __name__ == "__main__":
app.run(debug=True)
- msg保存着demo.txt的内容,通过apigetdata接口可以获取内容,通过apisetdata接口可以修改demo.txt的内容,我们现在需要对修改demo.txt的内容后查询这个场景进行压测,这样我们就可以获得这样的压测脚本
package main
import (
"fmt"
"net/http"
"sync"
"time"
"bytes"
"encoding/json"
"io/ioutil"
simplejson "github.com/bitly/go-simplejson"
)
var (
success = 0
failure = 0
useTime = 0.0 //记录请求成功失败数和使用时间
)
var (
num =100 //要发送的请求数
con =100 //并发数
)
type HttpData struct {
Msg string `json:"msg"`
}
var wg sync.WaitGroup //创建一个计数器
func dotest(num int) { //场景化请求方法,在里面可以自定义需要编辑的内容
defer wg.Done() //进入到方法后计算器-1,标记创建了一个goroutine
no := 0
ok := 0
url := "http://127.0.0.1:5000/apisetdata"
url2:= "http://127.0.0.1:5000/apigetdata"
contentType := "application/json;charset=utf-8"
var httpdata HttpData
httpdata.Msg = "terrychow"
b ,err := json.Marshal(httpdata)
if err != nil {
fmt.Println("json format error:", err)
return
}
body := bytes.NewBuffer(b)
for i := 0; i < num; i++ {
//修改内容部分
resp, err := http.Post(url, contentType, body) //通过apisetdata接口修改内容
if err != nil {
no += 1
fmt.Println("error failed:", err)
continue
}
defer resp.Body.Close()
//读取内容部分
resp2, err := http.Get(url2) //通过apigetdata接口获取内容
if err != nil {
no += 1
fmt.Println("error failed:", err)
continue
}
defer resp2.Body.Close()
body, err := ioutil.ReadAll(resp2.Body)
res, err := simplejson.NewJson([]byte(string(body)))
getmsg, err := res.Get("msg").String()
if resp.StatusCode != 200 {
no += 1
continue
}
if getmsg!=httpdata.Msg{ //断言修改的内容
no += 1
continue
}
ok += 1
continue
}
success += ok
failure += no
}
// 主函数
func main() {
startTime := time.Now().UnixNano()
// 并发开始
for i := 0; i < con; i++ {
wg.Add(1)
go dotest(num/con)
}
// fmt.Println("主程序开始wait")
wg.Wait()
endTime := time.Now().UnixNano()
useTime = float64(endTime-startTime) / 1e9
// 输出结果
fmt.Println()
fmt.Println("Complete requests:", success)
fmt.Println("Failed requests:", failure)
// fmt.Println("SuccessRate:", fmt.Sprintf("%.2f", ((success/total)*100.0)), "%")
fmt.Println("UseTime:", fmt.Sprintf("%.4f", useTime), "s")
fmt.Println("场景每秒处理数 sps:", fmt.Sprintf("%.4f", float64(num)/useTime))
fmt.Println("请求每秒处理数 qps:", fmt.Sprintf("%.4f", float64(num*2)/useTime))
}
- 执行一下查看效果
go run pertestgo.go
Complete requests: 100
Failed requests: 0
UseTime: 0.1738 s
场景每秒处理数 sps: 575.4601
请求每秒处理数 qps: 1150.9202
- 这里的压测脚本主要强调场景,像ab等压测工具,很多时候都是只有单接口的并发,对于复杂接口和做一些自定义断言的时候,就相对比较复杂,类似python的locust就是按照场景化写压测脚本的方式实现,这里也是运用了同样的思想
小结
- 希望本文对于同学们在使用golang进行压测的时候能够起到作用,接下来的文章还会讲述几个golang的压测工具和其他测试工具等,敬请期待
有疑问加站长微信联系(非本文作者)