Skip to content
发布于 更新于

压力测试 ​

这篇笔记主要记录两件事:

  1. 如何通过 Arthas trace 快速判断接口耗时落在哪一层;
  2. 如何更稳妥地给业务接口设定压测目标,而不是直接拍一个 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() #115

1.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() #148

1.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 以内”这种说法本身不算错,但要加前提:

  • 是命中本地缓存还是回源查询。
  • 是否需要多维度拼装。
  • 是否同步调用了外部风控服务。

五、如何给接口设压测目标 ​

推荐按下面这个顺序设目标,而不是直接拍脑袋给数字:

  1. 先根据业务峰值估算目标流量。
  2. 再拆分到接口层,区分读接口和写接口。
  3. 明确缓存命中率、下游调用数、数据库负载。
  4. 先做单接口压测,再做链路压测。
  5. 最后验证资源余量、熔断阈值和降级策略。

六、这篇笔记里哪些结论是合理的 ​

下面这些判断我认为是合理的:

  • trace 很适合快速看耗时分布。
  • 一般对象访问不是主要瓶颈。
  • 数据库、RPC、远程依赖通常是主要耗时来源。
  • 风控查询接口通常会借助多级缓存降低时延。

下面这些表述原来不够严谨,我已经调整成了更稳妥的说法:

  • “某类接口单机 TPS 一般是多少”。 这个必须带前提,否则很容易误导。
  • “订单查询 1 秒内超时”。 更适合改成 SLO / RT 分位值目标,而不是口语化描述。
  • “某接口 RT 一定在多少毫秒以内”。 应该区分缓存命中、缓存未命中、回源、下游依赖等场景。

七、总结 ​

压力测试不是为了得到一个漂亮的 TPS 数字,而是为了回答三个更关键的问题:

  • 系统真正的瓶颈在哪里。
  • 到达峰值时会先坏在哪一层。
  • 出现抖动后有没有足够的缓冲、降级和恢复能力。

如果只是看平均 RT 或单次 trace 输出,很容易低估真实风险;真正有效的压测结论,必须结合链路结构、数据规模、资源使用率和业务目标一起看。

基于 VitePress + GitHub Actions 自动部署