性能测试-软件测试
什么是性能测试?
这一点可以想象一下,大学一道选课的时候系统非常卡,网页打不开,刷新半小时。
那么性能测试就是:通过模拟多用户并发访问,对被测系统施加特定负载,监控其响应时间,吞吐量,资源利用率等指标,以验证系统是否在预期负载下稳定运行,并找出性能瓶颈的测试活动
核心机制
并发模拟
一台机器上开很多线程,同时申请向服务器发送请求,模拟真实场景下很多人同时操作
负载生成
按照预设的测试计划控制请求的发送频率,数量和节奏
指标采集
工具记录每个请求的响应时间,成功/失败状态,同时监控服务器的CPU、内存,磁盘,网络
瓶颈定位
当响应时间变长或者错误率上升时,结合服务器资源监控,判断是代码问题、数据库慢查询、内存泄漏还是网络带宽不足
三大指标
用户体验维度:用户直观感受到的快慢(响应时间)
1.基础定义
响应时间 = 网络传输时间 + 服务器处理时间 + 前端渲染时间,指从用户发起请求,到客户端完整收到并展示结果的总耗时
2.行业通用体验标准(C 端互联网产品)
-
2 秒以内:优秀,用户几乎无等待感知
-
2-5 秒:合格,用户有轻微等待感但可接受
-
5 秒以上:较差,用户流失风险显著上升
3.百分位数(P50 / P95 / P99)
不用平均时间是因为因为一些少数极端慢请求会拉高,掩盖大多数普通用户的真实体验
-
P50(中位数):50% 的请求耗时不超过该值,代表普通用户体验
-
P95:95% 的请求耗时不超过该值,大厂核心接口的核心考核指标
-
P99:99% 的请求耗时不超过该值,代表极端用户的体验上限
系统容量维度:衡量系统的承载能力
1.并发数:同一时间点,同时向系统发起有效请求的用户数量,是真正给服务器造成压力的用户
2.吞吐量
服务器资源维度:定位性能瓶颈的依据
-
CPU 使用率:服务器计算资源的占用比例,持续超过 70% 属于高风险区间
-
内存使用率:服务器内存占用比例,持续上涨不回落大概率是内存泄漏,超过 80% 有风险
-
磁盘 IO:磁盘的读写速度,数据库、大量日志写入的场景容易出现 IO 瓶颈
-
网络带宽:服务器出入口的流量大小,大文件上传、视频类场景容易出现带宽瓶颈
| 术语 | 大白话解释 | 含义 |
|---|---|---|
| 并发(Concurrency) | 同一时刻有多少人在"同时操作" | 同时在线操作的用户数 |
| TPS(Transactions Per Second) | 系统每秒能成功处理多少笔业务 | 收银员每秒能结几单账 |
| 响应时间(Response Time) | 从"你点按钮"到"页面出来"等了多久 | 从发请求到收到响应的总时间 |
| 吞吐量(Throughput) | 单位时间内系统能传输多少数据/处理多少请求 | 每秒处理的事务数/请求数 |
| 指标 | 英文 | 含义 | 通俗理解 |
|---|---|---|---|
| 响应时间 | Response Time | 从发请求到收到响应的总时间 | 你点完单到拿到奶茶等了多久 |
| 吞吐量 | Throughput (TPS/QPS) | 每秒处理的事务数/请求数 | 奶茶店一分钟能做多少杯 |
| 并发数 | Concurrency | 同时在线操作的用户数 | 店里同时站着多少顾客 |
| 错误率 | Error Rate | 失败请求占总请求的百分比 | 10杯里有1杯做失败了 |
| CPU利用率 | CPU Usage | 服务器CPU被占用的比例 | 员工有多忙 |
| 内存占用 | Memory Usage | 服务器内存使用情况 | 原料仓库用了多少空间 |
| 磁盘I/O | Disk I/O | 读写硬盘的频率和速度 | 记账本翻得多快 |
应关注的核心逻辑:随着并发数的增加,吞吐量应先线性上升,然后趋于平稳;响应时间应该抱持低位,直到某个临界点才陡增。如果响应时间提前飙升,吞吐量上不去,说明存在瓶颈
响应时间(Response Time)
从发送请求,到服务器处理完,数据传回来所需要的时间
标准:普通接口<0.5秒算优秀;>2秒用户开始烦躁;>5秒直接关闭页面
吞吐量(Throughput)
两个单位:
TPS:每秒处理的事务数
QPS::每秒查询数
标准:TPS越高,系统处理能力越强
并发数
两个概念:
在线用户数:挂着网页不操作
并发用户数:真实同时在操作
错误率
100个人下单,有3个人看到"系统繁忙,请稍后再试",错误率就是3%
标准:一般要求 < 0.1%,核心业务要求 0%
资源利用率(CPU/内存/磁盘/网络)
关注阈值:
CPU > 80%:危险,随时可能卡死
内存 > 85%:危险,可能开始杀进程
磁盘I/O 100%:读写堵死,数据库查询变慢
性能测试3中常见类型
负载测试
模拟真实用户正常并发量持续访问系统,观察系统在标准业务压力下的性能指标,验证系统能否稳定支撑日常业务流量
目的:验证系统在预期负载下的表现
简单一句话:测系统在正常、常规访问量下跑得稳不稳、速度快不快
压力测试
持续施加超出系统承载能力的负载,直到系统崩溃
目的:找到系统的崩溃临界点和恢复能力
稳定性测试
在预期负载下持续运行较长时间通常 8h~72h
目的:发现内存泄漏、连接池耗尽等慢性问题
性能测试流程
阶段1:需求分析与指标分析
核心工作:
1.明确测试范围
2.确定量化指标
3.确认业务模型,明确真实用户的行为比例
阶段2:测试计划制定
Jmeter 初步学习

右键 测试计划 ->添加 ->线程(用户)-> 线程组
设置:线程数=5(模拟5个虚拟用户) Ramp-up=5 (5个虚拟用户在5秒内陆续上线,相当于每秒启动1个) 循环次数:10(每个用户执行10次请求)
总请求量:5*10=50
添加一个HTTP请求:右键 线程组 → 添加 → 取样器 → HTTP请求
协议:https
服务器或者IP:httpbin.org(安全的对外公开测试站点)
端口号:443(https默认端口)
HTTP请求方法:GET
路径:/get
添加监听器,用来查看结果
没有监听器看不到任何结果

右键 线程组 → 添加 → 监听器 → 选择以下三个(多选添加):
查看结果数:查看每一次请求 详细请求/响应数据
集合报告:看平均响应时间,吞吐量等统计数据
图形结果(可选,更直观地看曲线)
保存并运行
压测(GET参数 + CSV参数化 + 响应断言)
为什么需要参数化?
测一个商品列表的接口,如果每一次请求page=1&size=12,服务器会会把结果缓存起来(Redis缓存)第二次请求直接返回缓存,测出来的性能是假的。真实场景下,用户会翻页,会换页大小,所以每一次请求的参数应该是不一样的。
参数化:让jemter每一次请求带不同的参数值,模仿真实用户行为
这里讲解一下为什么要参数化?
这个问题讲的性能测试里面最大的陷阱之一:缓存欺骗
这里我们举一个在京东查商品列表的例子
用第一性原理看,任何商品查询都需要经过三层:
手机——>API网关——>缓存层(Redis)——>数据库(MySQL)
在缓存层(Redis)中,如果缓存命中,则直接返回结果,不去查数据库,速度极快,通常几毫秒
如果缓存未命中,则去数据库查,耗时几十毫秒到几百毫秒,然后再把结果塞进缓存



2. 网关是什么?
网关 = 京东机房的"大门保安"。
你的手机发请求到京东,不是直接冲进数据库的。先到网关,网关做几件事:
-
检查你有没有登录(验证Token)
-
检查你是不是恶意攻击(限流、防刷)
-
把请求转发给正确的内部服务(搜索服务、订单服务、商品服务)
3. Redis是什么?
Redis = 京东机房的"速查笔记本"或"快递柜"。
-
特点:数据存在内存里,查起来极快(几毫秒)
-
缺点:断电就丢(所以只存临时数据)
-
作用:最近查过的结果先记在这里,下次有人查同样的内容,直接翻笔记本,不用去翻仓库账本
4. MySQL数据库是什么?
MySQL = 京东机房的"仓库总账本"。
-
特点:数据存在硬盘里,永久保存,查起来慢一些(几十到几百毫秒)
-
作用:所有商品的真实信息(名称、价格、库存、图片链接、详情页内容)都永久存在这里
-
关系:Redis是MySQL的"加速器",MySQL是Redis的"后备仓库"
所以:如果测试脚本每次发的参数都一致,服务器只会查一次数据库,后面全走缓存。那么实则测出来的是“缓存性能”而不是“用户真实的性能”
这里我们举一个场景:
你打开京东APP,搜索"手机",服务器返回第1页10条数据。
| 你的手指动作 | 实际发出去的请求参数 | 服务器怎么处理 | 是否走数据库 |
|---|---|---|---|
| 刚搜"手机",看第1页 | keyword=手机&page=1&size=10&sort=综合 | 第一次查,缓存里没有,去数据库查 | ✅ 走数据库 |
| 往下滑,看第2页 | keyword=手机&page=2&size=10&sort=综合 | 新参数组合,缓存里没有,去数据库查 | ✅ 走数据库 |
| 继续滑,看第3页 | keyword=手机&page=3&size=10&sort=综合 | 新参数组合,缓存里没有,去数据库查 | ✅ 走数据库 |
| 点"价格从低到高" | keyword=手机&page=1&size=10&sort=price_asc | 排序变了,缓存里没有,去数据库查 | ✅ 走数据库 |
| 筛选"华为+5G" | keyword=手机&brand=华为&network=5G&page=1&size=10 | 筛选条件变了,缓存里没有,去数据库查 | ✅ 走数据库 |
| 返回去看第1页(综合排序) | keyword=手机&page=1&size=10&sort=综合 | 这个参数之前查过,直接读缓存 | ❌ 不走数据库 |
多用户并发场景:100个人同时逛京东
| 用户 | 操作 | 请求参数 |
|---|---|---|
| 用户A | 搜"手机"看第1页 | keyword=手机&page=1&size=10 |
| 用户B | 搜"手机"看第2页 | keyword=手机&page=2&size=10 |
| 用户C | 搜"手机"按销量排序看第1页 | keyword=手机&page=1&size=10&sort=sales |
| 用户D | 搜"耳机"看第1页 | keyword=耳机&page=1&size=10 |
| 用户E | 搜"手机"筛选华为看第1页 | keyword=手机&brand=华为&page=1&size=10 |
| 用户F | 搜"手机"看第3页 | keyword=手机&page=3&size=10 |
| ... | ... | ... |
| 用户100 | 搜"充电宝"看第2页 | keyword=充电宝&page=2&size=10 |
| 时间 | 谁 | 搜了什么 | Redis有没有? | 走哪条路 | 耗时 |
|---|---|---|---|---|---|
| 09:00:01 | 用户A(北京) | keyword=手机&page=1 | ❌ 没有 | 查MySQL → 写入Redis | 80ms |
| 09:00:03 | 用户B(上海) | keyword=手机&page=1 | ✅ 有(用户A刚写的) | 直接读Redis | 3ms |
| 09:00:05 | 用户C(广州) | keyword=手机&page=1 | ✅ 有 | 直接读Redis | 3ms |
| 09:00:10 | 用户D(河北) | keyword=华为手机&page=1 | ❌ 没有(参数不同) | 查MySQL → 写入Redis | 80ms |
数据库连接池
连接池原理:不用 VS 用

没有连接池,每一次用户请求,都需要新建TCP连接,然后数据库认证,查数据,然后断开连接。
由此可以看到,查数据只有5ms,但是你建立连接和断开连接竟然还比查数据的时间还长?高并发时1000个请求同时来,要新建1000条连接,数据库直接崩。

所以,需要连接池的原因:
| 原因 | 大白话解释 | 后果 |
|---|---|---|
| 建连耗时 | TCP握手+数据库认证比查数据还慢 | 用户等半天,响应时间虚高 |
| 连接数上限 | MySQL本身有最大连接数限制(默认151) | 超了就直接拒绝连接,报错 |
| 资源浪费 | 频繁创建/销毁连接,CPU和内存吃不消 | 数据库服务器卡死,整个系统瘫痪 |
为什么需要断言?
状态码200:服务器已成功接收,理解并处理了请求,并且正常返回了响应体。只代表网络是通的,至于回应的内容对不对,业务操作有没有成功,HTTP状态码完全不关心
也就是说只表示【HTTP传输层面成功】,不代表【业务逻辑成功】
业务逻辑层:由响应体里自定义的业务状态码(通常是code字段)标识,管【业务操作有没有做对】,是业务层面的状态。这个时候就需要断言来告诉Jemter:"返回的 JSON 里必须包含 code: 0 和 list 字段,否则算失败。"
学习代码:
"""
========================================
性能测试待测服务 - PerfTestServer
========================================
用途:专门给JMeter压测练习用的本地Web服务
技术:Python Flask + SQLite(零依赖,开箱即用)
包含接口:
1. GET /api/health - 健康检查(最快,练手用)
2. POST /api/login - 用户登录(返回Token)
3. GET /api/products - 商品列表(分页查询,读数据库)
4. POST /api/order - 创建订单(需Token,写数据库)
5. GET /api/order/<id> - 查询订单(单条查询)
6. POST /api/slow-query - 慢查询(故意延迟,模拟瓶颈)
启动方式:
python perf_test_server.py
默认监听:http://127.0.0.1:5000
"""
from flask import Flask, request, jsonify
import sqlite3
import time
import uuid
import os
app = Flask(__name__)
# ==================== 数据库初始化 ====================
# 使用文件数据库,这样重启后数据还在
DB_PATH = os.path.join(os.path.dirname(__file__), "perf_test.db")
def get_db():
"""获取数据库连接"""
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
conn.row_factory = sqlite3.Row
return conn
def init_db():
"""初始化数据库:建表 + 插入测试数据"""
conn = get_db()
cursor = conn.cursor()
# 用户表
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
)
""")
# 商品表
cursor.execute("""
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER NOT NULL DEFAULT 100
)
""")
# 订单表
cursor.execute("""
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
total_price REAL NOT NULL,
create_time REAL NOT NULL
)
""")
# 插入测试用户(如果表为空)
cursor.execute("SELECT COUNT(*) FROM users")
if cursor.fetchone()[0] == 0:
test_users = [
("admin", "admin123"),
("user001", "pass001"),
("user002", "pass002"),
("user003", "pass003"),
("user004", "pass004"),
("user005", "pass005"),
]
cursor.executemany("INSERT INTO users (username, password) VALUES (?, ?)", test_users)
print("[DB] 已插入 6 个测试用户")
# 插入测试商品(如果表为空)
cursor.execute("SELECT COUNT(*) FROM products")
if cursor.fetchone()[0] == 0:
test_products = [
("iPhone 15", 5999.00, 50),
("MacBook Pro", 14999.00, 30),
("AirPods Pro", 1899.00, 100),
("iPad Air", 4799.00, 40),
("小米14", 3999.00, 80),
("华为Mate60", 6999.00, 60),
("机械键盘", 399.00, 200),
("显示器27寸", 1299.00, 45),
("鼠标", 199.00, 300),
("充电宝", 99.00, 500),
]
cursor.executemany("INSERT INTO products (name, price, stock) VALUES (?, ?, ?)", test_products)
print("[DB] 已插入 10 个测试商品")
conn.commit()
conn.close()
print(f"[DB] 数据库初始化完成,路径: {DB_PATH}")
# ==================== 接口定义 ====================
@app.route("/api/health", methods=["GET"])
def health():
"""
健康检查接口
用途:JMeter练手第一步,验证服务是否存活
特点:无数据库操作,响应极快
"""
return jsonify({
"code": 0,
"msg": "ok",
"data": {"status": "running", "timestamp": time.time()}
})
@app.route("/api/login", methods=["POST"])
def login():
"""
登录接口
请求体:{"username": "admin", "password": "admin123"}
响应:{"code": 0, "data": {"token": "xxx"}}
用途:
- 练习POST请求
- 获取Token供后续接口使用(JMeter参数关联)
"""
data = request.get_json(silent=True) or {}
username = data.get("username", "")
password = data.get("password", "")
if not username or not password:
return jsonify({"code": 400, "msg": "用户名或密码不能为空"}), 400
conn = get_db()
cursor = conn.cursor()
cursor.execute(
"SELECT id FROM users WHERE username = ? AND password = ?",
(username, password)
)
user = cursor.fetchone()
conn.close()
if user:
# 生成简单token(真实项目会用JWT,这里简化)
token = f"token_{username}_{uuid.uuid4().hex[:8]}"
return jsonify({
"code": 0,
"msg": "登录成功",
"data": {"token": token, "user_id": user[0]}
})
else:
return jsonify({"code": 401, "msg": "用户名或密码错误"}), 401
@app.route("/api/products", methods=["GET"])
def products():
"""
商品列表接口
查询参数:?page=1&size=10
响应:商品列表(分页)
用途:
- 练习GET请求带参数
- 练习分页参数化(JMeter CSV Data Set Config)
- 读数据库操作,可观察查询性能
"""
page = request.args.get("page", 1, type=int)
size = request.args.get("size", 10, type=int)
# 限制最大页大小,防止恶意请求
if size > 100:
size = 100
offset = (page - 1) * size
conn = get_db()
cursor = conn.cursor()
# 查询总数
cursor.execute("SELECT COUNT(*) FROM products")
total = cursor.fetchone()[0]
# 分页查询
cursor.execute(
"SELECT id, name, price, stock FROM products LIMIT ? OFFSET ?",
(size, offset)
)
rows = cursor.fetchall()
conn.close()
product_list = [{
"id": row[0],
"name": row[1],
"price": row[2],
"stock": row[3]
} for row in rows]
return jsonify({
"code": 0,
"msg": "ok",
"data": {
"list": product_list,
"total": total,
"page": page,
"size": size
}
})
@app.route("/api/order", methods=["POST"])
def create_order():
"""
创建订单接口
请求头:X-Token: <token>
请求体:{"product_id": 1, "quantity": 2}
响应:订单详情
用途:
- 练习Header传参(Token关联)
- 练习POST + JSON Body
- 写数据库操作,可观察写入性能
"""
token = request.headers.get("X-Token", "")
if not token.startswith("token_"):
return jsonify({"code": 403, "msg": "缺少有效Token"}), 403
# 从token中提取用户名(简化版,真实项目会解析JWT)
username = token.split("_")[1] if len(token.split("_")) > 1 else "unknown"
data = request.get_json(silent=True) or {}
product_id = data.get("product_id", 0)
quantity = data.get("quantity", 1)
if not product_id or quantity <= 0:
return jsonify({"code": 400, "msg": "参数错误"}), 400
conn = get_db()
cursor = conn.cursor()
# 查询用户ID
cursor.execute("SELECT id FROM users WHERE username = ?", (username,))
user_row = cursor.fetchone()
if not user_row:
conn.close()
return jsonify({"code": 404, "msg": "用户不存在"}), 404
user_id = user_row[0]
# 查询商品信息
cursor.execute("SELECT price, stock FROM products WHERE id = ?", (product_id,))
product_row = cursor.fetchone()
if not product_row:
conn.close()
return jsonify({"code": 404, "msg": "商品不存在"}), 404
price, stock = product_row[0], product_row[1]
# 库存检查
if quantity > stock:
conn.close()
return jsonify({"code": 400, "msg": "库存不足"}), 400
# 计算总价
total_price = round(price * quantity, 2)
create_time = time.time()
# 创建订单
cursor.execute(
"INSERT INTO orders (user_id, product_id, quantity, total_price, create_time) VALUES (?, ?, ?, ?, ?)",
(user_id, product_id, quantity, total_price, create_time)
)
order_id = cursor.lastrowid
# 扣减库存
cursor.execute(
"UPDATE products SET stock = stock - ? WHERE id = ?",
(quantity, product_id)
)
conn.commit()
conn.close()
return jsonify({
"code": 0,
"msg": "订单创建成功",
"data": {
"order_id": order_id,
"product_id": product_id,
"quantity": quantity,
"total_price": total_price,
"create_time": create_time
}
})
@app.route("/api/order/<int:order_id>", methods=["GET"])
def get_order(order_id):
"""
查询订单接口
用途:单条查询,练习路径参数
"""
conn = get_db()
cursor = conn.cursor()
cursor.execute(
"SELECT id, user_id, product_id, quantity, total_price, create_time FROM orders WHERE id = ?",
(order_id,)
)
row = cursor.fetchone()
conn.close()
if not row:
return jsonify({"code": 404, "msg": "订单不存在"}), 404
return jsonify({
"code": 0,
"msg": "ok",
"data": {
"order_id": row[0],
"user_id": row[1],
"product_id": row[2],
"quantity": row[3],
"total_price": row[4],
"create_time": row[5]
}
})
@app.route("/api/slow-query", methods=["GET", "POST"])
def slow_query():
"""
慢查询接口(故意制造瓶颈)
查询参数:?delay=2 (延迟秒数,默认2秒,最大10秒)
用途:
- 模拟生产环境中的慢SQL、慢接口
- 在JMeter中观察:当并发上来时,响应时间是否线性增长
- 自己改代码里的延迟时间,制造不同级别的瓶颈
扩展玩法:
- 把 delay 改成随机数,模拟不稳定的第三方接口
- 把 time.sleep 改成复杂的数据库全表扫描
"""
delay = request.args.get("delay", 2, type=float)
if delay > 10:
delay = 10 # 安全限制,防止恶意请求
# 模拟耗时操作:可以是 sleep,也可以是复杂计算
time.sleep(delay)
# 顺便做一次数据库查询,增加负载
conn = get_db()
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM products")
count = cursor.fetchone()[0]
conn.close()
return jsonify({
"code": 0,
"msg": "慢查询完成",
"data": {
"delay": delay,
"product_count": count,
"timestamp": time.time()
}
})
# ==================== 启动入口 ====================
if __name__ == "__main__":
init_db()
print("\n" + "=" * 50)
print(" 性能测试待测服务已启动")
print("=" * 50)
print(" 访问地址: http://127.0.0.1:5000")
print(" 接口列表:")
print(" GET /api/health")
print(" POST /api/login")
print(" GET /api/products?page=1&size=10")
print(" POST /api/order (Header: X-Token)")
print(" GET /api/order/<id>")
print(" GET /api/slow-query?delay=2")
print("=" * 50 + "\n")
# threaded=True 支持多线程并发处理请求
app.run(host="0.0.0.0", port=5000, threaded=True, debug=False)
Token关联与业务流程串联
场景:模拟真实用户——先登录获取Token,再带着Token去创建订单
为什么学这个?
| 错误做法 | 问题 |
|---|---|
直接压测 /api/order,不带 Token | 服务器返回 403,测的是"拒绝访问"的性能,不是"创建订单"的性能 |
| 手动写死 Token 到 Header | Token 会过期,而且每个用户 Token 不同,不能模拟多用户 |
正确做法:
1.线程启动后,先执行登录->拿到响应里的Token
2.把Token提取出来到变量里
3.下一个请求从变量里提取Token,放到请求头里
4.这样每一个用户都有独立的Token
Jemter实现这个流程的核心组件:
| 组件 | 作用 | 放在哪里 |
|---|---|---|
| JSON Extractor | 从 JSON 响应中提取指定字段的值 | 放在登录请求下面(后置处理器) |
| HTTP Header Manager | 给请求添加自定义 Header | 放在创建订单请求下面(配置元件) |
| 事务控制器 | 把多个请求打包成一个"业务操作"统计 | 放在线程组下面(逻辑控制器) |
性能瓶颈
判断性能拐点的4个标准
1.TPS停滞
并发数继续增加,但 TPS 不再同步增长,甚至出现下降
2.响应陡增
P95/P99 响应时间出现阶跃式上升(比如从 10ms 涨到 100ms)
3.错误率飙升
错误率从接近 0 开始快速上升,出现大量超时、服务报错
4.资源打满
被测服务器的 CPU 使用率持续超过 85%,或内存、磁盘 IO 出现瓶颈
排查思路
发现拐点只是第一步,还要能定位瓶颈出在哪一层。大厂通用的排查思路是从外到内、从上到下逐层排除:
第 1 层:施压端瓶颈(优先排除)
- 现象:并发加不上去,TPS 上不去,但被测服务器资源使用率很低
- 排查:查看运行 JMeter 的电脑 CPU、内存占用情况
- 结论:如果施压端资源占满,测出来的是本地电脑的上限,不是服务器的上限,需要增加施压机器做分布式压测
第 2 层:网络瓶颈
- 现象:响应时间里大部分是网络耗时,服务器实际处理时间很短
- 排查:本地服务基本不存在网络问题;跨机房、公网压测需要关注带宽、延迟、丢包率
第 3 层:应用服务端瓶颈
- CPU 瓶颈:业务逻辑复杂、大量循环计算、对象频繁创建销毁,CPU 使用率持续高位
- 内存瓶颈:内存泄漏、大对象过多,内存占用持续上涨不释放
- 线程池瓶颈:服务端工作线程数不足,请求排队等待,CPU 空闲但响应慢
第 4 层:数据库瓶颈(线上最常见)
- 慢 SQL:无索引、全表扫描、多表关联复杂,单条 SQL 耗时极高
- 锁等待:高并发写操作,行锁 / 表锁竞争,大量请求阻塞等待锁释放
- 连接池瓶颈:数据库连接数打满,新请求拿不到连接
更多推荐


所有评论(0)