最难不过二叉树

API性能测试指标以及压测方式

2023-02-01

当我们开发完server的api后,我们需要对这些api做性能测试,通过压测来了解我们的server所能承载的最大请求量,以及通过压测结果分析出性能瓶颈,来对我们的API做进一步的性能优化。

本文首先介绍API性能测试的指标,然后使用ab这个压测工具分析Python Flask server和Go Gin server 的性能。

性能测试指标

用来衡量API性能的指标主要有三个:

  • 并发数(concurrent):并发数只某个时间范围内。同时在使用系统的用户个数。从实际场景来看,并发数就是同时使用该服务器接口的客户端数,这些客户端可能调用不同的API。严格意义上来说,并发数是指同时请求同一个API的用户个数。
  • 每秒查询数(QPS):QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。QPS = 并发数 / 平均请求响应时间。QPS就是一秒内查询完成的次数。
  • 每秒事务数(TPS):一个事务是指客户端向服务器发送请求,然后服务器做出反应的过程。客户端在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。TPS就是一秒内处理事务完成的次数。

这里有一些关键点是需要讨论的。

第一点,TPS和QPS有什么区别?

如果是对一个查询接口(单场景)压测,且这个接口内部不会再去请求其他接口,那么 TPS=QPS,否则,TPS≠QPS。如果是对多个接口(混合场景)压测,假设 N 个接口都是查询接口,且这个接口内部不会再去请求其他接口,QPS=N*TPS。

第二点,QPS和并发数是怎么样的关系?

QPS = 并发数 / 平均请求响应时间。比如我们有500个客户端,并发对API发起请求,平均的请求返回时间是200ms,那么我们可以算出该接口的QPS=500/0.2=2500。

在并发数设置过大时,API 同时要处理很多请求,会频繁切换上下文,而真正用于处理请求的时间变少,反而使得 QPS 会降低。并发数设置过大时,请求响应时间也会变长。API 会有一个合适的并发数,在该并发数下,API 的 QPS 可以达到最大,但该并发数不一定是最佳并发数,还要参考该并发数下的平均请求响应时间。

比如下面这个API压测数据,并发50至并发300的QPS都是220左右,但是并发2000的平均请求响应时间是60ms,平均QPS下降到33,那么我们可以理解为并发2000下API处于不可用状态了。

方案平均请求响应时间(ms)平均QPS
并发146.12221.68
并发106.706149.11
并发504.585218.08
并发1004.513221.58
并发2004.637215.69
并发3004.528220.85
并发200060.00033.33

第三点,哪些指标最能反应API的处理性能?

衡量 API 性能的最主要指标是 QPS,但是在说明 QPS 时,需要指明是多少并发数下的 QPS,否则毫无意义,因为不同并发数下的 QPS 是不同的。举个例子,单用户 100 QPS 和 100 用户 100 QPS 是两个不同的概念,前者说明 API 可以在一秒内串行执行 100 个请求,而后者说明在并发数为 100 的情况下,API 可以在一秒内处理 100 个请求。当 QPS 相同时,并发数越大,说明 API 性能越好,并发处理能力越强。当然上文也提到了,做性能评估时还需参考该并发数下的平均请求响应时间,如果并发数大的但平均请求响应时间还是很稳定,那么说明该API性能越好。

压测方法

压测工具我们选择使用ab,ab是apachebench命令的缩写。ab的原理:ab命令会创建多个并发访问线程,模拟多个访问者同时对某一URL地址进行访问。

安装方法:apt install apache2-utils

接下来我们将压测两个web server下的api,对比观察哪个框架具有更好的并发性能。这两个web server分别是:

  • Python的Flask
  • Go的Gin

该两个框架都将实现一个数字求和计算的http GET接口,计算结果将以json形式返回给客户端。

Python Flask的压测

Flask 提供的接口如下:

from flask import request, Flask, jsonify

app = Flask(__name__)

@app.route('/get_result', methods=["GET"])
def query_user_info() :
    a = int(request.args.get('a'))
    b = int(request.args.get('b'))

    result = {}
    result["result"] = a+b

    return jsonify(result)


if __name__ == '__main__':
    app.run(host="127.0.0.1", port=9999, threaded=False, processes=20)

压测过程

root@JamesLee:~# ab -n 2000 -c 1200  "http://127.0.0.1:9999/get_result?a=10&b=20"
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 200 requests
Completed 400 requests
Completed 600 requests
Completed 800 requests
Completed 1000 requests
Completed 1200 requests
Completed 1400 requests
Completed 1600 requests
Completed 1800 requests
Completed 2000 requests
Finished 2000 requests


Server Software:        Werkzeug/2.2.2
Server Hostname:        127.0.0.1
Server Port:            9999

Document Path:          /get_result?a=10&b=20
Document Length:        14 bytes

Concurrency Level:      1200  //并发数,这个就是我们-c 里指定的值
Time taken for tests:   0.978 seconds // 共使用了多少时间
Complete requests:      2000 // 完成的请求数
Failed requests:        0  //失败的请求数
Total transferred:      358000 bytes //总共传输字节数,包含http的头信息等
HTML transferred:       28000 bytes  //html字节数,实际的页面传递字节数
Requests per second:    2045.24 [#/sec] (mean) //QPS,反应服务器的吞吐量
Time per request:       586.729 [ms] (mean) // 用户平均请求等待时间
Time per request:       0.489 [ms] (mean, across all concurrent requests) // 服务器平均处理时间,上面的Time per request/Concurrency Level就是本项的值
Transfer rate:          357.52 [Kbytes/sec] received //每秒获取的数据长度

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   2.4      0      10
Processing:     7   62   9.1     63      73
Waiting:        5   61   9.2     63      73
Total:         16   62   7.4     63      75

Percentage of the requests served within a certain time (ms)
  50%     63  //50%的请求在63ms内返回 
  66%     64
  75%     66
  80%     66
  90%     67
  95%     67
  98%     68
  99%     73
 100%     75 (longest request)  //100%的请求在75ms内返回

这里解析下ab工具的参数

-n 测试几次

-c 模拟多少客户端

-T 内容类型。这个一般和-p 一起使用(Content-type header to use for POST data.)

-p 包含POST参数的文件(File containing data to POST.)

注意,最后的 URL 需要加引号。

比如

ab -n 2000 -c 1200  "http://127.0.0.1:9999/get_result?a=10&b=20"

表示对"http://127.0.0.1:9999/get_result?a=10&b=20"这个接口在并发数为1200的条件下访问了2000次。

对于上面指令执行的结果,主要关注这几个指标:

  • Concurrency Level:并发数,这个就是我们-c 里指定的值。
  • Failed requests: 请求的失败数,有时候你的接口在高并发下会接口不可用,导致请求失败。
  • Requests per second:平均的QPS。
  • Time per request(mean, across all concurrent requests): 平均的请求响应时间,单个客户端平均的请求响应时间。

那么从上述指令执行接口来看,该接口在1200并发下的平均QPS是2045,每个并发的平均请求响应延时是0.489ms。这就是Python Flask server的/get_result接口的性能。一般我们还需要测试多组数据,以得出该接口的最佳性能数据。

方案平均请求响应延时(ms)平均QPS
并发12.865349.00
并发500.5811693.41
并发1000.5591790.19
并发2000.5261901.99
并发5000.4922032.48
并发8000.4942022.75
并发10000.4952019.87
并发20000.4932027.89
并发3000\\

性能分析:

  • 在并发量小的时候,测出来的QPS是偏低的,此时的QPS并不能反应系统的性能。
  • 随着并发量的提升,QPS会随之提升,之后到达2020左右后保持稳定,平均请求响应延时稳定在0.5ms。
  • 当并发量超过一定数量后,整个接口变得不可用,请求会超时,因此我们认为该Python Flask server的/get_result接口的极限值应该在并发2000左右,此时QPS会在2020左右。

Go Gin的压测

作为对比,我们会用Gin也实现一个一样的接口,然后也使用ab工具进行压测和做性能分析。

package main

import (
    "strconv"
    "github.com/gin-gonic/gin"
)

func GetResult(c *gin.Context) {
    a,_ := strconv.Atoi(c.Query("a"))
    b,_ := strconv.Atoi(c.Query("b"))

    result := a+b

    c.JSON(200, gin.H{
        "result": result,
    })
}

func main() {
    r := gin.Default()

    // 普通用户接口
    r.GET("/get_result", GetResult)

    r.Run(":7777") 

}

这里直接给出压测数据:

方案平均请求响应延时(ms)平均QPS
并发10.1109094.95
并发500.02050046.86
并发1000.01951541.94
并发2000.02051010.26
并发5000.02050186.69
并发8000.02049999.38
并发10000.02050032.46
并发20000.02245526.30
并发30000.02245969.46
并发50000.02441834.40
并发80000.02540013.36
并发100000.02441985.23
并发150000.2573897.59
并发200000.3922549.04

性能分析:

  • Gin 接口在并发1000时能保持50000左右的QPS,在并发10000仍能保持40000左右的QPS,性能十分强悍。
  • 从并发15000开始,QPS开始断崖式下降,基本处于不可用状态,我们可以认为Gin server的/get_result接口的极限值应该在并发10000左右,此时QPS会在40000左右。