什么是压测?
- 压测(Load Testing)是一种测试方法,它通过模拟实际用户在不同情景下的并发量、访问量或者负载量,来测试被测系统的性能、稳定性和可靠性。压测可以帮助我们了解系统的承载能力,找出系统的性能瓶颈和潜在问题,为系统优化提供数据支持。
- 压测的意义在于:
- 提升系统性能:通过压测可以找出系统的性能瓶颈,优化系统架构、代码和数据库等方面,提升系统的性能和响应速度。
- 保障系统稳定性:压测可以模拟高并发的请求,发现系统的容量极限和峰值,避免系统出现因压力过大导致的宕机、崩溃等问题。
- 确保系统可靠性:压测可以模拟多种情景下的用户行为,测试系统在不同负载下的性能表现,以保证系统的可靠性和稳定性,从而提升用户体验。
- 减少成本浪费:通过压测可以发现系统的问题,避免在生产环境中出现严重的问题,从而减少因故障造成的成本浪费。
- 总之,压测是一种非常重要的测试手段,可以帮助我们了解系统性能和稳定性,提升用户体验和满意度,降低故障率和成本。
需求
一个文件中有若干个接口,需要对这些接口进行压测。需要灵活指定并发数量,压测时间
场景模拟
- 假如我们指定并发数量为100,压测时间10分钟。
- 想象这样一个场景,100个人分别请求一个接口,在10分钟内不断发起请求。
- 实际情况下,有的人操作快,有的人操作慢。
- 所以实际上还需要一个参数:请求频率。假如请求频率是 1 秒 5 次,即 200 ms 1 次
- 那么对应实际场景就是,100个人不断请求,请求频率是 每秒钟请求 5 次
如何开发?
受开源仓库 go-stress-testing 代码启发。开发步骤如下
step 1 创建场景
- 首先需要创建这样一个场景。该场景中我们指定并发数量、压测时间、请求频率为三个参数
func NewSceneWithInterval(name string, duration int, interval int) *Scene {
return &Scene{
Name: name,
// 设置默认每秒钟执行一次
Frequency: time.Second,
// 设置测试场景执行 duration 分钟
Duration: time.Duration(duration * 60) * time.Second,
// 执行压测频率
Interval: time.Millisecond * interval
}
}
step 2 创建任务
- 根据并发数,来创建任务
- 这里假如待压测接口数量为
m
并发数为n
- 如果
n > m
那么重复利用一些接口来创建n
个任务
func NewTask(task *stress.Task, url string) {
*task = *stress.NewTask("test", func() *stress.TestResult {
var errors uint64
var success uint64
// 开始时间
start := time.Now()
resp, err := http.Get(url)
// 统计任务执行时间,并保存到测试结果中
elapsed := time.Since(start)
if err != nil {
// 发生错误,请求失败
errors = 1
success = 0
}
if resp.StatusCode == 200 {
// 请求成功
success = 1
errors = 0
} else {
// 请求失败
success = 0
errors = 1
}
// 统计任务执行时间,并保存到测试结果中
elapsed := time.Since(start)
result := &stress.TestResult{
Requests: 1,
Errors: errors,
Success: success,
Rps: 0,
Elapsed: elapsed,
}
task.Result = result
return result
})
}
- 任务创建完成之后加入到 step1 步骤中创建的场景中
step 3 运行场景
- 主要难点在于如何模拟真实场景压测
- 这里的功能实现利用了go语言并发模型,充分利用了 goroutine
func (s *Scene) RunWitTime() *TestResult {
// 并发数量-即任务的数量
concurrent := len(s.tasks)
// 用于等待所有的任务执行完成
wg := sync.WaitGroup{}
// 创建一个令牌桶,用于控制并发数
tokens := make(chan bool, concurrent)
for i := 0; i < concurrent; i++ {
tokens <- true
}
// 创建一个停止信号通道,用于控制场景测试的执行时长
stop := make(chan bool)
go func() {
time.Sleep(s.Duration)
close(stop)
}()
// 按照指定的频率执行请求
ticker := time.NewTicker(s.Interval)
// 统计请求次数、错误次数、成功次数、总耗时、成功请求耗时
loop:
for {
select {
case <-stop:
// 停止场景测试
ticker.Stop()
wg.Wait()
break loop
default:
// 遍历所有任务,获取令牌并执行请求
for _, task := range s.tasks {
select {
case <- tokens:
// 执行请求
wg.Add(1)
go func(task *Task) {
// 一个请求执行完毕,释放令牌
defer func() {
tokens <- true
wg.Done()
}()
// 执行任务逻辑 并处理具体逻辑
// xxx
}(task)
default:
// 令牌桶已满,等待重试机制
// 等待令牌桶中有可用令牌
retryTicker := time.NewTicker(time.Millisecond * 50)
defer retryTicker.Stop()
select {
case <-tokens:
retryTicker.Stop()
wg.Add(1)
go func(task *Task) {
defer func() {
tokens <- true
wg.Done()
}()
// 执行任务逻辑 并处理具体逻辑
// xxx
}(task)
case <-stop:
ticker.Stop()
wg.Wait()
break loop
case <-retryTicker.C:
// 随机休眠 几百毫秒
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
}
}
}
}
}
return 结果集
}
step 4 测试并处理压测数据
- 使用四个接口,并发数数10,压测1分钟,压测频率 500ms
- 测试结果如下
- 计算其它指标
- 处理转化成 json 串
{
"total":815,
"success":657,
"error":158,
"successRate":"80.61%",
"rps":11,
"avgRt":"532.00ms",
"minRt":"53.76ms",
"maxRt":"1033.55ms",
"p90Rt":"708.68ms",
"p95Rt":"727.30ms",
"p99Rt":"758.09ms",
"successTime":"349762.00ms",
"allTime":"366977.00ms"
}
- 逻辑代码
successRate := fmt.Sprintf("%.2f", float64(ret.Success) / float64(ret.Requests) * 100) + "%"
// 总的成功请求次数 / 总的压测时间
rps := uint64(math.Round(float64(ret.Success) / float64(runTime*60)))
// 平均
avgRt := fmt.Sprintf("%.2f", float64(ret.SuccessTime / (1000000 * ret.Success))) + "ms"
// 对成功响应时间进行排序
sort.Slice(ret.SuccessElapseds, func(i, j int) bool {
return ret.SuccessElapseds[i] < ret.SuccessElapseds[j]
})
minRt := fmt.Sprintf("%.2f", float64(ret.SuccessElapseds[0])/1000000) + "ms"
length := len(ret.SuccessElapseds)
maxRt := fmt.Sprintf("%.2f", float64(ret.SuccessElapseds[length - 1])/1000000) + "ms"
// 计算 90%Rt、95%Rt、99%Rt
TempP90 := float64(ret.SuccessElapseds[int(float64(length)*0.9)])/1000000
p90 := strconv.FormatFloat(TempP90, 'f', 2, 64) + "ms"
TempP95 := float64(ret.SuccessElapseds[int(float64(length)*0.95)])/1000000
p95 := strconv.FormatFloat(TempP95, 'f', 2, 64) + "ms"
TempP99 := float64(ret.SuccessElapseds[int(float64(length)*0.99)])/1000000
p99 := strconv.FormatFloat(TempP99, 'f', 2, 64) + "ms"
allElapsedTime := fmt.Sprintf("%.2f", float64(ret.ElapsedTime / 1000000)) + "ms"
allSuccessTime := fmt.Sprintf("%.2f", float64(ret.SuccessTime / 1000000)) + "ms"
// 打印每一个变量值
fmt.Printf("successRate: %.2f%%\n", successRate)
fmt.Printf("rps: %d\n", rps)
fmt.Printf("avgRt: %v\n", avgRt)
fmt.Printf("minRt: %v\n", minRt)
fmt.Printf("maxRt: %v\n", maxRt)
fmt.Printf("p90: %v\n", p90)
fmt.Printf("p95: %v\n", p95)
fmt.Printf("p99: %v\n", p99)
fmt.Printf("ElapsedTime: %v\n", allElapsedTime)
fmt.Printf("SuccessTime: %v\n", allSuccessTime)
fmt.Printf("Total: %v\n", ret.Requests)
fmt.Printf("Success: %v\n", ret.Success)
fmt.Printf("Error: %v\n", ret.Errors)
report := StressTestReportForAddress {
Total: ret.Requests,
Success: ret.Success,
Error: ret.Errors,
SuccessRate: successRate,
Rps: rps,
AvgRt: avgRt,
MinRt: minRt,
MaxRt: maxRt,
P90Rt: p90,
P95Rt: p95,
P99Rt: p99,
SuccessTime: allSuccessTime,
AllTime: allElapsedTime,
}
jsonBytes, err := json.Marshal(report)
if err != nil {
fmt.Println(err)
return ""
}
jsonString := string(jsonBytes)
fmt.Println(jsonString)