添加基准测试 - 第一部分
学习如何添加基准测试来比较纯函数和非纯函数的实现。
设置
现在我们的实现已经就位,可以开始进行对比了。我们将首先实现一些基准测试,以评估两种实现的性能。
请注意,基准测试与负载测试不同。后者旨在将系统或应用推向极限,而前者则是为了在特定条件下收集性能统计数据。
有许多应用程序可用于执行负载测试和基准测试。对于基准测试,Apache JMeter 是一个不错的起点。
启动使用非常简单。文档中提到:“真正的工作应仅使用命令行应用程序,而用图形界面来创建和测试基准。”
我们将跳过对 JMeter 的冗长介绍和教程,因为其官方文档中已有大量相关内容,网上也有许多教程可供参考。
系统环境
服务端和测试软件(Apache JMeter)运行在通过 100 MBit/s 网络连接的不同工作站上。
服务端工作站
- CPU:Core i5-9600K,6 核,3.7 GHz
- 内存:32 GB
- 硬盘:2 块三星 SSD 860 PRO 512GB,SATA 接口
- 操作系统:FreeBSD 12(禁用超线程)
- JDK:11.0.4+11-2
- 数据库:PostgreSQL 11.3
客户端工作站
- CPU:AMD Ryzen Threadripper 2950X
- 内存:32 GB
- 硬盘:2 块三星 SSD 970 PRO 512GB,M.2 接口
- 操作系统:FreeBSD 12(禁用超线程)
- JDK:11.0.4+11-2
使用 Apache JMeter 版本 5.1.1 进行基准测试,每个测试使用 10 个线程,每轮测试的预热时间为 10 秒。
指标
我们将采用三次基准测试结果的平均值作为最终指标。以下为表格和图例中使用的缩写:
- AVG:响应时间的平均值(毫秒)
- MED:响应时间的中位数(毫秒)
- 90%:90% 的请求在指定毫秒数内完成响应
- 95%:95% 的请求在指定毫秒数内完成响应
- 99%:99% 的请求在指定毫秒数内完成响应
- MIN:最小响应时间(毫秒)
- MAX:最大响应时间(毫秒)
- ERR:错误率(百分比)
- R/S:每秒可处理的请求数
- MEM:服务在基准测试期间占用的最大内存(MB)
- LD:服务机器在测试期间的平均系统负载
创建 100,000 个产品
| 指标 | 不纯实现 | 纯实现 |
|---|---|---|
| AVG (ms) | 98 | 12 |
| MED (ms) | 95 | 11 |
| 90% (ms) | 129 | 15 |
| 95% (ms) | 143 | 18 |
| 99% (ms) | 172 | 30 |
| MIN (ms) | 53 | 5 |
| MAX (ms) | 1288 | 675 |
| ERR (%) | 0% | 0% |
| R/S (每秒请求数) | 100 | 765.33 |
| MEM (MB) | 1158 | 1308 |
| LD | 16 | 9 |
人们普遍认为不纯实现虽然不够干净,但速度一定更快。然而,实际情况似乎正好相反。
我们逐项分析一下。
最引人注目的是,纯实现的速度大约是不纯实现的七倍!不纯实现平均每秒处理约 100 个请求,而纯实现则达到每秒 765.33 个请求。
<chart config={{ "type": "bar", "data": { "labels": [ "R/S" ], "datasets": [ { "label": "纯实现", "yAxisID": "y-axis-1", "backgroundColor": "rgb(255, 99, 132)", "borderColor": "rgb(255, 99, 132)", "data": [ "765.33" ] }, { "label": "不纯实现", "yAxisID": "y-axis-1", "data": [ "99.9999999" ] } ] }, "options": { "responsive": true, "title": { "display": true, "text": "每秒请求数" }, "tooltips": { "mode": "index", "intersect": true }, "scales": { "yAxes": [ { "type": "linear", "display": true, "position": "left", "id": "y-axis-1" } ] } } }}>
此外,响应时间的各项指标也支持这一结论。在内存使用方面,纯实现比不纯实现多消耗约 13% 的内存。
<chart config={{ "type": "bar", "data": { "labels": [ "内存使用量" ], "datasets": [ { "label": "纯实现", "yAxisID": "y-axis-1", "backgroundColor": "rgb(255, 99, 132)", "borderColor": "rgb(255, 99, 132)", "data": [ "1308" ] }, { "label": "不纯实现", "yAxisID": "y-axis-1", "data": [ "1158" ] } ] }, "options": { "responsive": true, "title": { "display": true, "text": "内存使用量基准测试" }, "tooltips": { "mode": "index", "intersect": true }, "scales": { "yAxes": [ { "ticks": { "beginAtZero": true, "steps": 100 }, "type": "linear", "display": true, "position": "left", "id": "y-axis-1" } ] } } }}>
在当前内存成本低廉的时代,为获得显著的性能提升而多付出约 13% 的内存开销,是完全可以接受的。
另一个值得注意的点是,不纯实现的平均系统负载明显更高(接近两倍)。虽然合理利用资源没有问题,但较低的资源利用率能为其他任务(如操作系统、数据库等)留出更多“缓冲空间”。
加载 100,000 个产品
| 指标 | 不纯实现 | 纯实现 |
|---|---|---|
| AVG (ms) | 7 | 7 |
| MED (ms) | 8 | 7 |
| 90% (ms) | 10 | 9 |
| 95% (ms) | 11 | 10 |
| 99% (ms) | 14 | 19 |
| MIN (ms) | 4 | 2 |
| MAX (ms) | 347 | 118 |
| ERR (%) | 0% | 0% |
| R/S (每秒请求数) | 1162.20 | 1248.63 |
| MEM (MB) | 1449 | 1538 |
| LD | 13 | 8 |
加载基准测试的结果更为均衡。尽管纯实现仍略胜一筹(约快 7%),但其内存使用量比不纯实现高出约 6%。
<chart config={{ "type": "bar", "data": { "labels": [ "内存使用量" ], "datasets": [ { "label": "纯实现", "yAxisID": "y-axis-1", "backgroundColor": "rgb(255, 99, 132)", "borderColor": "rgb(255, 99, 132)", "data": [ "1538" ] }, { "label": "不纯实现", "yAxisID": "y-axis-1", "data": [ "1449" ] } ] }, "options": { "responsive": true, "title": { "display": true, "text": "内存使用量基准测试" }, "tooltips": { "mode": "index", "intersect": true }, "scales": { "yAxes": [ { "ticks": { "beginAtZero": true, "steps": 100 }, "type": "linear", "display": true, "position": "left", "id": "y-axis-1" } ] } } }}>
总体来看,两种实现的结果非常接近。但再次说明,纯实现在系统负载方面表现更优,与首次基准测试一致。
我们将在下一课中继续对剩余接口进行基准测试。