压测是什么
压测,即压力测试,是确立系统稳定性的一种测试方法,通常在系统正常运作范围之外进行,以考察其功能极限和隐患
常见需要压测的场景
1.背景:老接口优化
目标:对比优化前优化后的性能指标,是否有优化效果
2.背景:新增接口,需要评估业务预估调用量
目标:极限压测,在保证服务正常服务的情况下,允许调用量的大小
3.背景:大促活动,峰值调用量相比日常调用量没有明显变化,但是高峰时间拉长的业务
目标:峰值流量稳定性压测。即拉长压测时间,确保在长时间处于峰值的情况下,服务正常可用
4.背景:不知道系统是否需要扩容
目标:极限压测,应用服务器和数据库资源使用情况是否合理
多少用户(Who)在什么时间或者持续多长多久(When),在多大的数据量的基础上(How much),完成了什么业务(What),最终需要关注怎样的指标(How)
压测的目的
- 量化具体的性能指标,验证是否满足业务需要
- 发现系统的性能短板,针对性地优化。重点关注吞吐量和时延
- 验证在长时间高并发和大数据量的环境下,是否能正常运行
压测名词解释
压测类型
| 压测类型 | 内容 | 评估范围 |
|---|---|---|
| 单场景/接口压测 | 评估单场景/接口当前是否能够支撑到目标压力 | 目标 QPS、预期响应时间、性能风险、压测并发数 |
| 全量压测 | 对业务全场景/接口进行并发压力测试,评估达到目标 QPS 时的压力情况 | 目标 QPS、预期响应时间、性能风险、容量规划 |
| 持续性压测 | 评估长时间持续压力下,服务是否稳定 | 预期响应时间、堆内存及 GC、99 线抖动 |
| 故障演练 | 对熔断、降级方案进行验证,并针对故障场景进行应急流程演练 | 熔断降级方案、应急流程 |
压测名词
| 压测名词 | 解释 |
|---|---|
| 并发 | 指一个处理器同时处理多个任务的能力(逻辑上处理的能力) |
| 并行 | 多个处理器同时处理多个不同的任务(物理上同时执行) |
| QPS(Queries Per Second) | 服务器每秒钟处理请求数量 |
| 事务 | 用户一次或者是几次请求的集合 |
| TPS(Transaction Per Second) | 每秒钟处理事务数量。是吞吐量(Throughput)的一种具体表现 |
| 错误率 | 在压测中,请求成功的数量与请求失败数量的比率 |
| RT(Response Time) | 响应时间。发送一次请求到接收到响应所需要的时间 |
| P99(99th Percentile) | 99% 的请求响应时间都小于或等于某个特定值,而 1% 的请求响应时间大于该值。同理还有 P95/P90。不过在高并发的情况下,P99 会更重要。即使只是 1% 的请求响应时间大于该值,但在用户数和调用链足够大的情况下,带来的性能影响也将不断放大 |
机器性能指标
| 机器性能 | 解释 |
|---|---|
| CPU 使用率 | CPU 使用率高,意味着系统处理请求的能力达到了瓶颈 |
| 内存使用率 | 过高的内存使用率可能导致频繁触发 swap,导致性能下降 |
| 磁盘 IO | 在数据库密集型应用中,磁盘瓶颈可能导致显著的性能下降 |
| 网络带宽 | 如果带宽过低,可能导致请求的延迟增加或丢包 |
访问指标
| 访问 | 解释 |
|---|---|
| PV(Page Views) | 页面浏览量。1 个用户访问 n 次相同网站,就加 n 次 PV |
| UV(Unique Visitors) | 网站独立访客。1 个用户访问 n 次相同网站,只加 1 次 UV |
计算压测指标
压测我们需要有目的性的压测,这次压测我们需要达到什么目标(如:单台机器的性能为 100 QPS ?网站能同时满足 100W 人同时在线)
压测原则:每天 80% 的访问量集中在 20%的时间里,这 20% 的时间就叫做峰值
预期峰值 QPS = ( 总 PV 数*80% ) / ( 每天的秒数*20% )
需要的机器的数量 = 峰值时间每秒钟请求数(QPS) / 单台机器的 QPS
案例
假设:网站每天的用户数(100W),每天的用户的访问量约为 3000W PV,这台机器的需要多少 QPS ?
答:(30000000*0.8) / (86400 * 0.2) ≈ 1389 (QPS)
假设:单台机器的的 QPS 是 69,需要需要多少台机器来支撑?
答:1389 / 69 ≈ 20
压测工具
-
ab:轻量级命令行工具,适合简单的 HTTP 性能测试
-
jmeter:带有图形化界面的压测工具。功能强大,支持多种协议
-
jmh:专注于 Java 应用的微基准测试,适合代码层面的性能优化
-
云压测:利用云平台(腾讯云、阿里云)的分布式资源进行大规模的压力测试。但云压测服务是收费的
如何选择?
-
简单的 HTTP 压测:ab
-
精确度高的 Java 方法性能测试:jmh
-
需要功能强大的 Web 压测工具:jmeter
-
模拟更真实的线上环境:云压测
案例 1——量化接口优化后的数值
压测背景
接口优化后,量化优化后相比优化前,具体提升的数值
压测环境
在客户端使用 JMeter 对服务器的 Springboot 服务进行压测
服务器配置:
- CPU: 2核
- 内存: 2GB
- 系统: Ubuntu 22.04 LTS
- 磁盘: SSD 40GB
网络环境:
- 网络延迟: 16ms
- 峰值带宽: 2Mbit/s
软件版本:
- JMeter: 5.5
- JDK版本: JDK 17
- SpringBoot: 3.0.7
压测对象
这里使用 sleep 简单模拟一下业务处理耗时,假设优化后的耗时预估减少 1000ms
// 假设这是优化前的接口,业务需要约 2000ms
@GetMapping("/hello1")
public String hello1() throws InterruptedException {
// 2000ms ± 200ms
int sleepTime = 2000 + ThreadLocalRandom.current().nextInt(401) - 200;
Thread.sleep(sleepTime);
return "Hello, World1!";
}
// 假设这是优化后的接口,业务需要约 1000ms
@GetMapping("/hello2")
public String hello2() throws InterruptedException {
// 1000ms ± 200ms
int sleepTime = 1000 + ThreadLocalRandom.current().nextInt(401) - 200;
Thread.sleep(sleepTime);
return "Hello, World2!";
}
预期结果
-
吞吐量提升 100%
-
平均响应时间、P99/P95/P90 响应时间、最大响应时间降低 50%
-
资源使用率(CPU、内存、网络等)平稳
-
JVM 内存运行稳定,无 OOM,堆内存中无不合理的大对象的占用
-
业务线程负载均衡
执行压测
如下图所示,设置:
- Number of Threads:模拟有 20 个用户线程数
- Ramp-up:表示在 20 秒内将用户线程数启动完毕(预热)
- Loop Count:每个用户线程执行 50 次
总计采样 20 * 50 = 1000 次
结果分析
实际压测结果与预期结果基本一致。性能提升接近一倍
本次测试的局限性
本次压测主要是通过简单的案例,介绍如何进行压测
存在压测时间短、场景简单、压测数据量不够大等问题,实际压测需要更多额外的因素。如不同的并发请求用户数、请求携带的数据量大小、服务端线程池配置等等
案例 2——反射对性能的影响
压测背景
我们都知道反射是要比正常的调用性能要差的,但实际会损耗多少性能?这个损耗的性能是否可以接受?
就让我们来压测一下看看吧
压测环境
因为我们要对 Java 的方法的进行性能测试,所以选择 JMH 进行压测
服务器配置:
- CPU: 2核
- 内存: 2GB
- 系统: Ubuntu 22.04 LTS
- 磁盘: SSD 40GB
软件版本:
- JMH: 1.37
- JDK版本: JDK 17
压测对象
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
public class ReflectionPerformanceTest {
private TestClass instance;
private Method method;
@Setup
public void setup() throws NoSuchMethodException {
instance = new TestClass();
// 预先获取Method对象
method = TestClass.class.getDeclaredMethod("getMessage", String.class);
}
// 测试直接方法调用
@Benchmark
public String directMethodCall() {
return instance.getMessage("Hello");
}
// 测试每次都重新获取Method对象的反射调用
@Benchmark
public String reflectionWithoutCache() throws Exception {
Method method = TestClass.class.getDeclaredMethod("getMessage", String.class);
return (String) method.invoke(instance, "Hello");
}
// 测试使用缓存Method对象的反射调用
@Benchmark
public String reflectionWithCache() throws Exception {
return (String) method.invoke(instance, "Hello");
}
// 被测试的类
public static class TestClass {
public String getMessage(String msg) {
return msg;
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ReflectionPerformanceTest.class.getSimpleName())
.resultFormat(ResultFormatType.JSON)
.result("result.json") // 输出文件名为 result.json
.build();
new Runner(opt).run();
}
}
代码说明:
@BenchmarkMode(Mode.AverageTime): 测试平均执行时间@OutputTimeUnit(TimeUnit.NANOSECONDS): 输出结果使用纳秒为单位@State(Scope.Thread): 每个测试线程一个实例@Fork(1): 进行 1 次 Fork@Warmup(iterations = 5): 预热 5 次@Measurement(iterations = 10): 测试 10 次
测试了三种场景:
directMethodCall(): 直接方法调用,这是最快的reflectionWithoutCache(): 每次都重新获取 Method 对象的反射调用,这是最慢的reflectionWithCache(): 使用缓存的 Method 对象进行反射调用,性能介于两者之间
预期结果
执行时间从小到大排序:directMethodCall、reflectionWithCache、reflectionWithoutCache
执行压测
- mvn clean package 打包
- 在服务器中,使用 java -jar xxx.jar 运行
- 将生成的 result.json 上传到 jmh.morethan.io/,可视化结果
结果分析
与预期结果相同。其中,reflectionWithoutCache 耗时是 directMethodCall 的 8 倍,reflectionWithCache 耗时是 directMethodCall 的 38 倍。差距好像挺大?但对性能真的有很严重的影响吗?我们需要去避免使用反射吗?让我们来分析一下:
- 反射对性能的影响实际是可忽略不计的。倍数相差确实比较大,但耗时还是纳秒级别的。假设要调用 1000 个反射方法,也不过才 19ms,要是使用带缓存的反射调用,那只需要 4ms。对用户而言,是没法感知这点时间带来的延迟的。与其关注反射带来微不足道的性能损耗,不如优化一下慢 SQL、降低方法的复杂度、提高下缓存的命中率等等。优化这些功能很容易就可以减少几百毫秒的 RT
- 反射带来的收益远大于因性能损耗所带来的影响。反射是框架的灵魂。dubbo 的远程方法调用、Spring 的 IoC 和 AOP、jackson 的序列化等等,都是依赖反射实现的。反射可以运行时操作类,从而实现直接调用方法无法做到的功能
结论
反射对性能有影响,但不多,放心用就好
不过尽可能使用带缓存的反射调用,不仅可以减少一些调用时间,还因为共享了同一个 Method 实例,降低内存开销
本次测试的局限性
- 研究「反射对性能的影响」,但只做了反射方法的性能测试。可以进一步测试反射创建对象、反射修改类的属性、动态代理等反射操作对性能的影响
- 方法调用的内容非常简单,可以考虑方法在不同复杂度的情况下,测试非反射调用和反射调用的性能差距