性能测试

性能测试的目的是发现系统的瓶颈;做性能测试前,需要对系统有较好的了解,能预估出系统的瓶颈。

性能测试有两种类型,负载测试和压力测试:

  • 负载测试(Load Testing):主要是测试软件系统是否达到需求文档设计的目标,如软件在一定时期内,最大支持多少并发用户数,软件请求出错率等。关注点是在满足响应正常的条件下,请求的极限值能有多高。
  • 压力测试(Stress Testing):主要是测试硬件系统是否达到需求文档设计的性能目标,如在一定时期内,系统的CPU利用率,内存使用率,磁盘I/O吞吐率,网络吞吐量等。关注点是性能指标,给定一个较高的基准,系统的最好响应情况。

压力测试

压测时重点关注的指标:并发用户数、TPS、RT、事务失败率、吞吐量、CPU和内存占用量。

注意事项:

  1. 测试机与被测试机要分开
  2. 不要对线上的服务器做压力测试

负载测试

一般情况是没有必要严格区别压力测试和负载测试,目标都是为了评估系统的极限工作状态或者发现系统的性能瓶颈进一步做系统性能优化,下面列举的工具一般情况下对这两种测试场景都是支持的。

指标

指标可简单分类为Web站点指标和服务器指标。

Web站点指标包括:

  • QPS:Query Per Second,每秒查询数,指服务器每秒能响应的查询次数;包括峰值QPS、平均QPS,单机QPS、集群QPS等不同度量;
  • TPS:Transaction Per Second,每秒事务数。相对于查询,事务一般指写请求;
  • PV:Page View,页面访问量,即页面浏览量或点击量,用户每次刷新(或一定间隔后,如5分钟)即被计算一次。可分析服务访问日志计算得到;
  • UV :Unique Visitor,独立访客,统计1天内访问站点的不同用户数。同样也可分析服务访问日志计算得到;
  • RT:Response Time,响应时间,是指系统对请求作出响应的时间,一般取平均响应时间。可通过Nginx、Apache之类的Web Server得到。
  • DAU:Daily Active User,日活跃用户数。常用于反映站点等运营情况。统计周期为一日,不同公司(站点)对活跃的定义不一致;
  • MAU:Month Active User:月活跃用户数;

服务器指标:

  • CPU:使用率一般不应超过80%
  • Mem:同上
  • 文件句柄数:
  • Throughput:吞吐量,系统在单位时间内处理请求的数量,可具化为TPS、QPS等;
  • 吞吐率(Requests per second):总请求数/处理完成这些请求数所花时间
  • 延迟:
  • 并发数:Concurrency Level,并发度,并发连接数,指系统同时能处理的请求数量;或同时在线用户数等;
  • 用户平均请求等待时间:处理完成所有请求数所花费的时间/(总请求数/并发用户数)
  • 服务器平均请求等待时间:处理完成所有请求数所花费的时间/总请求数

目标

  1. 发现系统的指标量级和性能瓶颈;
  2. 进一步为性能优化提供可视化的依据,故而最好需要压测工具自带生成报告的能力;

站在管理员的角度考虑需要关注的性能点:

  1. 响应时间
  2. 服务器资源使用情况是否合理
  3. 应用服务器和数据库资源使用是否合理
  4. 系统能否实现扩展
  5. 系统最多支持多少用户访问、系统最大业务处理量是多少
  6. 系统性能可能存在的瓶颈在哪里
  7. 更换那些设备可以提高性能
  8. 系统能否支持7×24小时的业务访问

站在开发(设计)人员角度去考虑

  1. 架构设计是否合理
  2. 数据库设计是否合理
  3. 代码是否存在性能方面的问题
  4. 系统中是否有不合理的内存使用方式
  5. 系统中是否存在不合理的线程同步方式
  6. 系统中是否存在不合理的资源竞争

分类

性能测试,看要测试什么对象,如代码的执行性能,接口的耗时,单机的承载,DB的读写极限等,根据不同的待压测对象使用不同的工具:;

  1. 代码:JMH
  2. 接口:JMeter
  3. web:前端页面,知识盲区;
  4. DB:sysbench

应用类型:

  • CPU密集型应用
  • IO密集型应用

JMH

另起一章,参考基准测试工具JMH。

JMeter

另起一章,参考JMeter学习笔记。

Gatling

官网,使用Scala+Java实现、开源(GitHub,7K Star,1.2K Fork)压力测试神器,官方宣言Load test as code,即用代码方式做压力测试。

从代码级别支持性能测试,能生成丰富多彩的报告,包含测试案例中收集的所有指标。同时提供脚本从命令行运行性能测试,支持录制(record)功能。

实战

<dependency>
	<groupId>io.gatling</groupId>
	<artifactId>gatling-core</artifactId>
	<version>3.3.1</version>
</dependency>

每一个Gatling测试都要继承Simulation类:

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class ApiGatlingSimulationTest extends Simulation {
	// 定义protocol
	val httpProtocol = http
		.baseUrl("http://computer-database.gatling.io") // 相对路径的根路径
		.acceptHeader("text/html,application/xhtml+xml;q=0.9,*/*;q=0.8") // 通用头
		.doNotTrackHeader("1")
		.acceptLanguageHeader("en-US,en;q=0.5")
		.acceptEncodingHeader("gzip, deflate")
		.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0")
	// 一个repeat对应一次模拟测试,使用{}
	val scn = scenario("InjectAndGet").repeat(1000, "n") {
		exec(
			http("AddPerson-API")
			.post("http://localhost:8080/api")
			.header("Content-Type", "application/json")
			.body(StringBody("""{"firstName":"John${n}","lastName":"Smith${n}","birthDate":"1980-01-01", "address":{"country":"pl","city":"Warsaw","street":"Test${n}","postalCode":"02-200","houseNo":${n}}}"""))
		.check(status.is(200))
		)
		// 或.pause(629 milliseconds)
		.pause(Duration.apply(5, TimeUnit.MILLISECONDS))
	}.repeat(1000, "n") {
		exec(
			http("GetPerson-API")
			.get("http://localhost:8080/api/${n}")
			.check(status.is(200))
		)
	}
	// 30个并发用户&测试10min
	setUp(scn.inject(atOnceUsers(30))).maxDuration(FiniteDuration.apply(10, "minutes")
	//.protocols(httpProtocol)
	)
}

对于POST请求,还支持formParam:

.exec(
	http("post-demo")
		.post("/api")
		.formParam("name", "Beautiful Computer") // Note the triple double quotes: used in Scala for protecting a whole chain of characters (no need for backslash)
		.formParam("introduced", "2012-05-30")
)

配置maven插件:

<build>
	<plugins>
		<plugin>
			<groupId>io.gatling</groupId>
			<artifactId>gatling-maven-plugin</artifactId>
			<version>${gatling-plugin.version}</version>
			<configuration>
				<!-- 测试脚本 -->
				<simulationClass>com.anoyi.test.ApiGatlingSimulationTest</simulationClass>
				<!-- 结果输出地址 -->
				<resultsFolder>/Users/admin/code/gatling</resultsFolder>
			</configuration>
		</plugin>
	</plugins>
</build>

执行命令:mvn gatling:execute,生成的所有HTML报告都可以在performance-test/build/gatling-results目录下找到

ab

Apache Benchmark,统计功能强大。默认使用GET方法测试。

实战

Ubuntu安装:sudo apt-get install apache2-utils
Redhat安装:yum -y install httpd-tools

命令格式:ab -n 100 -c 10 'http://10.34.216.49:8000/test'

常用参数:

-c,concurrency,测试并发度,即连接数
-t,测试时间,此时默认-n默认为50000
-n,测试请求次数,一般-t或-n选一个
-p,postfile,读取json文本request,需要一并使用-T
-u ,putfile,发送PUT请求时需要上传的文件,需要一并使用-T
-T,Content-Type,指明请求数据格式

示例:

# 如果只用到一个Cookie,只需键入命令:
ab -n 100 -C key=value http://test.com/
# 如果需要多个Cookie,就直接设Header:
ab -n 100 -H "Cookie: Key1=Value1; Key2=Value2" http://test.com/

比较重要的返回信息:

信息示例解读
Concurrency Level1000并发数
Time per request(mean)465.338[ms]所有并发用户都请求一次的平均时间
Time request0.247[ms]单个用户请求一次的平均时间,计算全部次数的均值
Requests per second(mean)2148平均每秒请求数,服务器的吞吐量
Percentage of the requests served within a certain time(ms)95% 84695%的请求在846ms内返回,可看95线,99线

测试POST方法时,requestBody一般是JSON文本,需要放入txt格式文件中,且以''括起来,使用-p -T。

测试遇到:socket: Too many open files时,需调整可打开文件句柄数:ulimit -n 20480。

问题:

  1. 无法直接在命令行测试不同URL,只能测试一个URL?
  2. 输出结果格式无法修改?

wrk

几乎纯C实现、开源(GitHub,40.4K Star,3K Fork),
wrk只能运行在类Unix的系统上。git clone源码make编译,make成功后同目录生成wrk文件,可复制到bin目录。

使用:

wrk <选项> <被测HTTP服务的URL>
-c, --connections,跟服务器建立并保持的TCP连接数量
-d, --duration,压测时间
-t,--threads,配置线程
-s, --script,指定Lua脚本路径
-H, --header,为每一个HTTP请求添加HTTP头
--latency,在压测结束后打印延迟统计信息
--timeout,超时时间
-v,--version打印正在使用的wrk的详细版本信息
# 其中数字参数,支持国际单位 (1k, 1M, 1G)
# 其中时间参数,支持时间单位 (2s, 2m, 2h)

示例:
wrk -t8 -c200 -d30s --latency "http://www.baidu.com"

Siege

官网:http://www.joedog.org
一款HTTP/HTTPS负载测试和基准测试工具,支持基本身份验证、Cookie、HTTP、HTTPS和FTP协议,可配置模拟并发用户数。

实战

安装:yum install siege -y

安装成功与否,查看版本:siege -V

帮助文档:

siege --help
-C, --config:查看siege当前的配置信息
-V, --version:版权说明信息
-c, --concurrent=NUM:并行启动(访问)用户数,默认是10
-t, --time=NUMm:压力测试时间,比如-t5表示持续时间是5分钟
-b, --benchmark:基准测试,请求之间没有延迟。
-g, --get get方式请求
-d, --delay=NUM 时间延迟,每个请求之间的延迟时间
-i, --internet 模拟用户,随机点击的URL
-r, --reps=NUM:每个连接发出的请求数量,与t有些类似,两者设置一个即可
-f, --file=FILE:对应一个文件,这个文件里每一行为一个URL链接,格式如:
-m, --mark="text"  在日志里标记的字符串标识
-H, --header="text" 在Header里增加的字符串标识
-A, --user-agent="text" 在user-agent里增加的字符串标识
-u, --url="URL",设置被测Web的URL

举例:10个并发,每个连接10个请求,间隔1秒请求压测。

siege -u www.baidu.com -d1 -r10 -c 10
HTTP/1.1 200     0.05 secs:  143913 bytes ==> GET  /static/superman/js/lib/jquery-1-cc52697ab1.10.2.js
Transactions:            1017 hits  // 总处理数量
Availability:          100.00 %    // 成功请求百分百
Elapsed time:           10.06 secs //总耗时
Data transferred:         35.65 MB //总传输数据量
Response time:            0.03 secs  // 响应时间
Transaction rate:        101.09 trans/sec // 每秒处理请求数
Throughput:            3.54 MB/sec     // 吞吐量
Concurrency:            3.38           // 并发数
Successful transactions:        1017   //成功处理次数
Failed transactions:             0    //请求失败数
Longest transaction:          0.33   // 请求最长耗时
Shortest transaction:          0.00  //请求最短耗时

Locust

官网,使用Python语言写的分布式开源(GitHub,28.1K Star,3.2K Fork)压测工具。

实战

基于pip安装:pip install locustio

使用官方提供的云服务,Locust Could。

使用Locust时依赖的Python模块,参考setup.py:

  1. gevent:在Python中实现协程的第三方库。协程又叫微线程Corouine。使用gevent可以获取极高的并发能力;
  2. flask:Python的一个web开发框架,和django相当;
  3. requests:支持http/https访问的库;
  4. msgpack:一种快速、紧凑的二进制序列化格式,使用与类似json的数据;
  5. pyzmq:MQ中间件,可以把Locust运行在多个进程或多个机器(分布式)
  6. geventhttpclient-wheels
  7. ConfigArgParse
  8. psutil
  9. Flask-BasicAuth

入门示例:

from locust import HttpUser, task, between

class QuickstartUser(HttpUser):
	wait_time = between(1, 2)
	
	def on_start(self):
		self.client.post("/login", json={"username":"foo", "password":"bar"})
	
	@task
	def hello_world(self):
		self.client.get("/hello")
		self.client.get("/world")
	
	@task(3)
	def view_item(self):
		for item_id in range(10):
			self.client.get(f"/item?id={item_id}", name="/item")

Locust压力测试使用总结
Locust压测框架入门

sysbench

模块化,跨平台以及多线程的DB性能测试工具

参考

更多推荐