最近某个 Web 项目处于开发阶段,要求设计支持上万用户在线的场景。因此需要对应用整体进行性能评估,以便确认应用整体的可容纳在线用户数,因此有性能测试的需要。
经过评估,我们的应用架构是典型的 SPA + 后端 + 数据库应用,因此决定分开评测各自的性能,以便发现各自的性能问题,最终再集成整体评测。
因此我们分为了三个不同类型的性能测试:
本文我们分别总结这三个不同场景的性能测试。
我们测试的目的是为了发现我们的 SQL 是否会因为数据量大的情况下导致性能下降,因此我们需要先准备好大量的测试数据(填充数据库即可,无需在乎数据质量),然后大规模运行应用程序中可能用到的 SQL,以便发现数据库设计是否有缺陷,SQL 是否有性能问题等等。
我们使用的数据库为 PostgreSQL,而 PostgreSQL 官方本身就有个性能测试工具pgbench
可以实现这一需求。我们直接使用它即可。
pgbench
是 PostgreSQL 官方的性能测试工具,它自带有一组脚本,可以默认创建一个数据库并填充自己的数据表,然后运行自己的 SQL 脚本跑测试。显然默认的脚本和配置只能评测数据库服务器本身的性能,并不能发现我们的数据库设计和应用使用的 SQL 的性能隐患,因此我们需要定制 pgbench 的脚本。
我们需要创建应用自己的数据库(可以借助 db migration 工具完成),然后填充大量的测试数据,最好有一定的随机性。我们自己写个init.sql
,大体内容如下:
-- 这里我们用generate_series函数生成大量的记录行
-- coln_expr可以使用一些诸如random()函数产生一些随机性
INSERT INTO my_table (
col1,
col2,
col3,...
coln
)
SELECT col1_expr, col2_expr, col3_expr, ..., coln_expr
FROM generate_series(1, 1000000);
我们生成了 100 万条随机记录,可以使用命令psql -f ./init.sql db
向目标数据库插入这 100 万条记录,作为数据准备。
然后我们需要准备性能测试过程运行的 SQL 语句,再写一个benchmark.sql
:
-- 注意整个benchmark要求必须包裹在整个事务中,因此必须使用BEGIN-END包裹
-- \set是pgbench扩展,可以用于设置一些变量,方便构造随机参数
-- pgbench 可选函数和命令参考官方文档: https://www.postgresql.org/docs/current/pgbench.html
\set var1_random random(0, 2)
\set var2_random random(0, 5)
\set num random(1, 100000 * :scale)
\set var3_random random(0, 2)
\set var4_random hash(:num)
BEGIN;
SELECT col1, col2 from my_table WHERE col1 = :var1 AND col2 = :var2;
UPDATE my_table SET col3 = :var3 WHERE col4 = :var4;
UPDATE my_table SET col2 = :var2 WHERE some_expr;
INSERT INTO my_table (
col1, col2, col3, col4, ... , coln
)
VALUES (...);
END;
我们的应用主要使用这几种 SQL 语句,构造好参数之后就可以运行了:
pgbench -r -j2 -c10 -T120 -n -f ./benchmark.sql db
我们用 2 线程运行,每个线程运行 10 个客户端,总共运行 2 分钟(120 秒),最终得到的结果大体如下:
pgbench (14.5 (Ubuntu 14.5-0ubuntu0.22.04.1))
transaction type: ./benchmark.sql
scaling factor: 1
query mode: simple
number of clients: 10
number of threads: 2
duration: 600 s
number of transactions actually processed: 6426
latency average = 934.322 ms
initial connection time = 171.960 ms
tps = 10.702950 (without initial connection time)
statement latencies in milliseconds:
4.767 BEGIN;
160.203 SELECT xxx,
176.959 UPDATE xxx
188.223 UPDATE xx
188.389 UPDATE xxx
187.733 UPDATE xx
18.776 INSERT INTO xxx (
9.352 END;
我们可以清楚的看到每个 SQL 大体上的延迟是多少,就能发现是否存在慢查询以及需要优化的部分了。
最初我们发现 INSERT 语句延迟较低,而其他 SELECT 和 UPDATE 延迟非常高,因此推断 RDS 实例性能较低,于是升级 RDS 实例规格,并且存储改为 SSD 存储实例之后,性能显著提升,问题得到解决。
在数据库的性能测试完成之后,我们可以将数据准备部分复用,直接给后端性能测试的准备数据阶段使用。
后端性能测试过程中,我们需要模拟大量用户访问后端 API 的情况,以便发现后端 API 处理过程中的性能问题。
特别注意如果后端有调用第三方接口的情况,需要在测试前进行屏蔽相关功能,或者进行 Mock 接口,防止压力测试中对第三方接口请求量太大从而耗尽配额或者被 ban。
这里我们选用了 artillery 这个开源工具跑压力测试,因为它使用比较简单,而且未来会有云服务,方便跑分布式压测场景。不过当前版本已经支持运行在 AWS Lambda,我们可以利用这个特性跑简单的分布式压力测试场景。
我们需要编写好 artillery 的 yaml 配置文件,这里建议参考官方文档,有大量的范例可供参考: https://github.com/artilleryio/artillery/tree/main/examples
我们在 lambda 上运行,启动 40 个 lambda 实例跑测试:
artillery run --platform aws:lambda --platform-opt region=us-west-1 --count=40 -o result.json config.yml
最终输出的报告大体如下:
--------------------------------
Summary report @ 10:50:26(+0800)
--------------------------------
http.codes.200: ................................................................ 37927
http.codes.201: ................................................................ 60978
http.codes.400: ................................................................ 23046
http.codes.502: ................................................................ 2
http.request_rate: ............................................................. 144/sec
http.requests: ................................................................. 121953
http.response_time:
min: ......................................................................... 4
max: ......................................................................... 11839
median: ...................................................................... 1085.9
p95: ......................................................................... 5944.6
p99: ......................................................................... 7407.5
http.responses: ................................................................ 121953
plugins.metrics-by-endpoint.GET /endpoint1/:argument.codes.200: ................ 30441
plugins.metrics-by-endpoint.POST /endpoint1 (offchain).codes.201: .............. 30590
plugins.metrics-by-endpoint.POST /endpoint1 (offchain).codes.502: .............. 1
plugins.metrics-by-endpoint.POST /endpoint1 (onchain).codes.201: ............... 30388
plugins.metrics-by-endpoint.POST /endpoint1 (onchain).codes.502: ............... 1
plugins.metrics-by-endpoint.POST /endpoint2.codes.200: ......................... 7486
plugins.metrics-by-endpoint.POST /endpoint2.codes.400: ......................... 23046
plugins.metrics-by-endpoint.response_time.GET /endpoint1/:targetToken:
min: ......................................................................... 5
max: ......................................................................... 5966
median: ...................................................................... 645.6
p95: ......................................................................... 2018.7
p99: ......................................................................... 3464.1
plugins.metrics-by-endpoint.response_time.POST /endpoint1 (offchain):
min: ......................................................................... 59
max: ......................................................................... 10607
median: ...................................................................... 2725
p95: ......................................................................... 6064.7
p99: ......................................................................... 7260.8
plugins.metrics-by-endpoint.response_time.POST /endpoint1 (onchain):
min: ......................................................................... 68
max: ......................................................................... 11839
median: ...................................................................... 3464.1
p95: ......................................................................... 7117
p99: ......................................................................... 8024.5
plugins.metrics-by-endpoint.response_time.POST /endpoint2:
min: ......................................................................... 4
max: ......................................................................... 10146
median: ...................................................................... 391.6
p95: ......................................................................... 3752.7
p99: ......................................................................... 5944.6
vusers.completed: .............................................................. 121953
vusers.created: ................................................................ 121953
vusers.created_by_name.GET /endpoint1/:targetToken: ............................ 30441
vusers.created_by_name.POST /endpoint1 (offchain): ............................. 30591
vusers.created_by_name.POST /endpoint1 (onchain): .............................. 30389
vusers.created_by_name.POST /endpoint2: ........................................ 30532
vusers.failed: ................................................................. 0
vusers.session_length:
min: ......................................................................... 11
max: ......................................................................... 11847.7
median: ...................................................................... 1176.4
p95: ......................................................................... 5944.6
p99: ......................................................................... 7407.5
vusers.skipped: ................................................................ 35146
Log file: result.json
Estimated AWS Lambda cost for this test: $0.1833
通过测试结果,我们发现了某些 endpoint 接口运行较慢,经过进一步排查最终定位到性能下降的原因进行优化即可。
最初我们通过测试报告发现,不请求数据库的 API 性能很高,反之如果 API 有数据库的调用,则压测之后延迟非常高,甚至还有超时错误。最终排查之后发现是数据库连接池的类库性能较差,更换新的数据库 driver,并且优化部分 SQL 之后性能问题得到很大的改善。
前端性能测试主要包含用户的加载页面速度和页面渲染的速度两方面,我们可以通过配合 lighthouse 和 artillery + playwright 实现。
lighthouse 可以重点帮助分析页面渲染方面的最佳实践,而 artillery + playwright 的组合可以方便的模拟大量用户反复点击某些页面之后,页面加载速度的情况,可以更准确的得到页面相关的 FCP 等重要参数。
特别注意 playwright 运行浏览器的时候,如果需要加载浏览器插件,无法通过访问在线商店的方式进行安装,必须将插件下载到本地之后使用 playwright API进行加载。
--------------------------------
Summary report @ 17:24:38(+0000)
--------------------------------
browser.http_requests: ......................................................... 4392
browser.page.CLS.e2e test:
min: ......................................................................... 0
max: ......................................................................... 0
median: ...................................................................... 0
p95: ......................................................................... 0
p99: ......................................................................... 0
browser.page.FCP.e2e test:
min: ......................................................................... 311.1
max: ......................................................................... 1307.7
median: ...................................................................... 415.8
p95: ......................................................................... 925.4
p99: ......................................................................... 1249.1
browser.page.FID.e2e test:
min: ......................................................................... 1.7
max: ......................................................................... 22.2
median: ...................................................................... 2.5
p95: ......................................................................... 9.5
p99: ......................................................................... 12.6
browser.page.LCP.e2e test:
min: ......................................................................... 726.6
max: ......................................................................... 2226.1
median: ...................................................................... 788.5
p95: ......................................................................... 2018.7
p99: ......................................................................... 2143.5
browser.page.TTFB.e2e test:
min: ......................................................................... 35.5
max: ......................................................................... 494
median: ...................................................................... 48.9
p95: ......................................................................... 125.2
p99: ......................................................................... 478.3
vusers.completed: .............................................................. 28
vusers.created: ................................................................ 36
vusers.created_by_name.e2e test: ............................................... 36
vusers.failed: ................................................................. 8
vusers.session_length:
min: ......................................................................... 3314.8
max: ......................................................................... 6396.5
median: ...................................................................... 3534.1
p95: ......................................................................... 5826.9
p99: ......................................................................... 6187.2
vusers.skipped: ................................................................ 204
Log file: result.json
特别注意 playwright 当前无法在 lambda 环境下运行
本文我们大体总结了一个 Web 应用在性能测试方面的套路,以便指导发现应用的性能问题,从而优化应用本身。
觉得有帮助的话,不妨考虑购买付费文章来支持我们 🙂 :
付费文章