什么是压测?

  • 压测(Load Testing)是一种测试方法,它通过模拟实际用户在不同情景下的并发量、访问量或者负载量,来测试被测系统的性能、稳定性和可靠性。压测可以帮助我们了解系统的承载能力,找出系统的性能瓶颈和潜在问题,为系统优化提供数据支持。
  • 压测的意义在于:
  • 提升系统性能:通过压测可以找出系统的性能瓶颈,优化系统架构、代码和数据库等方面,提升系统的性能和响应速度。
  • 保障系统稳定性:压测可以模拟高并发的请求,发现系统的容量极限和峰值,避免系统出现因压力过大导致的宕机、崩溃等问题。
  • 确保系统可靠性:压测可以模拟多种情景下的用户行为,测试系统在不同负载下的性能表现,以保证系统的可靠性和稳定性,从而提升用户体验。
  • 减少成本浪费:通过压测可以发现系统的问题,避免在生产环境中出现严重的问题,从而减少因故障造成的成本浪费。
  • 总之,压测是一种非常重要的测试手段,可以帮助我们了解系统性能和稳定性,提升用户体验和满意度,降低故障率和成本。

需求

一个文件中有若干个接口,需要对这些接口进行压测。需要灵活指定并发数量,压测时间

场景模拟

  1. 假如我们指定并发数量为100,压测时间10分钟。
  2. 想象这样一个场景,100个人分别请求一个接口,在10分钟内不断发起请求。
  1. 实际情况下,有的人操作快,有的人操作慢。
  1. 所以实际上还需要一个参数:请求频率。假如请求频率是 1 秒 5 次,即 200 ms 1 次
  1. 那么对应实际场景就是,100个人不断请求,请求频率是 每秒钟请求 5 次

如何开发?

受开源仓库 go-stress-testing 代码启发。开发步骤如下

step 1 创建场景

  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 创建任务

  1. 根据并发数,来创建任务
  2. 这里假如待压测接口数量为m 并发数为 n
  3. 如果 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
	})
}
  1. 任务创建完成之后加入到 step1 步骤中创建的场景中

step 3 运行场景

  1. 主要难点在于如何模拟真实场景压测
  2. 这里的功能实现利用了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 测试并处理压测数据

  1. 使用四个接口,并发数数10,压测1分钟,压测频率 500ms
  2. 测试结果如下
  3. 计算其它指标
  4. 处理转化成 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"
}
  1. 逻辑代码
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)

完结撒花