压力测试
这篇笔记主要记录两件事:
- 如何通过
Arthas trace快速判断接口耗时落在哪一层;- 如何更稳妥地给业务接口设定压测目标,而不是直接拍一个 TPS 数字。
核心结论
- 压测之前,先区分接口是 CPU 密集、IO 密集,还是缓存命中型接口。
- 从实际排查经验看,普通
getter/setter、对象转换、小规模集合处理通常不是主要瓶颈。 - 真正拉高 RT 的,往往是数据库、远程 RPC、缓存未命中后的回源链路。
TPS/QPS只能在明确机器规格、数据量、依赖拓扑、缓存命中率之后才有参考意义。- 文中给出的数值只适合作为经验起点,不能直接当作系统容量上限。
一、用 Arthas trace 看接口耗时
1.1 Controller 层示例
text
[arthas@82]$ trace com.test.web.controller.OrderUniteController queryOrderPage -n 5 --skipJDKMethod false
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 655 ms, listenerId: 1
`---ts=2024-01-22 11:02:30;thread_name=http-nio-9166-exec-6;id=122;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@5ea0f263
`---[197.718153ms] com.test.web.controller.OrderUniteController:queryOrderPage()
+---[0.16% 0.321685ms ] org.slf4j.Logger:info() #83
+---[0.01% 0.014182ms ] com.test.web.request.OrderUnitePageDTO:getOrderType() #84
+---[0.01% 0.010145ms ] com.test.common.to.PaginationRequest:<init>() #101
+---[0.02% 0.043364ms ] com.test.web.mapstruct.OrderUniteMapStruct:pageDtoToRequest() #102
+---[8.46% 16.728599ms ] com.test.web.controller.OrderUniteController:setDataAuthByOrderType() #104
+---[0.01% 0.011294ms ] com.test.common.to.PaginationRequest:setConditions() #107
+---[0.00% 0.008099ms ] com.test.web.request.OrderUnitePageDTO:getPageSize() #108
+---[0.00% 0.008459ms ] com.test.common.to.PaginationRequest:setPageSize() #108
+---[0.00% 0.009668ms ] com.test.web.request.OrderUnitePageDTO:getPageIndex() #109
+---[0.00% 0.007329ms ] com.test.common.to.PaginationRequest:setPageNum() #109
+---[87.44% 172.885494ms ] com.test.web.client.OrderUniteClient:queryOrderPage() #110
+---[0.04% 0.070558ms ] com.test.web.mapstruct.OrderUniteMapStruct:dtoFaceToPageVO() #112
`---[3.14% 6.209232ms ] com.test.web.controller.OrderUniteController:doVoucherPageSumAmt() #1151.2 Service / Mapper 层示例
text
Affect(class count: 2 , method count: 2) cost in 374 ms, listenerId: 7
`---ts=2024-01-22 11:46:05;thread_name=DubboServerHandler-10.253.68.210:9165-thread-56;id=9150;is_daemon=true;priority=5;TCCL=org.springframework.boot.loader.LaunchedURLClassLoader@3c8a7e38
`---[26.0524ms] com.test.core.accounting.impl.TestServiceImpl:queryTestPage()
+---[0.13% 0.034457ms ] com.test.facade.api.request.accounting.TestPageRequest:getPage() #136
+---[0.03% 0.007267ms ] com.test.facade.api.request.accounting.TestPageRequest:getRows() #136
+---[0.04% 0.010735ms ] com.baomidou.mybatisplus.extension.plugins.pagination.Page:<init>() #136
+---[1.94% 0.50524ms ] com.test.core.accounting.impl.TestServiceImpl:buildWrapper() #137
+---[79.70% 20.763484ms ] com.test.repository.mapper.voucher.TestMapper:selectPage() #137
+---[0.05% 0.012289ms ] com.baomidou.mybatisplus.extension.plugins.pagination.Page:getRecords() #137
+---[0.04% 0.010785ms ] com.test.common.to.PaginationResponse:<init>() #138
+---[0.02% 0.005444ms ] org.apache.commons.collections4.CollectionUtils:isEmpty() #141
+---[0.11% 0.028614ms ] java.util.Comparator:comparing() #145
+---[0.10% 0.026304ms ] java.util.Comparator:thenComparingInt() #145
+---[0.37% 0.096225ms ] java.util.List:sort() #145
+---[0.72% 0.187958ms ] com.test.repository.mapStruct.TestMapStruct:entityListToDtoList() #147
+---[0.03% 0.008859ms ] com.test.common.to.PaginationResponse:setList() #147
+---[0.03% 0.006987ms ] com.baomidou.mybatisplus.extension.plugins.pagination.Page:getTotal() #148
`---[0.03% 0.007384ms ] com.test.common.to.PaginationResponse:setTotal() #1481.3 从 trace 可以得到什么判断
| 现象 | 含义 | 结论 |
|---|---|---|
getter/setter 只有几微秒甚至更短 | JVM 内部对象访问成本很低 | 一般不是性能瓶颈 |
小规模 List.sort() 有少量耗时 | 集合处理有成本,但通常不是主因 | 除非数据量大,否则优先级不高 |
| Mapper / Client / RPC 占比极高 | 耗时集中在 IO 或远程调用 | 优先查数据库、索引、网络、下游服务 |
1.4 这里要注意的边界
trace只能说明一次调用链中的时间分布,不能直接替代系统级压测结论。- 单次耗时低,不代表高并发下一定稳定,还要结合线程池、连接池、锁竞争一起看。
- 统计比例高的节点,只说明它在当前请求里最慢,不代表它一定是唯一问题。
二、压测时重点关注哪些指标
2.1 常见指标
| 指标 | 含义 | 建议关注点 |
|---|---|---|
| QPS | 每秒请求数 | 更适合查询型接口 |
| TPS | 每秒事务数 | 更适合支付、下单、扣减库存等事务接口 |
| RT | 响应时间 | 至少看平均值、P95、P99 |
| 错误率 | 失败请求占比 | 要区分业务失败和系统失败 |
| CPU / 内存 | 机器资源使用情况 | 看是否接近瓶颈 |
| 线程池 / 连接池 | 服务内部容量 | 看是否出现排队和耗尽 |
2.2 压测目标不要脱离上下文
在讨论“这个接口能扛多少 TPS”之前,至少要先明确下面几个前提:
- 单机还是集群。
- 机器规格和 JVM 参数。
- 数据量级、索引情况、冷热数据比例。
- 是否命中本地缓存、Redis、数据库。
- 是否串行调用多个下游,是否有远程 RPC。
- 是否涉及事务、锁、幂等校验、消息发送。
三、不同类型接口的经验判断
3.1 支付类接口
支付接口通常具备以下特点:
- 强事务性。
- 写操作多。
- 对一致性要求高。
- 往往依赖库存、账户、风控、消息等多个系统。
更合理的判断方式:
- 核心支付不是单看 TPS,而是看“稳定成功率 + RT 分位值 + 下游是否抖动”。
- 取消订单通常比支付正向链路更轻,但也要看是否涉及库存回补、优惠回滚、消息补偿。
- 查询接口通常吞吐更高,但要看是否走缓存、是否带复杂聚合。
经验上,支付类接口的容量评估一定要保守,因为它最怕的不是慢,而是慢的时候引发超时、重试、重复支付和级联故障。
3.2 交易类接口
交易链路常见操作包括:
- 创建订单
- 取消订单
- 订单查询
这类接口的差别通常很大:
| 接口 | 常见瓶颈 | 压测重点 |
|---|---|---|
| 创建订单 | 库存、价格、营销、写库、消息 | 成功率、锁竞争、事务耗时 |
| 取消订单 | 状态校验、库存回滚、补偿逻辑 | 幂等、回滚时延 |
| 订单查询 | 数据聚合、RPC 扇出、分页 | 缓存命中率、慢 SQL、P99 |
“订单查询 1 秒内超时”这种说法不够准确。更合理的表达应该是:
- 先约定接口的 SLO,例如
P95 < 200ms、P99 < 500ms。 - 再根据页面场景区分同步查询接口和后台批量查询接口。
四、风控类接口
风控接口更常见的是查询型场景,例如:
- 黑名单查询
- 设备画像查询
- 灰产识别
- 规则命中判断
这类接口的关键不只是快,还要稳定:
- 本地缓存命中时,要足够快。
- 缓存未命中时,回源链路也不能雪崩。
- 风控结果通常处在主交易链路上,所以抖动会直接放大到支付或下单接口。
4.1 一个更合理的目标表达方式
| 接口类型 | 常见目标 | 备注 |
|---|---|---|
| 本地缓存命中 | RT 通常要求很低 | 目标可以控制在几十毫秒内 |
| Redis 命中 | RT 稳定即可 | 要看网络和序列化成本 |
| 回源数据库 / 远程规则引擎 | RT 可能明显上升 | 要配合降级、兜底、超时控制 |
所以“黑名单查询 RT 在 100ms 以内”这种说法本身不算错,但要加前提:
- 是命中本地缓存还是回源查询。
- 是否需要多维度拼装。
- 是否同步调用了外部风控服务。
五、如何给接口设压测目标
推荐按下面这个顺序设目标,而不是直接拍脑袋给数字:
- 先根据业务峰值估算目标流量。
- 再拆分到接口层,区分读接口和写接口。
- 明确缓存命中率、下游调用数、数据库负载。
- 先做单接口压测,再做链路压测。
- 最后验证资源余量、熔断阈值和降级策略。
六、这篇笔记里哪些结论是合理的
下面这些判断我认为是合理的:
trace很适合快速看耗时分布。- 一般对象访问不是主要瓶颈。
- 数据库、RPC、远程依赖通常是主要耗时来源。
- 风控查询接口通常会借助多级缓存降低时延。
下面这些表述原来不够严谨,我已经调整成了更稳妥的说法:
- “某类接口单机 TPS 一般是多少”。 这个必须带前提,否则很容易误导。
- “订单查询 1 秒内超时”。 更适合改成 SLO / RT 分位值目标,而不是口语化描述。
- “某接口 RT 一定在多少毫秒以内”。 应该区分缓存命中、缓存未命中、回源、下游依赖等场景。
七、总结
压力测试不是为了得到一个漂亮的 TPS 数字,而是为了回答三个更关键的问题:
- 系统真正的瓶颈在哪里。
- 到达峰值时会先坏在哪一层。
- 出现抖动后有没有足够的缓冲、降级和恢复能力。
如果只是看平均 RT 或单次 trace 输出,很容易低估真实风险;真正有效的压测结论,必须结合链路结构、数据规模、资源使用率和业务目标一起看。