高并发系统稳定性:压测理论学习及代码实践
1. 参考资料
迈向及格线的开发必备的JMeter性能测试总结1:基础概念与环境搭建
Prometheus + Grafana 监控,验证 Hystrix 超时熔断
2. 内容设问
- 什么是压力测试&性能测试?
- 压力测试和性能测试的关系是什么?
- 性能测试包含哪些类型?
- 压力测试的目的是什么?
- 性能测试核心指标类型有哪些?
- 性能测试核心指标的关系图?
- 计算压测指标举例
- 何时进行压力测试
- 压测的一般步骤
- 思考:如何确定系统需要进行扩容?
- 思考:反射对性能的影响?
- 如何知道线上的QPS?【基于grafana和Prometheus的例子】
- 进行压测的方式?
3. 正文
1. 什么是压力测试&性能测试?
- 概念说明
压力测试(Stress Testing) 针对系统或者组件,为要确认其稳定性而特意进行的测试。测试系统的极限承受能力,通过将系统暴露于超负荷或极端条件下,观察系统在异常情况(如超负载、高并发、大数据量)下的表现。
性能测试(Performance Testing) 是测试系统在正常负载下的响应速度、吞吐量和资源利用率等,评估其性能 瓶颈。
- 性能测试 & 压力测试的关系
① 性能测试的压力测试的基础
性能测试通常是渐进式的,通过模拟正常的业务负载来逐步增加负载,从而评估系统性能。是对系统常规负载下的性能进行评估, 根据性能测试的结果可以进行压力测试,看超出负载后会出现哪些瓶颈。
② 压力测试是对性能测试的延申和补充
性能测试关注的是系统在正常使用情况下的表现, 而压力测试则将系统置于极限负载下,验证系统是否能承受更高的并发量、大流量或者长时间运行等异常情况。
- 性能测试类型包括哪些类型
基准测试:单用户理想环境下的性能基准
负载测试:逐步增加并发用户直至预期最大值
压力测试:超过系统设计容量的极端测试(如双十一流量3倍的模拟)
尖峰测试:瞬间流量暴涨测试(1秒内从100QPS到10000QPS)
耐久测试:持续72小时以上的长时间稳定性测试
2. 压力测试的目的是什么?
① 找出系统的承载极限:找出性能短板,针对性优化,估计系统的性能上限
测试系统能承受的最大负载、并发量和数据量,找出系统的性能瓶颈或崩溃点。主要关注的是吞吐量和时延。
② 测试系统的稳定性和韧性:负载增加,各性能指标的变化情况是否有异常
验证系统在超负荷或极端情况下的表现,是否能够稳定运行,或者在出现异常时能否平稳恢复。
③ 测试系统的容错机制:对熔断、限流等高并发策略进行测试,判断系统在高并发下是否会报错,进程是否会挂掉
确保系统能在出现错误或崩溃时,正确地进行容错和恢复。(比如对熔断机制、限流、降低等的测试)
3. 压测核心性能指标有哪些?
- 核心指标:
响应时间RT、吞吐量、资源利用率、并发用户数UV、错误率,主要指标分类如下。
- 系统能力指标:并行和并发
指标 | 解释 | 健康阈值 |
---|---|---|
并发 | 处理器逻辑上同时处理多个任务的能力。 | 通常 50-100 并发线程,具体值依硬件和系统优化能力而定。 |
并行 | 多个处理器物理上同时处理多个不同的任务。 | 根据处理器数量和硬件资源,通常为 CPU 核心数。 |
- 吞吐量类指标:QPS、TPS、事务
指标 | 解释 | 健康阈值 |
---|---|---|
QPS (Queries Per Second) | 每秒处理请求的数量。 | 根据系统容量,常见阈值:500-5000 QPS。超出此值需要扩展。 |
事务 | 用户一次或几次请求的集合。 | 通常与 QPS 相似,具体值依据负载量和事务大小。 |
TPS (Transaction Per Second) | 每秒处理事务的数量。 | 视系统和应用而定,健康范围大致为 200-1000 TPS。 |
- 响应时间指标:RT、P99
指标 | 类型 | 解释 | 健康阈值 |
---|---|---|---|
RT (Response Time) | 响应时间类指标 | 请求到响应所需的时间。 | 通常 < 500ms:延迟低于此值表示良好,过高则影响用户体验。 |
P99 (99th Percentile) | 响应时间类指标 | 99%的请求响应时间小于某个值,主要用于评估高并发时的尾部延迟。 | 一般 < 2秒:P99 延迟大于 2 秒时说明系统尾部请求性能问题。 |
- 可靠性指标:错误率指标
指标 | 解释 |
---|---|
错误率 | 请求成功与请求失败的比率。 |
- 机器性能指标:用于了解系统的运行状态,识别性能瓶颈,优化资源分配,确保系统在高负载下仍能稳定运行。
机器性能 | 解释 | 健康阈值 |
---|---|---|
CPU 使用率 | CPU 使用率高,意味着系统处理请求的能力达到了瓶颈。高 CPU 使用率会导致系统响应延迟。 | 不超过 80%-85% :如果持续高于该值,可能导致系统负载过重、响应延迟增大,甚至崩溃。 |
内存使用率 | 过高的内存使用率可能导致频繁触发 swap,导致性能下降,甚至出现系统崩溃。 | 不超过 75%-80% :过高的内存占用可能导致性能下降。 如果使用 swap 操作,性能会严重影响。 |
磁盘 IO | 在数据库密集型应用中,磁盘瓶颈可能导致显著的性能下降。频繁的磁盘读取/写入会影响整体响应时间。 | 磁盘带宽占用不超过 70%-80% :如果持续接近 100%,可能会导致磁盘 I/O 阻塞,从而影响数据库性能。 |
网络带宽 | 如果带宽过低,可能导致请求的延迟增加或丢包,影响数据的传输速度和服务响应。 | 不超过 70%-80% :超出该值会导致带宽瓶颈,影响请求响应速度和稳定性。 需要定期监控并进行扩展。 |
- 访问指标:用于衡量网站的流量和活跃度。
同时,分析 PV/UV 比(即每个独立访客的页面浏览量),可以帮助判断是否存在重复访问的趋势,以及是否需要增强内容吸引力或优化用户体验。
除此之外,还有DAU(Daily Active User),日活跃用户数量;MAU(Month Active User),月活跃用户数量。
指标 | 解释 | 健康阈值 |
---|---|---|
PV (Page Views) | 页面浏览量,指某页面被访问的总次数。一个用户访问同一页面多次,会多次计算。 | 根据具体的业务需求、网站的规模和流量预期而定。通常,每日 PV 数量要在百万级别或更高,特别是在高流量的网站中。 |
UV (Unique Visitors) | 独立访客,指在一定时间内访问网站的独立用户。一个用户多次访问只计算为一个 UV。 | 根据行业平均水平和预期流量设置,常见的阈值是每天访问量在几万到几十万 UV。 具体值视业务目标而定。 |
4. 性能测试指标关系图
四种性能指标: 响应时间(RT)、并发用户量(UV)、吞吐量(Throughput)和资源利用率(Utilization)。
图片记忆【源于link:迈向及格线的开发必备的JMeter性能测试总结1:基础概念与环境搭建 】 :2333:即两个点、三条线、三个区、三个状态。
① 三条线: 吞吐量、响应时间、利用率
② 两个点: 最优并发用户数(并发情况能够实现最佳性能的用户数)、最大并发用户数(能够处理的最大负载量下,系统可以不崩溃或者发生严重错误的最大并发数)
③ 三个区:轻负载、重负载、塌陷区(无法处理请求、 RT急剧增加、资源利用率过高)
④ 三个状态: 资源饱和、吞吐下降、用户受影响
5. 计算压测指标举例
压测我们需要有目的性的压测,这次压测我们需要达到什么目标
(如:单台机器的性能为 100 QPS ?网站能同时满足 100W 人同时在线。此次指标为吞吐量指标和访客指标)
压测原则:每天 80% 的访问量集中在 20%的时间里,这 20% 的时间就叫做峰值时段, 这个时间也就是系统负载最重的时候。
基于帕累托原则得出结论,认为大多数情况下,80%的结果或者影响来自于20%的原因或者输入。
因此可以计算预期峰值QPS:
每天秒数通常为86400 秒(24 小时)
预期峰值 QPS = 总 PV 数 × 80 % 每天的秒数 × 20 % \text{预期峰值 QPS}=\frac{\text{总 PV 数}\times80\%}{\text{每天的秒数}\times20\%} 预期峰值 QPS=每天的秒数×20%总 PV 数×80%
需要的机器的数量 = 峰值时间每秒钟请求数(QPS) / 单台机器的 QPS
示例:
网站每天的用户数(100W),每天的用户的访问量约为 3000W PV,这台机器的需要多少 QPS ?
答:(30000000*0.8) / (86400 * 0.2) ≈ 139 (QPS)
计算所需机器支撑: 单台机器的的 QPS 是 58,需要需要多少台机器来支撑?
答:139 / 58 ≈ 3.
6. 何时进行压力测试
① 旧的接口优化:对比优化前后的性能指标,是否有优化效果,来充分应对不断增长的用户量。
② 新接口需要评估业务预估调用量:常规的操作是上线前对系统进行一个摸高压测。根据预估的流量,对系统配置进行优化调整,保证运行期间,系统能正常运行。
7. 压力测试的一般步骤?
① 需求分析:明确需量化的指标
明确定义性能指标,比如RT、QPS、CPU占有率和UV等。
② 基于性能指标定义到实际的性能场景以及测试数据的量级
根据业务需求和系统负载预期,设计与实际业务场景相符的测试用例。
性能场景如单一业务场景(只测试登录等单个功能的性能)和混合场景(测试用户同时进行多个操作的功能)。
③ 根据不同的场景编写脚本
根据单一场景或者业务场景等编写测试的脚本。
④ 执行脚本:开始压测,关注关键指标的变化
⑤ 性能诊断: 内存、CPU、磁盘 I/O、网络 I/O 等
8.思考:如何确定系统需要扩容的时机?
(一)技术指标触发扩容
① 资源利用率持续高位
关注CPU、内存、磁盘I/O、网络带宽等指标,超过健康阈值
② 服务性能瓶颈
响应时间变慢、错误率上升、队列堆积等
③ 容量限制接近阈值
数据库连接数、存储空间剩余不足等
(二) 业务需求触发扩容
① 用户增长与流量趋势
日活用户(DAU)、请求量(QPS/TPS)持久上升,超出当前系统承载能力;
业务计划上线新功能、 营销活动(如双11、秒杀)或进入新市场,预期流量激增。
② 用户体验下降
-
用户投诉增加(如页面加载慢、支付失败)。
-
关键业务指标(如转化率、留存率)因性能问题下降。
③ 业务连续性要求
-
系统需满足高可用性(如SLA 99.99%),当前架构存在单点故障风险。
-
灾备需求(如跨地域部署、冗余扩容)。
(三) 总结扩容决策流程
监控报警触发 → 分析具体瓶颈(是CPU、网络还是代码问题?)
评估业务影响 → 是否影响核心功能或用户体验?
选择扩容方案 → 临时扩容(云实例+自动伸缩)还是长期扩容(采购硬件)?
实施与验证 → 扩容后持续监控,确保问题解决且无副作用
9. 思考:反射对性能的影响
① 反射操作对性能的影响
- 直接调用 vs 反射调用(Java基准测试)
反射调用耗时可能比直接调用高 100倍以上,尤其在循环或高频调用时更为显著。
// 直接调用
obj.method(); // 平均耗时 1 ns
// 反射调用
Method method = obj.getClass().getMethod("method");
method.invoke(obj); // 平均耗时 100-1000 ns
- 字段访问
反射访问字段的速度通常比直接访问慢 10-50倍。
② 性能瓶颈来源
- 类型解析开销
元数据查找:反射需要在运行时动态解析类、方法或者字段的元数据,比编译时静态绑定慢。
安全检查: 反射回调用验证访问权限,增加额外开销。
- 方法调用与字段访问
间接调用:反射调用方法(如Method.invoke()
)或访问字段时,需通过JVM内部机制,无法享受编译器优化(如内联)。
装箱/拆箱:若反射操作涉及基本类型(如int
),可能触发频繁的装箱拆箱(如转为Integer
),影响性能。
- 缓存缺失:反复解析
未缓存反射对象(如Method
、Field
)时,重复解析同一元数据会导致冗余开销。
③ 优化策略:核心在于减少运行时解析次数和绕过安全检查。
- 缓存反射对象:避免重复解析元数据
// 缓存Method对象
private static final Method CACHED_METHOD;
static {
try {
CACHED_METHOD = MyClass.class.getMethod("method");
CACHED_METHOD.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
- 绕过安全检查:减少对私有成员访问时调用权限验证
Field field = MyClass.class.getDeclaredField("privateField");
field.setAccessible(true); // 仅需一次设置
- 在以下场景避免对反射的使用
高频调用的核心逻辑(如游戏循环、算法核心)。
对延迟敏感的系统(如金融交易、实时控制)。
10. 如何知道线上的QPS?
① 为什么要监控QPS?
QPS 是衡量后端服务性能的关键指标,可以帮助你及时发现性能瓶颈和问题。
② 所需要的工具:
Prometheus:开源监控框架,用于收集和存储时序数据;
Grafana:开源的可视化工具,用于创建和共享交互式仪表盘,展示Prometheus等监控系统收集的数据;
③ 如何可视化QPS的数据?
在 Grafana 中创建一个仪表盘,添加一个图形面板,显示 my_app_requests_total{method=“GET”} 度量标准,并且可以添加红线标记阈值(Thresholds)。
④ 代码实践
参考链接:Prometheus + Grafana 监控,验证 Hystrix 超时熔断
【注意点】
① 在根pom.xml文件中添加Actuator和Prometheus的依赖以及Hytsrix依赖
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>1.5.18</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
② 用docker容器化技术在服务器部署Prometheus和Grafana
注意: 两者以及启动应用程序需要在同一个网络,此处自定义network。
并且,在项目的docs目录下Prometheus和grafana目录下对应文件顺序不变,要根docker-compose文件容器的volumns相对应。
version: '3' # 指定 docker-compose 文件的版本为 3,适用于大多数场景
networks:
app-net:
driver: bridge
# 服务定义部分
services:
# Prometheus 服务:用于数据采集
prometheus:
image: bitnami/prometheus:2.47.2 # 使用 bitnami/prometheus 镜像,版本为 2.47.2
container_name: prometheus # 容器名称为 prometheus
restart: always # 容器停止时自动重启
networks:
- app-net
ports:
- 9090:9090 # 将宿主机的 9090 端口映射到容器的 9090 端口,方便访问 Prometheus 服务
volumes:
- ./etc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml # 将本地配置文件映射到容器内的配置文件,定制 Prometheus 配置
# Grafana 服务:用于展示 Prometheus 收集的数据
grafana:
image: grafana/grafana:10.2.0 # 使用 grafana 镜像,版本为 10.2.0
container_name: grafana # 容器名称为 grafana
restart: always # 容器停止时自动重启
networks:
- app-net
ports:
- "4000:4000" # 添加 Grafana 端口映射
depends_on:
- prometheus # 依赖于 Prometheus 服务,确保 Prometheus 在 Grafana 启动之前启动
volumes:
- ./etc/grafana/provisioning:/etc/grafana # 将本地配置文件映射到容器内的配置文件,用于定制 Grafana 配置
③ grafana相关定义
a/ 用docker部署时,数据源的url的地址是prometheus容器的名称 + 端口号;
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus:9090
access: proxy
isDefault: true
b/ 对dashboard定义的json文件
datasource要修改为自己在grafana中创建的文件的uid:通过connections中打开对应datasource,其url中 …/edit/xxx,edit后面的内容就是uid。
expr: 其job名称要跟prometheus中定义的一致。
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "bf0bc282-d985-449b-bc12-b9bd4a41677a"
},
"disableTextWrap": false,
"editorMode": "code",
"expr": "tomcat_connections_config_max_connections{job=\"dev-test\"}",
"fullMetaSearch": false,
"includeNullMetadata": true,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "A",
"useBackend": false
}
],
④ 对Prometheus的抓取地址
Prometheus.yml文件中,targets的地址:
如果在服务器部署了对应的容器,则需要在服务器部署对应的app;
如果在本地用docker部署的容器,targets则通过cmd指令ipconfig查看本地的ipv4地址进行替换。
但是服务器部署容器不能访问本地的springboot应用地址, 因为服务器属于外网,无法访问本机内网。(使用cpolar进行内网穿透会出现targets格式问题)
global:
scrape_interval: 15s # 全局配置:抓取数据的间隔为15s
scrape_configs:
- job_name: 'dev-test'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['hystrix-test-app:8091']
11. 进行压测的方式
① 简单方式: 通过APIPost一键压测。
② 专业方式: JMeter进行压测