如何压测?2 个案例带你入门压测!

1,333 阅读10分钟

压测是什么

压测,即压力测试,是确立系统稳定性的一种测试方法,通常在系统正常运作范围之外进行,以考察其功能极限和隐患

常见需要压测的场景

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();
    }
}

代码说明:

  1. @BenchmarkMode(Mode.AverageTime): 测试平均执行时间
  2. @OutputTimeUnit(TimeUnit.NANOSECONDS): 输出结果使用纳秒为单位
  3. @State(Scope.Thread): 每个测试线程一个实例
  4. @Fork(1): 进行 1 次 Fork
  5. @Warmup(iterations = 5): 预热 5 次
  6. @Measurement(iterations = 10): 测试 10 次

测试了三种场景:

  1. directMethodCall(): 直接方法调用,这是最快的
  2. reflectionWithoutCache(): 每次都重新获取 Method 对象的反射调用,这是最慢的
  3. reflectionWithCache(): 使用缓存的 Method 对象进行反射调用,性能介于两者之间

预期结果

执行时间从小到大排序:directMethodCall、reflectionWithCache、reflectionWithoutCache

执行压测

  1. mvn clean package 打包
  2. 在服务器中,使用 java -jar xxx.jar 运行
  3. 将生成的 result.json 上传到 jmh.morethan.io/,可视化结果

结果分析

与预期结果相同。其中,reflectionWithoutCache 耗时是 directMethodCall 的 8 倍,reflectionWithCache 耗时是 directMethodCall 的 38 倍。差距好像挺大?但对性能真的有很严重的影响吗?我们需要去避免使用反射吗?让我们来分析一下:

  • 反射对性能的影响实际是可忽略不计的。倍数相差确实比较大,但耗时还是纳秒级别的。假设要调用 1000 个反射方法,也不过才 19ms,要是使用带缓存的反射调用,那只需要 4ms。对用户而言,是没法感知这点时间带来的延迟的。与其关注反射带来微不足道的性能损耗,不如优化一下慢 SQL、降低方法的复杂度、提高下缓存的命中率等等。优化这些功能很容易就可以减少几百毫秒的 RT
  • 反射带来的收益远大于因性能损耗所带来的影响。反射是框架的灵魂。dubbo 的远程方法调用、Spring 的 IoC 和 AOP、jackson 的序列化等等,都是依赖反射实现的。反射可以运行时操作类,从而实现直接调用方法无法做到的功能

结论

反射对性能有影响,但不多,放心用就好

不过尽可能使用带缓存的反射调用,不仅可以减少一些调用时间,还因为共享了同一个 Method 实例,降低内存开销

本次测试的局限性

  • 研究「反射对性能的影响」,但只做了反射方法的性能测试。可以进一步测试反射创建对象、反射修改类的属性、动态代理等反射操作对性能的影响
  • 方法调用的内容非常简单,可以考虑方法在不同复杂度的情况下,测试非反射调用和反射调用的性能差距

参考资料

github.com/link1st/go-…

juejin.cn/post/729938…

cn.dubbo.apache.org/zh-cn/docsv…