
|----------------|
| 你好,我是安然无虞。 |
文章目录

性能测试概述
性能测试通常比较复杂, 要真正做好很不容易.
-
需要有产品视野, 明白真实场景下, 用户是怎样使用产品的, 这样才能知道哪些场景是用户大量使用的.
-
需要有开发视野, 明白产品架构, 甚至一些实现细节, 这样才能对哪些 使用场景 会带来性能问题 了然于胸.
-
需要有测试经验, 结合前面的知识, 写出良好的性能测试用例.
-
需要有开发技能, 灵活使用各种测试工具, 有的测试工具需要二次开发, 甚至市场上没有现成可以使用的测试工具, 必须得自己开发测试工具.
所以通常 性能测试 和 自动化测试 能力 是高级测试人员的 必备技能.
性能测试 和 功能测试一样, 通常需要经历如下3个过程:
- 分析需求、确定性能测试场景
- 编写测试计划、测试用例
- 执行测试
在我们开始前, 还有一个很重要的问题:
也就是什么时候开始着手 启动 性能测试的工作, 包括上面说的3个阶段流程?
因为性能测试的需求指标, 随着产品的开发过程会非常容易产生变动.
比如:
原来估计的性能瓶颈场景, 随着开发过程, 会发生改变, 导致早早写好的测试用例没有用.
再比如, 原来计划的产品 生产环境 (包括运行设备、操作系统等), 到了后来, 也会发生改变, 导致原来准备的硬件软件环境没有意义, 产生金钱和时间上的损失.
所以这里建议, 在产品功能测试 完成几轮, 产品相对比较稳定, 再启动性能测试.
当然, 参与性能测试的人员, 预先做一些准备是必要的. 比如: 对测试工具的熟悉、相关基础知识的学习等.
需求分析
确定性能测试场景
功能测试需要测试系统的所有功能点, 而性能测试只需要关注 系统功能点中 比较容易成为系统性能瓶颈的部分.
比如, 一个商城系统, 通常系统的瓶颈 在于 大量客户登录浏览商品, 秒杀抢购某个商品 的业务场景.
而 管理员 后台操作 通常不会成为性能瓶颈的, 因为管理员就寥寥数人而已.
所以性能测试的场景应该是这些客户浏览商品、秒杀抢购, 而不是去测试管理员 后台操作.
确定性能测试场景, 测试人员需要了解产品的所有功能点, 并且了解业务使用, 甚至了解系统的实现细节.
往往测试工程师很难做到这些所有要求, 这就需要和产品团队、开发团队 多方面的合作.
性能测试工程师应该仔细分析 产品的功能, 并且和系统的设计者交流, 才能合理、全面的选定性能测试场景.
性能测试场景确定后, 就应该和产品部门以及开发部门一起确定 硬件环境、软件环境和性能指标.
性能指标
如果我们要测试的是 白月crm系统 这样的一个 API服务系统.
常见的性能指标有:
- 支持并发连接数量 (使用业务用户数量)
- 单位时间处理请求数量
- 响应正确的数量、百分比
- 响应错误的数量、百分比
- 响应超时的数量、百分比
- 平均响应时间
而且还应该包括, 在执行性能测试的过程中, 被测系统对硬件资源的占用情况.
最重要的是:
- CPU占用率
- 内存使用率
有时也需要观察如下指标:
- 磁盘访问量
- 网络吞吐量
运行环境、数据配置
给出性能测试指标, 却不提 在什么样的 运行环境、数据配置 下测试的, 是没有意义的.
- 运行环境
运行环境 就是 被测系统在服务客户时, 所运行的 硬件环境 和 软件环境.
脱离被测系统的运行环境 谈指标是毫无意义的.
进行测试时, 要尽量使测试环境 贴近 实际的 运行环境.
运行环境, 包括:
- 硬件环境
包括: 服务器机型、CPU配置、内存配置、网卡配置、硬盘配置等.
有些被测系统 运行在集群系统, 就需要指明集群的整体环境配置.
有些被测系统运行在云平台上, 也需要指明相应的环境配置.
- 软件环境
包括: 操作系统、数据库 和 被测系统运行时所依赖的其他第三方组建服务, 比如: 消息队列系统、缓存系统、异步任务系统、反向代理系统等.
重要系统的设置项也应该指明, 比如缓存的内存大小分配, 数据库系统的参数设置等.
- 数据配置
数据配置 是性能测试的 业务数据设置, 不同的系统有各自的业务数据.
比如 白月CRM系统 包括: 多少注册用户、多少药品数据、多少业务订单.
业务数据配置(数据环境)对 测试结果影响非常大.
编写测试计划、测试用例
确定测试资源, 包括: 测试工程师人员、测试工具、所需测试硬件 (包括何时采购到位)、测试预计启动时间、结束时间.
这和功能测试计划没有太大差异, 不赘述了.
性能测试用例, 就是根据前面需求分析阶段得出的测试场景来写, 当然要注意包括如下信息:
- 运行硬件、软件环境、数据配置
- 对被测系统的输入, 通过工具测试出的系统性能指标
- 对资源的占用指标
因为性能测试通常执行的比较复杂, 为了方便测试, 测试用例中可以附加上测试工具的对应 操作步骤 和 测试代码等.
下面是 对 白月SMS2 系统的 一个性能测试用例 示例
-
测试用例场景分析
-
在月末结算时,大量代理商(销售) 会登陆 白月SMS2 系统,查询 自己的销售数据, 对系统造成性能冲击.
-
根据系统的架构设计和数据库设计, 代理商查询销售数据 会涉及到 服务端对 订单表,用户表、药品表、代理商表 进行联合查询, 比较容易成为性能瓶颈,
-
尤其是 月末 结算时, 他们会在同一时间段,查询销售数据, 前期系统曾经出现 查询卡死现象.
-
本用例针对该场景进行测试:
目前系统共有 :
客户 2000 左右, 性能测试 模拟 3000
代理商账户 1800 左右, 性能测试 模拟 2000
药品 800 左右, 性能测试 模拟 1000
历史订单数量 40000 左右, 性能测试 模拟 50000
- 运行主机
text
主机 : 惠普 HPE ProLiant DL580 Gen10 Server
CPU : Intel® Xeon® Scalable 5220 (8 core, 2.2 GHz, 24.75 MB, 125 W)
内存 : 32 GB (2x 16 GB) RDIMM
硬盘 : 1TB SSD ( 三星970 Pro M2 SSD)
网络 : 千兆网口 连接 千兆 交换机 连接 千兆网口测试主机
- 操作系统
python
CentOS 7.2 x64
- 其他软件系统
python
MySQL 5.7 参数配置由 产品自动化安装工具 设置
Memcached 参数配置由 产品自动化安装工具 设置
RabbitMQ 参数配置由 产品自动化安装工具 设置
- 测试工具
python
黑羽压测 1.3.1
- 数据配置
text
客户账户 3000 条
代理商账户 2000 条
药品 1000 个
订单数量 50000 条 :
为每个代理商,创建 25 条订单,
每条订单中,随机选择一个客户,随机选择一种药品.
- 测试过程
使用 hyload 模拟高峰期 代理商登录查询场景.
- 单个 代理商 的 模拟操作如下:
python
1. 从登录网页 登录网站
2. 查询自己的所有订单(25条,分5页展示)
每隔30秒,点击下一页订单,到底后,每隔30秒,点击前一页订单
这样循环2次
预估 单个代理商操作总时长 (30*4*2)*2 = 480秒 (8分钟)
- 整个 性能场景如下:
每秒2个代理商登录, 直到 2000 人全部登录操作完毕. 预计耗时 1000秒
每个代理商各自进行上述流程的操作.
整个测试预计耗时 1000+480=1480秒.
注意:
- 测试前,进入黑羽压测监控统计界面,先点击 右上角
清除按钮,确保重新产生统计文件 - 测试前,使用 黑羽压测 部署资源统计软件到被测主机,删除老的资源统计数据,并启动 对 被测主机的 资源统计进程
- 测试完成后, 停止对 被测主机的 资源统计进程,获取资源统计数据
- 预期结果
python
并发连接数量 : 系统要求最大支持10000代理商客户同时访问,
所以该场景 不应该出现 HTTP 连接超时错误
请求 正确响应率 = 100%
平均响应时长 <= 500ms
响应时长 [500ms,1000ms] 不超过 20个
响应时长 [1000ms,2000ms] 不超过 10个
响应时长 [2000ms,5000ms] 不超过 5个
在执行性能测试的过程中,被测系统对硬件的资源占用情况,
python
CPU 占用率 没有出现连续1分钟超过 70%
系统使用内存使用率 不超过 50% (16G)
执行测试
运行环境搭建
首先要搭建 硬件运行环境,包括 运行主机、网络环境 等. 根据产品规划,可能简单到 一台主机即可,也可能复杂到 搭建 大型的集群环境、云集群环境,
现在很多公司产品都部署到云服务器上,这种情况我们做性能测试,有两种方式:
- 本地测试
需要购买 几台 主机,放到公司实验室里.
这些主机 一部分是组建被测系统, 一部分是安装测试工具.
然后在本地进行测试.
- 云端测试
需要购买 几台云主机, 一部分是组建被测系统, 一部分是安装测试工具.
然后直接 在 云端环境进行测试.
千万不要 只是被测系统 在 云端, 自己本地用测试工具进行测试.
因为 本地到云端 网络 节点很多,时延较长, 而且带宽往往也是受限的,失去了性能测试的意义.
硬件环境准备好后, 要在其上安装 软件运行环境,包括:操作系统、数据库 等 系统依赖的软件.
用例执行
创建数据环境
执行性能测试用例,通常需要准备大量的测试数据.
大家不要小看性能测试的 数据准备,往往是非常麻烦的.
特别是首次创建数据环境. 需要自己编写、调试创建环境的 脚本,程序,很考验开发技能.
比如,白月SMS2 系统, 上面给出的示例用例 就需要 准备大量客户、销售、药品、订单数据. 这些数据怎么创建?
可以写Python程序,调用API插入.
当然也可以直接用黑羽压测,导入数据.
上面给出的 测试用例,用黑羽压测进行初始化,代码如下:
python
# 定义 创建 客户、代理商、药品的数量
NUM_CUSTOMER = 3000
NUM_SALES = 2000
NUM_MEDICINE = 1000
# 每个代理商创建多少订单
ORDERS_PER_SALES = 25
from urllib.parse import quote
from base64 import b64encode
# 创建客户端
client = HttpClient('127.0.0.1', # 目标地址:端口
timeout=10 # 超时时间,单位秒
)
# 管理员登录
username = 'byhy'
password = '88888888'
up = b64encode(quote(username+'#$%'+password).encode())
print('管理员登录')
response = client.sendAndRecv(
'POST',
'/api/mgr/signin',
data={
'up':up
})
# 记录管理员 token,后面直接使用
admin_jwt = response.getheader('jwt')
pprint(response.json('utf8'))
# 添加客户
print(f'添加{NUM_CUSTOMER}客户')
# 记录 客户 id,后面添加订单时要用到
customerids = []
for i in range(NUM_CUSTOMER):
username = f'hospital_{i}'
print(f'添加客户 {username}')
response = client.sendAndRecv(
'POST',
'/api/mgr/users',
headers={'Authorization':admin_jwt},
json= {
"action":"add_customer",
"data":{
"username": username,
"password": "88888888",
"realname": f"测试医院{i}",
"desc": f"测试医院{i}",
"phone": f"1380000{i:04d}"
}
}
)
ret = response.json('utf8')
if ret["retcode"] != 0:
print('添加失败')
exit()
customerids.append(ret["id"])
# 添加代理商
print(f'添加{NUM_SALES}代理商')
for i in range(NUM_SALES):
username = f'sales_{i}'
print(f'添加代理商 {username}')
response = client.sendAndRecv(
'POST',
'/api/mgr/users',
headers={'Authorization':admin_jwt},
json= {
"action":"add_distributor",
"data":{
"username": username,
"password": "88888888",
"realname": f"测试代理商{i}",
"desc": f"测试代理商{i}",
"phone": f"1380001{i:04d}"
}
}
)
ret = response.json('utf8')
if ret["retcode"] != 0:
print('添加失败')
exit()
# 添加药品
print(f'添加{NUM_MEDICINE}药品')
# 记录 药品 id,后面添加订单时要用到
medicineids = []
for i in range(NUM_MEDICINE):
medicinename = f'青霉素_{i:04d}'
print(f'添加药品 {medicinename}')
response = client.sendAndRecv(
'POST',
'/api/mgr/medicines',
headers={ 'Authorization':admin_jwt },
json= {
"action":"add_one",
"data":{
"name": medicinename,
"desc": "青霉素 国字号",
"sn": f"SN00001{i:04d}"
}
}
)
ret = response.json('utf8')
if ret["retcode"] != 0:
print('添加失败')
exit()
medicineids.append(ret["id"])
# 添加订单
print(f'每个代理商 创建{ORDERS_PER_SALES}条订单')
for i in range(NUM_SALES):
# 代理商登录
username = f'sales_{i}'
print(f'代理商{username}登录')
password = '88888888'
up = b64encode(quote(username+'#$%'+password).encode())
response = client.sendAndRecv(
'POST',
'/api/distributor/signin',
data={
'up':up
})
sales_jwt = response.getheader('jwt')
# 创建订单
for j in range(ORDERS_PER_SALES):
time1 = time.time()
# 随机挑选 客户 和 药品, 作为订单的采购内容
from random import randint
customerid = customerids[randint(0,len(customerids)-1)]
medicineid = medicineids[randint(0,len(medicineids)-1)]
ordername = f"测试订单_{username}_{j}"
print(f'创建订单 {ordername}')
response = client.sendAndRecv(
'POST',
'/api/distributor/orders',
headers={'Authorization':sales_jwt},
json={
"action": "wf_order",
"wf_action": "wf_submit_order",
"data": {
"name": ordername,
"desc": ordername,
"customer_id": customerid,
"medicinelist": [
{
"id": medicineid,
"v": "20000",
"name": "青霉素盒装"
}
]
}
}
)
ret = response.json('utf8')
if ret["retcode"] != 0:
print('创建订单失败')
exit()
orderid = ret["id"]
# 审批订单
# print('审批订单')
response = client.sendAndRecv(
'POST',
'/api/mgr/orders',
headers={'Authorization':admin_jwt},
json={
"action": "wf_order",
"wf_action": "wf_approve_order",
"data": {
"id": orderid,
"comment": "同意"
}
}
)
ret = response.json('utf8')
if ret["retcode"] != 0:
print('审批订单失败')
exit()
# 签收订单
# print('签收订单')
response = client.sendAndRecv(
'POST',
'/api/distributor/orders',
headers={'Authorization':sales_jwt},
json={
"action": "wf_order",
"wf_action": "wf_confirm_receive",
"data": {
"id": orderid,
"comment": "收到"
}
}
)
ret = response.json('utf8')
if ret["retcode"] != 0:
print('签收订单失败')
exit()
takes = time.time()-time1
print(f'一个订单耗时{takes} 秒')
调用API插入的方式可能耗时很长(为什么?自己思考一下)。
比如上面的代码,在普通个人电脑上执行,创建完数据,要耗时 1小时左右.
如果有更大量的测试数据要导入,就会花费更多时间.
实际项目中,调用API插入数据慢到不可接受.
所以往往 我们采用的方法是: 通过SQL语句直接往数据库插入数据.
这种方式,会快很多.
当然, 前提是 写程序的人 需要 充分了解产品的数据库定义.
还是上面的用例,同样的数据,用下面的代码,导入MySQL数据库 ,只需不到1分钟,即可完成数据导入. 比用API插入的程序快 几十倍!!
python
# 定义 创建 客户、代理商、药品的数量
NUM_CUSTOMER = 3000
NUM_SALES = 2000
NUM_MEDICINE = 1000
# 每个代理商创建多少订单
ORDERS_PER_SALES = 25
# 客户id范围,用户表开始有3条数据
ID_CUSTOMER = [4, 4 + NUM_CUSTOMER - 1]
# 代理商id范围
ID_SALES = [4 + NUM_CUSTOMER, 4 + NUM_CUSTOMER + NUM_SALES - 1]
# 药品id范围,药品表开始有1条数据
ID_MEDICINE = [1, 1 + NUM_MEDICINE - 1]
from random import randint
import MySQLdb
# 创建一个 Connection 对象,代表了一个数据库连接
conn = MySQLdb.connect(
host="192.168.1.100",# 数据库IP地址
user="user2", # mysql用户名
passwd="Mima123$", # mysql用户登录密码
db="bycrm" , # 数据库名
charset = "utf8")
c = conn.cursor()
# 添加客户
print('添加客户')
tplt = '''INSERT INTO `by_user` (`password`, `last_login`, `is_superuser`, `username`, `first_name`, `last_name`, `email`, `is_staff`, `is_active`, `date_joined`, `usertype`, `realname`, `desc`, `phone`, `avatar_url`) VALUES
('md5$eaLJpard8wB4$136393387ec65b9e4a4cc5e4bb46ada9', NULL, 0, 'hospital_%s', '', '', '', 1, 1, '2018-10-07 13:26:16.355364', 3000, '测试医院%s', '测试医院%s', NULL, NULL) ; '''
for i in range(NUM_CUSTOMER):
sql = tplt % (i, i, i)
c.execute(sql)
conn.commit()
# 添加代理商
print('添加代理商')
tplt = '''INSERT INTO `by_user` (`password`, `last_login`, `is_superuser`, `username`, `first_name`, `last_name`, `email`, `is_staff`, `is_active`, `date_joined`, `usertype`, `realname`, `desc`, `phone`, `avatar_url`) VALUES
('md5$eaLJpard8wB4$136393387ec65b9e4a4cc5e4bb46ada9', NULL, 0, 'sales_%s', '', '', '', 1, 1, '2018-10-07 13:26:16.355364', 4000, '测试代理商%s', '测试代理商%s', NULL, NULL) ; '''
for i in range(NUM_SALES):
sql = tplt % (i, i, i)
c.execute(sql)
conn.commit()
# 添加药品
print('添加药品')
tplt = '''INSERT INTO `by_medicine` ( `name`, `sn`, `desc`) VALUES
( '青霉素_%s', 'snb000001', '青霉素_%s'); '''
for i in range(NUM_MEDICINE):
sql = tplt % (i, i)
c.execute(sql)
conn.commit()
# 添加订单
print('添加订单')
tplt1 = '''
INSERT INTO `by_order` ( `name`, `create_date`, `desc`, `creator_id`, `customer_id`, `workflow_ver`, `workflow_cur_statename`, `workflow_cur_statecode`, `fee`,`medicinelist`, `workflow_rec`) VALUES
('$订单名$', '2019-10-13 16:33:33.140735', '$订单名$', $代理商id$, $客户id$, '1.0', '订单完成', 's3', $金额$,
'[{"id": $药品id$, "v": "200", "name": "测试药品"}]',
'{"steps": [{"statecode": "init", "statename": "创建", "time": "2019-10-13 16:33:33", "action": "wf_submit_order", "actionname": "提交订单", "actor": $代理商id$, "actorname": "$代理商姓名$"}, {"statecode": "s1", "statename": "审核", "time": "2019-10-13 16:33:33", "action": "wf_approve_order", "actionname": "批准订单", "actor": 1, "actorname": "白月黑羽", "comment": ""}, {"statecode": "s5", "statename": "发货", "time": "2019-10-13 16:33:33", "action": "wf_confirm_receive", "actionname": "确认收货", "actor": $代理商id$, "actorname": "$代理商姓名$", "comment": ""}, {"statecode": "s3"}]}')
'''
# 订单id,因为数据库中已经有一条记录,所以从2开始
orderid = 2
medicine_order_recs = []
for i in range(NUM_SALES):
salesname = f'sales_{i}' # 代理商登录名
salesrealname = f'测试代理商_{i}' # 代理商真实姓名
sales_uid = ID_SALES[0] + i # 代理商id
print(salesrealname)
for j in range(ORDERS_PER_SALES):
# 随机挑选 客户 和 药品, 作为订单的采购内容
customerid = randint(ID_CUSTOMER[0], ID_CUSTOMER[1])
medicineid = randint(ID_MEDICINE[0], ID_MEDICINE[1])
sql = tplt1.replace('$订单名$', f'测试订单_{salesrealname}_{j}') \
.replace('$代理商id$', f'{sales_uid}') \
.replace('$客户id$', f'{customerid}') \
.replace('$药品id$', f'{medicineid}') \
.replace('$药品名称$', f'{medicineid}') \
.replace('$代理商姓名$', f'{salesrealname}') \
.replace('$金额$', f'{randint(50000, 90000)}.00')
# 订单表插入记录
c.execute(sql)
medicine_order_recs.append((medicineid, orderid))
orderid += 1
conn.commit()
# by_order_medicine 表插入记录
tplt2 = '''INSERT INTO `by_order_medicine` ( `amount`, `medicine_id`, `order_id`) VALUES
( 2000, '%s', '%s'); '''
print('by_order_medicine 表插入记录 ')
for rec in medicine_order_recs:
sql = tplt2 % rec
c.execute(sql)
conn.commit()
conn.close()
print('=== 完成 ====')
但是这种 方式 有一定风险,要确保你的插入数据格式 和 当前发布的产品 插入的 数据 格式 一致.
因为,你要测试的新版本,有可能数据格式已经有了改变,你必须做出相应的调整.
执行测试步骤
测试过程:
使用 hyload 模拟高峰期 代理商登录查询场景.
- 单个 代理商 的 模拟操作如下:
-
从登录网页 登录网站
-
查询自己的所有订单(25条,分5页展示)
每隔30秒,点击下一页订单,到底后,每隔30秒,点击前一页订单
这样循环2次
预估 单个代理商操作总时长 (30*4*2)*2 = 480秒 (8分钟)
- 整个 性能场景如下:
-
每秒2个代理商登录, 直到 2000 人全部登录操作完毕. 预计耗时 1000秒
-
每个代理商各自进行上述流程的操作.
整个测试预计耗时 1000+480=1480秒.
具体代码实现
python
# 使用 hyload 模拟高峰期 代理商登录查询场景.
# 1. 单个 代理商 的 模拟操作如下:
# - 从登录网页 登录网站
# - 查询自己的所有订单(25条,分5页展示)
# 每隔30秒,点击下一页订单,到底后,每隔30秒,点击前一页订单
# 这样循环2次
# 预估 单个代理商操作总时长 (30*4*2)*2 = 480秒 (8分钟)
# 2. 整个 性能场景如下:
# - 每秒2个代理商登录, 直到 2000 人全部登录操作完毕. 预计耗时 1000秒
# - 每个代理商各自进行上述流程的操作.
# 整个测试预计耗时 1000+480=1480秒.
from hyload import *
from urllib.parse import quote
from base64 import b64encode
Stats.start()
def list_order(client, jwt, pagenum):
try:
response = client.get(
f'http://192.168.64.4:81/api/distributor/orders?action=list&pagenum={pagenum}&pagesize=5&keywords=&_=1752825761202',
headers={
'authorization': jwt
},
)
except:
Stats.one_error(f"list_order超时了, 响应时长为: {response.response_time}")
try:
ret = response.json()
except:
Stats.one_error("distributor login error")
if ret['retcode'] != 0:
Stats.one_error("distributor login ret['retcode'] != 0")
return response
def perf_test(sale_index):
client = HttpClient(timeout=10)
response = client.get(
'http://192.168.64.4:81/distributor/index.html',
)
response = client.get(
'http://192.168.64.4:81/distributor/sign.html',
)
username = f'sales_{sale_index}'
print(f'代理商 {username} 登录')
password = '88888888'
up = b64encode(quote(username+'#$%'+password).encode())
response = client.post(
'http://192.168.64.4:81/api/distributor/signin',
data={
'up': up
},
)
try:
ret = response.json()
except:
Stats.one_error(f"{username} distributor login error")
if ret['retcode'] != 0:
Stats.one_error("distributor login ret['retcode'] != 0")
jwt = response.headers['jwt']
response = client.get(
'http://192.168.64.4:81/wm.css',
)
response = client.get(
'http://192.168.64.4:81/distributor/index.js?1583f75bc81de0123b0a',
)
for i in range(2):
list_order(client, jwt, 1)
sleep(30)
list_order(client, jwt, 2)
sleep(30)
list_order(client, jwt, 3)
sleep(30)
list_order(client, jwt, 4)
sleep(30)
list_order(client, jwt, 5)
sleep(30)
list_order(client, jwt, 4)
sleep(30)
list_order(client, jwt, 3)
sleep(30)
list_order(client, jwt, 2)
sleep(30)
# emulate 500 user's behavior of the same type
for i in range(500):
# run user behavior function in hyload task (a greenlet)
run_task(perf_test, i)
sleep(0.5)
# wait for all hyload tasks to end
wait_for_tasks_done()