背景 最近某个 Web 项目处于开发阶段,要求设计支持上万用户在线的场景。因此需要对应用整体进行性能评估,以便确认应用整体的可容纳在线用户数,因此有性能测试的需要。
经过评估,我们的应用架构是典型的 SPA (Vue) + 后端 + 数据库应用,因此决定分开评测各自的性能,以便发现各自的性能问题,最终再集成整体评测。
因此我们分为了三个不同类型的性能测试:
数据库(PostgreSQL)性能测试 
后端(Nest.js)性能测试 
前端(Vue+Vite)性能测试 
 
本文分别总结这三个不同场景的性能测试。
PS: 由于后端部署的环境是 AWS Lightsail,因此我们使用 AWS 提供的 RDS 作为数据库,国内云服务可能部分场景有不同,请考虑相关的替代品。
 
数据库性能测试 我们测试的目的是为了发现我们的 SQL 是否会因为数据量大的情况下导致性能下降,因此我们需要先准备好大量的测试数据(填充数据库即可,无需在乎数据质量),然后大规模运行应用程序中可能用到的 SQL,以便发现数据库设计是否有缺陷,SQL 是否有性能问题等等。
我们使用的数据库为 PostgreSQL,而 PostgreSQL 官方本身就有个性能测试工具pgbench可以实现这一需求。我们直接使用它即可。
pgbench是 PostgreSQL 官方的性能测试工具,它自带有一组脚本,可以默认创建一个数据库并填充自己的数据表,然后运行自己的 SQL 脚本跑测试。显然默认的脚本和配置只能评测数据库服务器本身的性能,并不能发现我们的数据库设计和应用使用的 SQL 的性能隐患,因此我们需要定制 pgbench 的脚本。
我们需要创建应用自己的数据库(借助 prisma migration 工具完成),然后填充大量的测试数据,最好有一定的随机性。我们自己写个seed.sql,大体内容如下:
1 2 3 4 5 6 7 8 9 10 INSERT  INTO  my_table (    col1,     col2,     col3,...     coln ) SELECT  col1_expr, col2_expr, col3_expr, ..., coln_exprFROM  generate_series(1 , 1000000 );
 
我们生成了 100 万条随机记录,可以使用命令psql -f ./seed.sql db向目标数据库插入这 100 万条记录,作为数据准备。
然后我们需要准备性能测试过程运行的 SQL 语句,再写一个benchmark.sql:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \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 语句,构造好参数之后就可以运行了:
1 pgbench - r - j2 - c10 - T120 - n - f ./ benchmark.sql db 
 
我们用 2 线程运行,每个线程运行 10 个客户端,总共运行 2 分钟(120 秒),最终得到的结果大体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pgbench (14.5  (Ubuntu 14.5 -0 ubuntu0.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  mstps =  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 存储实例之后,性能显著提升,问题得到解决。
PS: 大部分情况下,升级配置是很好的性能优化手段:)
后端性能测试 在数据库的性能测试完成之后,我们可以将数据准备部分复用,直接给后端性能测试的准备数据阶段使用。
后端性能测试过程中,我们需要模拟大量用户访问后端 API 的情况,以便发现后端 API 处理过程中的性能问题。
特别注意如果后端有调用第三方接口的情况,需要在测试前进行屏蔽相关功能,或者进行 Mock 接口,防止压力测试中对第三方接口请求量太大从而耗尽配额或者被 ban。
这里我们选用了 artillery 这个开源工具跑压力测试,因为它使用比较简单,而且未来会有云服务,方便跑分布式压测场景。不过当前版本已经支持运行在 AWS Lambda,我们可以利用这个特性跑简单的分布式压力测试场景。
我们需要编写好 artillery 的 yaml 配置文件,这里建议参考官方文档,有大量的范例可供参考: https://github.com/artilleryio/artillery/tree/main/examples
我们在 lambda 上运行,启动 40 个 lambda 实例跑测试:
1 artillery run --platform aws:lambda --platform-opt region=us-west-1 --count=40 -o result.json config.yml 
 
得到结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 -------------------------------- Summary report @ 11: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进行加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 -------------------------------- 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 应用在性能测试方面的套路,以便指导发现应用的性能问题,从而优化应用本身。