Skip to content

性能测试培训 · 通用篇

流程方法 + 实战技巧 · 适用于任何产品的完整8步性能测试流程

步骤1:需求分析 —— "到底要扛多少量?"

💡 这是整个性能测试最重要的一步

超过 60% 的"无效压测"都是因为跳过了需求分析。没搞清楚目标就开始压,测了个寂寞。

1.1 为什么需求分析如此重要?

性能测试不是"压到死"就完事的。你需要先回答三个核心问题:

  1. 系统要服务多少用户? —— 日活、月活、注册用户数
  2. 高峰期是什么时候? —— 早高峰、午高峰、促销活动
  3. 用户会做什么操作? —— 浏览为主还是交易为主

1.2 数据来源

数据来源获取方法适用阶段
生产日志分析 Nginx/应用 access log 的 QPS 峰值已上线系统
Google Analytics / 埋点查看用户行为路径、停留时长已上线系统
运营预估向运营团队要推广计划、预期用户增长新系统上线前
竞品对标参考同类产品的公开数据新系统上线前
业务方口述直接问产品/运营"上线后预估多少人用"通用

1.3 峰值 QPS 计算方法

📐 二八法则计算公式

峰值 QPS = (总请求数 × 80%) / (总时长 × 20%) × 峰值系数(3~5倍) 解读:80% 的请求集中在 20% 的时间段内,再乘以峰值系数留余量。

示例

电商场景 —— 双十一大促

已知条件:
  → 日活用户 50 万
  → 平均每人每天 20 次 API 请求
  → 活动期间流量预估翻 3 倍

计算过程:
  → 日总请求 = 500,000 × 20 × 3 = 30,000,000(3000万)
  → 80% 集中在 20% 时间 = 24,000,000 / (24h × 20% = 4.8h = 17,280s)
  → 平均 QPS = 24,000,000 / 17,280 ≈ 1,389
  → 峰值 QPS(×3 系数)≈ 4,167
  → 保守目标:5,000 QPS
  → 同时在线并发用户 ≈ 5,000 ~ 10,000
示例

社交 App 场景 —— 新版本发布

已知条件:
  → 注册用户 200 万,日活率 30% = 60 万日活
  → 新版本发布当天推送通知,预计 40% 打开 = 24 万集中访问
  → 推送后 1 小时内是高峰
  → 每人平均 30 次请求(浏览信息流、发消息)

计算过程:
  → 高峰期总请求 = 240,000 × 30 = 7,200,000
  → 集中在 1 小时 = 7,200,000 / 3,600s = 2,000 QPS
  → 峰值(×3)= 6,000 QPS
  → 目标:系统要扛住 6,000+ QPS
示例

金融交易场景 —— 股市开盘

已知条件:
  → 活跃交易用户 10 万
  → 9:30 开盘瞬间,80% 用户同时刷新行情
  → 每人每秒 2~3 次请求(行情查询 + 下单)

计算过程:
  → 峰值并发 = 100,000 × 80% = 80,000 人
  → 峰值 QPS = 80,000 × 2.5 = 200,000 QPS
  → 极端场景:需要水平扩展 + CDN + 本地缓存

1.4 常见错误

⚠️ 需求分析阶段的典型错误

  • 拍脑袋:"我们就压 1000 并发吧" —— 没有数据支撑
  • **混淆概念:**把"注册用户数"当"并发用户数" —— 100 万注册用户 ≠ 100 万并发
  • **忽略增长:**只按当前流量测,没有预留 3~6 个月增长空间
  • **忽略突发:**只算平均值,没考虑秒杀/推送等瞬时脉冲
  • 照搬别人:"XX 公司压了 10 万并发" —— 业务场景不同

1.5 需求分析产出物

完成需求分析后,你应该输出一份性能测试需求文档,包含:

项目内容
目标 QPS如:系统需稳定支撑 5,000 QPS
目标并发数如:支撑 2,000 同时在线用户
响应时间目标如:P95 < 500ms,P99 < 1s
错误率目标如:< 0.1%
稳定性要求如:连续运行 8 小时无性能衰减
数据来源说明以上数据的推算依据

课堂练习

练习:为你当前负责的项目做一次需求分析

  1. 找到你项目的日活用户数(没有的话估算)
  2. 用二八法则计算峰值 QPS
  3. 确定响应时间和错误率的目标值
  4. 写出完整的性能需求表格 提示:如果完全没有数据,先用竞品分析或运营预估。重要的是有数据支撑,而不是拍脑袋。

步骤2:识别核心场景 —— "测哪些接口?"

资源有限,不可能所有接口都测。二八法则再次生效:20% 的接口承载了 80% 的流量。

2.1 如何找到核心场景

📖 场景识别的三种方法

  1. **日志分析法:**从 access log 统计各接口调用量 Top 20
  2. **用户旅程法:**画出核心用户路径(注册→登录→核心操作→退出)
  3. **业务优先级法:**和产品经理确认哪些功能不能挂

2.2 场景建模 —— 按真实比例分配流量

示例

电商平台场景建模

场景 1(50% 流量):商品浏览
  → GET /api/home-feed         首页推荐
  → GET /api/products?page=1   商品列表
  → GET /api/product/:id       商品详情

场景 2(20% 流量):搜索
  → GET /api/search?q=xxx      搜索
  → GET /api/search/suggest    搜索联想

场景 3(15% 流量):用户相关
  → POST /api/login            登录
  → GET /api/user/profile      个人中心
  → PUT /api/user/address      修改地址

场景 4(10% 流量):交易核心
  → POST /api/cart/add         加购物车
  → POST /api/order/create     创建订单
  → POST /api/payment/pay      支付

场景 5(5% 流量):其他
  → GET /api/notifications     消息通知
  → POST /api/feedback         用户反馈
示例

社交 App 场景建模

场景 1(40% 流量):信息流
  → GET /api/feed              刷信息流
  → GET /api/post/:id          查看帖子详情
  → POST /api/post/:id/like    点赞

场景 2(25% 流量):即时通讯
  → WebSocket /ws/chat         聊天长连接
  → POST /api/message/send     发消息
  → GET /api/conversations     会话列表

场景 3(20% 流量):用户互动
  → GET /api/user/:id          查看他人主页
  → POST /api/follow           关注
  → GET /api/comments          查看评论

场景 4(15% 流量):内容发布
  → POST /api/post/create      发帖
  → POST /api/upload           上传图片/视频

2.3 常见错误

⚠️ 场景建模的常见坑

  • **只测首页:**首页有 CDN 缓存,性能很好不说明什么
  • **只测 GET:**写操作(POST/PUT/DELETE)才是真正考验数据库的
  • **所有接口平均分配:**真实流量不是均匀分布的
  • **忽略登录态:**未登录请求有缓存,登录后每次都要鉴权
  • **忽略关联性:**查看购物车和下单是有先后关系的

课堂练习

练习:为一个电商网站设计测试场景

  1. 列出该网站最核心的 10 个 API
  2. 按照你认为的真实流量比例分配权重
  3. 画出至少两条完整的"用户旅程"(如:浏览→加购→下单→支付)
  4. 标注哪些接口是读操作、哪些是写操作

步骤3:环境准备 —— "在哪里测?"

3.1 测试环境要求

要素要求为什么重要
服务器配置和生产环境尽量一致4核8G 测出来的结果不能代表 16核32G 的表现
数据库同版本、同配置、接近真实数据量100 条数据测索引是没有意义的
中间件Redis/MQ/Nginx 版本和配置一致Redis 内存不同、连接数配置不同都会影响结果
网络压测机和被测服务同机房/同网段跨机房的网络延迟会成为瓶颈假象
测试数据预埋足量数据(账号、业务数据)所有人用同一个账号 → 缓存命中 100% → 失真
监控装好 Prometheus + Grafana 或同类监控压测时必须能实时看到服务器指标

3.2 环境隔离

⚠️ 三条铁律

  1. 绝对不要在生产环境压测 —— 会直接影响真实用户
  2. 压测环境不要和开发/测试环境共享数据库 —— 压测会把库压崩
  3. 压测前通知相关方 —— 运维、DBA、网络团队都要知道

3.3 压测机准备

发起压力的机器本身也可能成为瓶颈:

检查项说明
CPU压测机 CPU 使用率不应超过 70%,否则瓶颈在压测机
内存JMeter 等 Java 工具吃内存很厉害,至少 4G+
网络千兆网卡跑满 ≈ 120MB/s,如果响应体大需注意
文件描述符Linux 默认 1024,高并发需要调到 65535+
端口范围默认可用端口约 28,000 个,大量短连接可能耗尽
# Linux 系统调优(在压测机上执行)
# 查看当前限制
ulimit -n

# 临时调大文件描述符
ulimit -n 65535

# 调大端口范围
sysctl -w net.ipv4.ip_local_port_range="1024 65535"

# 允许 TIME_WAIT 状态的连接复用
sysctl -w net.ipv4.tcp_tw_reuse=1

课堂练习

练习:搭建一个最小压测环境

  1. 准备一台被测机(可以用本地 Docker 跑个 Nginx + 简单 API)
  2. 准备一台压测机(可以是你的开发机)
  3. 检查两台机器的网络连通性(pingcurl
  4. 在被测机上安装基础监控(tophtop 先用起来)
  5. 确认文件描述符等系统参数已调优

步骤4:工具选型 —— "用什么工具?"

工具选型没有"最好的",只有"最适合的"。选型取决于你的团队技术栈测试场景集成需求

4.1 主流工具对比

维度JMeterk6Locustwrk/hey/ab
语言Java(GUI + XML)JavaScriptPython命令行参数
上手难度中等极低
资源消耗极低极低
协议支持HTTP/JDBC/JMS/FTP/...HTTP/WS/gRPCHTTP(可扩展)仅 HTTP
分布式内置k6 Cloud内置不支持
CI/CD 集成一般很好很好很好
脚本可维护性XML 不友好JS 代码,版本可控Python 代码,版本可控无脚本
实时 Web UIGUI 本身就是需搭配 Grafana内置
生态和插件极其丰富丰富中等

4.2 工具选型决策表

📖 根据你的场景选工具

你的情况推荐工具原因
团队用 Java,企业项目JMeter生态最全,企业级功能完善
需要嵌入 CI/CD Pipelinek6CLI 友好,结果输出标准化
团队用 PythonLocustPython 生态,逻辑灵活
只是快速验证一个接口wrk / hey一行命令搞定
需要测 WebSocketk6 / Artillery原生支持 WS 协议
需要复杂业务逻辑Locust / k6代码编写,逻辑灵活
需要测数据库/消息队列JMeter有 JDBC/JMS Sampler
预算有限,不想付费任意(都开源)以上都是免费开源工具

课堂练习

练习:为以下场景选择合适的工具,并说明理由

  1. 一个 Python + Flask 写的内部 API 服务,团队 3 人
  2. 一个微服务架构的电商平台,需要压测 50+ 接口,CI/CD 集成
  3. 紧急上线前 1 小时,需要快速验证首页 API 能不能扛住

步骤5:编写测试脚本 —— "模拟真实用户"

5.1 核心原则:模拟真实用户行为

⚠️ 最大的错误:把性能测试做成了 DDoS 攻击

1000 个线程无间隔循环请求 GET /api/home —— 这不是用户行为。真实用户会浏览、思考、点击、等待,中间有停顿。

5.2 脚本编写要点

要点说明
加思考时间每次请求之间加 2~10 秒随机等待
参数化不同用户用不同账号、搜索不同关键词
关联登录返回的 Token 传给后续请求
断言不只看状态码 200,要验证响应内容正确
错误处理遇到错误要记录,不要默默跳过

5.3 k6 脚本示例(电商下单流程)

python
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';

const users = new SharedArray('users', function () {
  return JSON.parse(open('./test-users.json'));
});

export const options = {
  stages: [
    { duration: '2m', target: 50 },   // 2分钟加到50并发
    { duration: '5m', target: 50 },   // 保持50并发5分钟
    { duration: '2m', target: 100 },  // 加到100
    { duration: '5m', target: 100 },  // 保持100并发5分钟
    { duration: '2m', target: 0 },    // 逐步减到0
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],  // P95 < 500ms
    http_req_failed: ['rate<0.01'],    // 错误率 < 1%
  },
};

export default function () {
  const user = users[__VU % users.length];
  const BASE = 'https://api.example.com';

  // Step 1: 登录
  const loginRes = http.post(`${BASE}/api/login`, JSON.stringify({
    username: user.username,
    password: user.password,
  }), { headers: { 'Content-Type': 'application/json' } });

  check(loginRes, { '登录成功': (r) => r.status === 200 });
  const token = loginRes.json('data.token');

  sleep(Math.random() * 3 + 2); // 思考 2~5

  // Step 2: 浏览商品列表
  const headers = { Authorization: `Bearer ${token}` };
  const listRes = http.get(`${BASE}/api/products?page=1`, { headers });
  check(listRes, { '商品列表加载': (r) => r.status === 200 });

  sleep(Math.random() * 5 + 3); // 思考 3~8

  // Step 3: 查看商品详情
  const productId = listRes.json('data.items.0.id');
  const detailRes = http.get(`${BASE}/api/product/${productId}`, { headers });
  check(detailRes, { '商品详情': (r) => r.status === 200 });

  sleep(Math.random() * 3 + 2);

  // Step 4: 加入购物车
  const cartRes = http.post(`${BASE}/api/cart/add`, JSON.stringify({
    product_id: productId, quantity: 1,
  }), { headers: { ...headers, 'Content-Type': 'application/json' } });
  check(cartRes, { '加购成功': (r) => r.status === 200 });

  sleep(Math.random() * 2 + 1);

  // Step 5: 创建订单
  const orderRes = http.post(`${BASE}/api/order/create`, JSON.stringify({
    cart_id: cartRes.json('data.cart_id'),
  }), { headers: { ...headers, 'Content-Type': 'application/json' } });
  check(orderRes, { '下单成功': (r) => r.status === 200 || r.status === 201 });

  sleep(Math.random() * 2 + 1);
}

5.4 常见错误

⚠️ 脚本编写常见坑

  • **没有思考时间:**结果严重偏悲观
  • **硬编码数据:**所有用户查同一个商品 → 缓存命中率 100%
  • **忽略 Cookie/Session:**接口依赖登录态,但脚本没传 Token
  • **没有断言:**返回 500 但因为"有响应"就当成功了
  • **没有处理依赖:**下单依赖加购的 cart_id,但没有提取

课堂练习

练习:编写一个完整的压测脚本

  1. 选择 k6 或 Locust(根据你熟悉的语言)
  2. 模拟一个用户的完整操作流程(至少 3 个步骤)
  3. 包含:思考时间、参数化、关联、断言
  4. 配置阶梯式加压(ramp-up)

步骤6:分阶段执行 —— "逐步加压"

6.1 五阶段执行法

基准测试 → 负载测试 → 压力测试 → 稳定性测试 → 峰值测试

阶段并发数持续时间目的关注指标
基准测试1 个用户跑完整流程建立性能基准线每个接口的 RT
负载测试10→50→100→200→500每级 5~10 分钟找拐点RT 变化趋势、错误率
压力测试超过目标 → 直到崩溃逐步加找天花板最大 QPS、崩溃表现
稳定性测试目标并发(如 500)4~8 小时找隐藏问题内存增长、连接泄漏
峰值测试低 → 瞬间拉到极高脉冲式抗突发能力恢复时间

6.2 每个阶段的详细操作

基准测试(Baseline)

操作:

1 个用户跑完整业务流程 3~5 遍 **记录:**每个接口的响应时间(这是零压力下的最佳值) **目的:**后续所有测试结果都和这个基准对比

负载测试(Load Test)

操作:

阶梯式加压 → 10 → 50 → 100 → 200 → 目标并发 每级保持 5~10 分钟,等系统稳定后记录数据 **重点看:**RT 曲线在哪个阶段开始上升(那个点就是"拐点")

压力测试(Stress Test)

操作:

继续加压超过目标 → 500 → 800 → 1000 → ... **目的:**找到系统天花板 —— 开始出现大量错误或直接崩溃的点 **注意:**记录崩溃时的表现(OOM?连接拒绝?响应超时?)

稳定性测试(Soak Test)

操作:

在目标并发下持续跑 4~24 小时 **目的:**找内存泄漏、连接泄漏、日志磁盘打满等长时间运行才会暴露的问题 **重点看:**内存是否持续增长、GC 频率是否增加、RT 是否逐渐变大

峰值测试(Spike Test)

操作:

从 50 并发瞬间拉到 1000,保持 2 分钟,然后降回 50 **目的:**模拟秒杀、推送通知等瞬时流量 **重点看:**高峰期是否有雪崩?降下来后多久恢复正常?

示例

阶梯式加压的典型数据记录表

并发数QPSP50(ms)P95(ms)P99(ms)错误率CPU内存
1095451201800%15%2.1G
50450551502200%42%2.3G
100880652003500.01%68%2.5G
2001200953806500.1%85%3.0G
5001350250120035002.5%98%3.8G
**结论:**拐点在 200 并发(RT 开始显著上升),天花板约 1350 QPS,CPU 是瓶颈。

课堂练习

练习:执行一次完整的分阶段压测

  1. 选择一个本地服务或公开 API
  2. 按照 5 个阶段依次执行(基准 → 负载 → 压力 → 稳定性 → 峰值)
  3. 记录每个阶段的关键数据
  4. 画出"并发数 vs 响应时间"的曲线,找到拐点

步骤7:分析结果 & 调优 —— "找到瓶颈"

7.1 两边同时看

压测时必须同时观察客户端和服务端

视角看什么说明
客户端 (压测工具)响应时间 P50/P95/P99是否达标
错误率是否在容忍范围内
QPS/TPS是否达到目标
并发数 vs RT 曲线拐点在哪里
服务端 (监控系统)CPU超 80% → 计算瓶颈
内存持续增长 → 内存泄漏
磁盘 IO高 → 数据库读写瓶颈
网络带宽打满 → 加带宽或压缩
数据库慢查询有 → 加索引优化 SQL
连接池耗尽 → 加大池或优化复用

7.2 调优的正确流程

压测 → 发现问题 → 定位瓶颈 → 制定方案 → 实施修改 → 再次压测 → 对比数据

💡 调优的核心原则:控制变量

每次只改一个地方,然后重新压测对比。如果同时改了 3 个地方,你不知道是哪个生效了。

7.3 常见调优手段

瓶颈类型调优手段预期效果
CPU 打满优化代码热点(循环、序列化)、升级配置、水平扩容QPS 提升 50%~200%
内存不够查泄漏(heap dump 分析)、减少对象创建、调整 GC 策略稳定性改善
数据库慢加索引、优化 SQL、读写分离、加 Redis 缓存RT 降低 50%~90%
连接不够加大连接池、使用连接复用、异步非阻塞并发能力翻倍
网络瓶颈启用 Gzip、减少响应体大小、使用 CDN带宽减少 60%+
第三方限流加队列缓冲、降级策略、协商提高配额减少 429 错误
示例

调优前后对比示例

指标调优前调优后改善
QPS8002,200+175%
P95 RT1,200ms350ms-71%
P99 RT3,500ms800ms-77%
错误率2.5%0.05%-98%
CPU 峰值98%65%-33%
**做了什么:**给 3 条慢 SQL 加了索引 + 高频查询加了 Redis 缓存(TTL 60s)

课堂练习

练习:做一次"测→找→改→再测"的调优循环

  1. 对一个接口做基准测试,记录 P95 响应时间
  2. 尝试一种优化手段(如加缓存、优化 SQL)
  3. 用完全相同的压测条件重新测试
  4. 对比优化前后的数据,计算改善百分比

步骤8:输出结论 —— "能不能上线?"

8.1 最终结论模板

✅ 性能测试最终结论(示例)

测试目标:系统需支撑 2,000 并发用户、3,000 QPS
测试环境:4C8G × 3 节点 + MySQL 8C16G + Redis 4G

测试结果:
  ✓ 系统稳定支撑 2,500 并发用户,QPS 达到 3,500
  ✓ P95 响应时间 320ms(目标 < 500ms)✅ 达标
  ✓ P99 响应时间 680ms(目标 < 1s)✅ 达标
  ✓ 错误率 0.03%(目标 < 0.1%)✅ 达标
  ✓ 持续运行 8 小时无内存泄漏、无性能衰减
  ✓ 性能拐点在 2,000 并发,天花板在 3,000 并发
  ✓ 峰值测试:瞬时 5,000 并发有 5% 超时,2 分钟内恢复

瓶颈分析:
  → 主要瓶颈在 MySQL 查询(已通过加索引+缓存解决)
  → 次要瓶颈在应用层 GC(已调整 JVM 参数)

结论:✅ 满足上线要求,可以发布

风险提示:
  → 如果日活超过 80 万,建议提前做数据库读写分离
  → 促销活动期间建议临时加 2 个应用节点

8.2 好结论 vs 差结论

差结论好结论
"测了 1000 并发没问题""系统在 1000 并发下 P95=320ms、错误率 0.03%,达标"
"性能还行""QPS 达到 3500,超过目标 3000 的 17%"
"数据库有点慢""order 表缺少 user_id 索引,200 并发时慢查询占比 35%"
"建议优化""建议给 order 表 user_id 字段加索引,预计 RT 降低 60%"

💡 结论要有"数据+分析+建议"

不只是堆数据,要分析原因,给出可执行的建议。领导/开发看你的报告后,应该知道下一步该做什么。

课堂练习

练习:写一份性能测试结论 假设你做了以下压测(数据虚构):

  • 目标:支撑 500 并发、1000 QPS、P95 < 800ms
  • 实际:500 并发时 QPS=950、P95=720ms、错误率 0.2%
  • 800 并发时 P95=2500ms、错误率 5%
  • CPU 在 500 并发时 75%,800 并发时 98% 请写出完整的结论,包含:是否达标、瓶颈在哪、优化建议、风险提示。

JMeter 入门

工具简介

Apache JMeter 是最老牌的性能测试工具,2003 年首次发布,基于 Java 开发。它有 GUI 界面,支持的协议最多(HTTP/HTTPS/FTP/JDBC/JMS/LDAP/SOAP 等),插件生态极其丰富。适合企业级项目和需要测试非 HTTP 协议的场景。

适用场景

  • 企业级项目,需要 GUI 方便非开发人员使用
  • 需要测试数据库(JDBC)、消息队列(JMS)等非 HTTP 协议
  • 需要丰富的插件(如 JP@GC 系列监控插件)
  • 团队技术栈是 Java

安装方法

# 前提:安装 Java 8+
java -version

# 方式 1:下载二进制包
# 访问 https://jmeter.apache.org/download_jmeter.cgi
# 下载 apache-jmeter-5.6.3.zip
unzip apache-jmeter-5.6.3.zip
cd apache-jmeter-5.6.3/bin

# 启动 GUI(仅用于编写脚本,不要用 GUI 压测)
./jmeter.sh        # Linux/Mac
jmeter.bat          # Windows

# 命令行模式执行(推荐!)
./jmeter -n -t test-plan.jmx -l result.jtl -e -o report/

# 方式 2:Mac Homebrew
brew install jmeter

最小可用示例

JMeter 使用 XML 格式的 .jmx 文件,通常用 GUI 创建后导出。以下是纯命令行操作:

# 1. 打开 JMeter GUI
./jmeter.sh

# 2. 在 GUI 中操作:
#    Test Plan → 右键 Add → Threads → Thread Group
#    设置:线程数=10, 循环=100
#    Thread Group → Add → Sampler → HTTP Request
#    设置:Server=httpbin.org, Path=/get
#    Thread Group → Add → Listener → View Results Tree
#    Thread Group → Add → Listener → Summary Report
#    保存为 test.jmx

# 3. 命令行执行(正式压测必须用命令行模式)
jmeter -n -t test.jmx -l result.jtl -e -o html-report/

# 参数说明:
#   -n  非 GUI 模式
#   -t  测试计划文件
#   -l  结果日志文件
#   -e  生成 HTML 报告
#   -o  报告输出目录

优缺点

优点缺点
协议支持最全Java 编写,资源消耗大
GUI 友好,门槛低.jmx 是 XML,版本管理困难
插件极其丰富启动慢,占内存多
分布式模式内置复杂逻辑用 BeanShell 不方便
社区大,资料多CI/CD 集成不如 k6 方便

k6 入门

工具简介

k6 是 Grafana Labs 开发的现代性能测试工具,用 Go 编写核心引擎、JavaScript 编写测试脚本。它极其轻量(单个二进制文件)、资源消耗低、对开发者友好,原生支持与 CI/CD 和 Grafana 集成。

适用场景

  • 开发者友好的团队,习惯写代码而非用 GUI
  • 需要嵌入 CI/CD Pipeline(GitHub Actions/GitLab CI)
  • HTTP/WebSocket/gRPC 测试
  • 需要与 Grafana 集成做可视化

安装方法

# Mac
brew install k6

# Linux (Debian/Ubuntu)
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
  --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D68
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" \
  | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6

# Docker
docker run --rm -i grafana/k6 run - <script.js

# Windows
choco install k6
# 或 winget install k6

最小可用脚本

// file: basic-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  vus: 10,           // 10 个虚拟用户
  duration: '30s',   // 持续 30 秒
  thresholds: {
    http_req_duration: ['p(95)<500'],  // P95 < 500ms
    http_req_failed: ['rate<0.01'],    // 错误率 < 1%
  },
};

export default function () {
  const res = http.get('https://httpbin.org/get');
  check(res, {
    '状态码 200': (r) => r.status === 200,
    '响应时间 < 500ms': (r) => r.timings.duration < 500,
  });
  sleep(1);
}

// 运行:k6 run basic-test.js

阶梯式加压脚本

// file: staged-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '1m', target: 20 },   // 1分钟内加到 20 VU
    { duration: '3m', target: 20 },   // 保持 20 VU 三分钟
    { duration: '1m', target: 50 },   // 加到 50
    { duration: '3m', target: 50 },   // 保持 50
    { duration: '1m', target: 100 },  // 加到 100
    { duration: '3m', target: 100 },  // 保持 100
    { duration: '2m', target: 0 },    // 降到 0
  ],
};

export default function () {
  const res = http.get('https://httpbin.org/get');
  check(res, { 'OK': (r) => r.status === 200 });
  sleep(Math.random() * 3 + 1); // 1~4 秒思考时间
}

// 运行并输出 JSON 结果:
// k6 run --out json=results.json staged-test.js

优缺点

优点缺点
Go 核心极其轻量,资源消耗低不支持浏览器端性能测试(需要 k6 browser)
JS 脚本,开发者友好不能安装 npm 包(受限的 JS 运行时)
CLI 模式天然适合 CI/CD没有内置 GUI
原生支持 Grafana 输出分布式需要 k6 Cloud(付费)
单机能模拟几万 VU非 HTTP 协议支持有限

Locust 入门

工具简介

Locust 是用 Python 编写的性能测试框架,以"蝗虫"命名,寓意"像蝗虫群一样发起攻击"。它使用 Python 代码定义用户行为,内置 Web UI 可以实时观察结果,分布式扩展非常容易。

适用场景

  • 团队技术栈是 Python
  • 测试逻辑复杂,需要灵活的代码控制
  • 需要实时 Web UI 监控
  • 需要简单的分布式压测

安装方法

# pip 安装
pip install locust

# 验证安装
locust --version

# Docker 方式
docker run -p 8089:8089 -v $PWD:/mnt/locust locustio/locust \
  -f /mnt/locust/locustfile.py

最小可用脚本

# file: locustfile.py
from locust import HttpUser, task, between

class WebUser(HttpUser):
    wait_time = between(1, 5)  # 每次请求间等待 1~5 秒
    host = "https://httpbin.org"

    @task(3)  # 权重 3,被选中的概率是其他的 3 倍
    def view_homepage(self):
        self.client.get("/get")

    @task(1)  # 权重 1
    def view_post(self):
        self.client.get("/headers")

    def on_start(self):
        """每个虚拟用户启动时执行一次"""
        pass

# 运行方式 1:Web UI 模式(浏览器打开 http://localhost:8089)
# locust -f locustfile.py

# 运行方式 2:命令行模式(无 UI)
# locust -f locustfile.py --headless -u 100 -r 10 -t 5m
#   -u 100  总用户数
#   -r 10   每秒启动 10 个用户
#   -t 5m   持续 5 分钟

带登录和业务逻辑的脚本

# file: locustfile_ecommerce.py
from locust import HttpUser, task, between, SequentialTaskSet
import json

class ShoppingFlow(SequentialTaskSet):
    """顺序执行的任务集:登录 → 浏览 → 加购 → 下单"""

    def on_start(self):
        resp = self.client.post("/api/login", json={
            "username": "testuser",
            "password": "testpass"
        })
        self.token = resp.json().get("token", "")
        self.headers = {"Authorization": f"Bearer {self.token}"}

    @task
    def browse_products(self):
        self.client.get("/api/products", headers=self.headers)

    @task
    def view_detail(self):
        self.client.get("/api/product/1001", headers=self.headers)

    @task
    def add_to_cart(self):
        self.client.post("/api/cart/add",
            json={"product_id": 1001, "quantity": 1},
            headers=self.headers)

    @task
    def create_order(self):
        self.client.post("/api/order/create",
            json={"cart_id": "latest"},
            headers=self.headers)
        self.interrupt()  # 结束这个任务集,重新开始

class EcommerceUser(HttpUser):
    wait_time = between(2, 5)
    host = "https://api.example.com"
    tasks = [ShoppingFlow]

优缺点

优点缺点
Python 代码,逻辑灵活Python 性能不如 Go/Java
内置 Web UI,实时可视化单机并发能力有限(几千 VU)
分布式简单(master-worker)只支持 HTTP/HTTPS(需扩展才支持其他)
代码即测试,版本管理方便缺少内置的断言框架
社区活跃,文档清晰报告功能较简单

命令行工具(wrk / hey / ab)

工具简介

当你只需要快速验证一个接口的基本性能时,这些命令行工具是最快的选择。一行命令就能得到 QPS、延迟分布等关键数据。

wrk —— 高性能 HTTP 基准测试工具

# 安装
brew install wrk        # Mac
apt install wrk         # Ubuntu

# 基本用法
wrk -t4 -c100 -d30s https://httpbin.org/get
# -t4     4 个线程
# -c100   100 个并发连接
# -d30s   持续 30 秒

# 输出示例:
# Running 30s test @ https://httpbin.org/get
#   4 threads and 100 connections
#   Thread Stats   Avg      Stdev     Max   +/- Stdev
#     Latency    45.32ms   12.10ms  250ms    85.20%
#     Req/Sec   552.31    120.45   1.2k      72.00%
#   66120 requests in 30s, 28.5MB read
# Requests/sec:   2204.00
# Transfer/sec:    973KB

# 带 Lua 脚本的高级用法(POST 请求)
wrk -t4 -c100 -d30s -s post.lua https://api.example.com/login

hey —— Go 编写的 HTTP 压测工具

# 安装
brew install hey        # Mac
go install github.com/rakyll/hey@latest  # Go

# 基本用法
hey -n 10000 -c 100 https://httpbin.org/get
# -n 10000   总共发 10000 个请求
# -c 100     100 个并发

# POST 请求
hey -n 5000 -c 50 -m POST \
  -H "Content-Type: application/json" \
  -d '{"username":"test","password":"pass"}' \
  https://api.example.com/login

# 输出包含:延迟分布(P10/P25/P50/P75/P90/P95/P99)、状态码分布

ab(Apache Bench)—— Apache 自带

# 大部分系统自带
ab -n 10000 -c 100 https://httpbin.org/get
# -n 10000   总请求数
# -c 100     并发数

# POST 请求
ab -n 5000 -c 50 -p data.json -T "application/json" \
  https://api.example.com/login

命令行工具对比

工具语言优势局限
wrkC性能最高,支持 Lua 脚本只支持 Linux/Mac
heyGo输出详细延迟分布,跨平台不支持脚本扩展
abC系统自带,无需安装功能最简单,单线程

📖 什么时候用命令行工具?

  • 快速验证:上线前 5 分钟,用 hey 验证一下接口
  • 基准对比:优化前后用 wrk 快速对比 QPS
  • CI 冒烟测试:在 Pipeline 里加一步 hey 验证 **不适合:**复杂业务流程、需要登录态、多接口混合场景

课堂练习

练习:用命令行工具对公开 API 做基准测试

  1. 安装 hey(或 wrk
  2. https://httpbin.org/get 做基准测试:100 并发、持续 30 秒
  3. 记录 QPS、P50/P95/P99 延迟
  4. 改成 200 并发重新测,对比结果差异
  5. 用 POST 方法测试 https://httpbin.org/post,观察和 GET 的性能差异

操作系统层监控

性能测试时,操作系统层是最基础的监控层。你需要实时观察 CPU、内存、磁盘、网络四大维度。

CPU 监控

指标含义健康范围异常说明
%user用户态 CPU(应用代码运行)< 70%高 → 应用逻辑是瓶颈
%system内核态 CPU(系统调用、网络处理)< 30%高 → 可能大量系统调用/上下文切换
%iowaitCPU 等待磁盘 IO 完成< 10%高 → 磁盘是瓶颈(慢查询/大量日志写入)
%steal被虚拟化宿主机抢占< 5%高 → 云主机被超卖,换机器
%idle空闲 CPU> 20%接近 0 → CPU 已饱和
# 查看 CPU 使用率(每 1 秒刷新)
top -d 1
# 或更友好的
htop

# 查看 CPU 各维度详情
vmstat 1
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs  us  sy  id  wa  st
#  2  0      0 245612  52840 1024000   0    0     0    12  800  600  35   5  55   5   0

# mpstat 查看每个 CPU 核心
mpstat -P ALL 1

# 查看上下文切换(context switch)
vmstat 1 | awk '{print $12, $13}'  # cs 和 us 列

内存监控

指标含义说明
used已使用内存应用 + 缓存实际占用
cached文件缓存(可回收)Linux 会尽量用内存做缓存,这部分可回收
buffer磁盘缓冲写操作的缓冲区
swap交换分区(用磁盘当内存)一旦使用 swap 性能会急剧下降
available实际可用内存free + 可回收的 cached/buffer

⚠️ 看内存不能只看 "free"

Linux 的 free 经常显示很低,但这不代表内存不够。要看 available( = free + cached 可回收部分)。真正的告警线是 available < 10%swap 使用 > 0

# 查看内存
free -h
#               total        used        free      shared  buff/cache   available
# Mem:           16Gi       6.2Gi       1.8Gi       256Mi       8.0Gi       9.2Gi
# Swap:         4.0Gi          0B       4.0Gi

# 持续监控内存变化(观察是否有泄漏)
watch -n 1 free -h

磁盘监控

指标含义健康范围
IOPS每秒读写次数取决于磁盘类型(SSD: 数万, HDD: 数百)
吞吐量每秒读写数据量(MB/s)SSD: 200~3000 MB/s, HDD: 50~200 MB/s
队列长度(avgqu-sz)等待处理的 IO 请求数< 2 为佳,> 10 说明磁盘忙不过来
%util磁盘忙碌时间占比< 80%
awaitIO 请求平均等待时间(ms)SSD < 2ms, HDD < 10ms
# iostat 查看磁盘 IO
iostat -xdm 1
# Device   r/s    w/s    rMB/s  wMB/s  await  %util
# sda      120    450    1.2    8.5    3.2    65%

# iotop 查看哪个进程占 IO 最多
sudo iotop -o

网络监控

指标含义说明
带宽使用进出流量(MB/s)千兆网卡上限约 120 MB/s
连接数TCP 活跃连接总数每个连接占用文件描述符
ESTABLISHED已建立的连接正在通信的连接
TIME_WAIT等待关闭的连接大量 TIME_WAIT → 短连接太多
CLOSE_WAIT对端已关闭但本端未关闭大量 CLOSE_WAIT → 应用没正确关闭连接(泄漏)
# 查看网络流量(需安装 iftop)
sudo iftop

# 查看 TCP 连接状态分布
ss -s
# 或
netstat -ant | awk '{print $6}' | sort | uniq -c | sort -rn

# 典型输出:
#   5200 ESTABLISHED
#   1800 TIME_WAIT
#     50 CLOSE_WAIT    ← 如果这个数字持续增长,说明有连接泄漏
#     20 LISTEN

常用监控命令汇总

命令用途常用参数
top / htop整体概览(CPU/内存/进程)top -d 1 每秒刷新
vmstatCPU/内存/IO/上下文切换vmstat 1 每秒采样
iostat磁盘 IO 详情iostat -xdm 1
netstat / ss网络连接ss -s 连接统计
iftop实时网络流量sudo iftop -i eth0
sar历史性能数据(系统活动报告)sar -u 1 10 CPU、 sar -r 内存
dstat多合一监控(CPU+IO+网络)dstat -cdnm

课堂练习

练习:在服务器上执行系统监控

  1. 在一台 Linux 服务器上打开 htop,识别 CPU 占用最高的进程
  2. free -h 查看内存,计算 availabletotal 的百分比
  3. ss -s 查看 TCP 连接状态分布,是否有大量 TIME_WAIT 或 CLOSE_WAIT
  4. iostat -xdm 1 观察磁盘,看 %util 是否接近 100%

应用层监控

操作系统指标告诉你"机器怎么样",但不告诉你"应用怎么样"。应用层监控深入到代码和运行时层面。

线程 / 协程

语言关注指标工具
Java线程数、BLOCKED/WAITING 状态线程jstack 、VisualVM、Arthas
GoGoroutine 数量pprof 、 runtime.NumGoroutine()
Python线程数、协程数(asyncio)py-spy 、内置 threading.active_count()
Node.jsEvent Loop 延迟、活跃 Handle 数clinic.js 、 --inspect

连接池使用率

💡 连接池是最常见的应用层瓶颈之一

连接池类型关注什么告警条件
数据库连接池活跃连接数 / 最大连接数使用率 > 80%
HTTP 连接池等待获取连接的线程数有线程等待超时
Redis 连接池已用 / 最大连接数使用率 > 80%
# Java:查看 HikariCP 连接池(通过 JMX 或日志)
# 配置 HikariCP 打印连接池状态:
# spring.datasource.hikari.maximum-pool-size=20
# spring.datasource.hikari.leak-detection-threshold=30000

# Go:database/sql 连接池监控
# db.Stats() 返回 MaxOpenConnections, InUse, Idle, WaitCount, WaitDuration

GC(垃圾回收)监控

语言GC 特点关注指标问题表现
JavaFull GC 会 STW(Stop The World)GC 频率、GC 停顿时间、老年代使用率Full GC 频繁 → 长时间卡顿
GoGC 停顿短(通常 < 1ms)GC 停顿、堆大小变化内存分配速率过高
Python引用计数 + 分代回收循环引用数量大对象释放慢
# Java GC 日志分析
# 启动参数加:
# -Xlog:gc*:file=gc.log:time,uptime,level,tags
# 或旧版本:
# -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

# 使用 Arthas(阿里开源 Java 诊断工具)
java -jar arthas-boot.jar
dashboard  # 实时看线程、内存、GC 等

请求队列与错误日志

  • **请求队列长度:**Tomcat 的 acceptCount、Nginx 的 listen backlog —— 队列满了请求就被拒绝
  • **错误日志频率:**压测时观察错误日志的增长速度 —— 如果每秒几百条错误日志,可能磁盘 IO 就被日志打满了

数据库层监控

活跃连接数

# MySQL
SHOW STATUS LIKE 'Threads_connected';    -- 当前连接数
SHOW VARIABLES LIKE 'max_connections';    -- 最大连接数
SHOW PROCESSLIST;                        -- 每个连接在做什么

# PostgreSQL
SELECT count(*) FROM pg_stat_activity;   -- 当前连接数
SHOW max_connections;                    -- 最大连接数

慢查询

📖 慢查询是数据库层最常见的性能杀手

# MySQL:开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;   -- 超过 1 秒记为慢查询
SET GLOBAL slow_query_log_file = '/var/log/mysql-slow.log';

# 查看慢查询日志
mysqldumpslow -s t -t 10 /var/log/mysql-slow.log  # 按时间排序 Top 10

# 分析执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 12345;
# 关注:type(ALL 是全表扫描=灾难)、rows(扫描行数)、Extra(Using filesort 不好)

# 典型的慢查询修复:
# 问题:SELECT * FROM orders WHERE user_id = 12345; → 全表扫描 50 万行
# 解决:ALTER TABLE orders ADD INDEX idx_user_id (user_id);
# 效果:50 万行扫描 → 3 行扫描,RT 从 2s 降到 5ms

锁等待

# MySQL:查看当前锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
SELECT * FROM information_schema.INNODB_TRX;

# 典型场景:大事务持有锁太久,其他请求排队等待
# 解决:缩小事务范围、避免在事务中调用外部 API

缓冲池命中率

# MySQL InnoDB Buffer Pool 命中率
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
# 命中率 = 1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) × 100%
# 健康值:> 99%
# 低于 95% 说明 buffer pool 太小,数据频繁从磁盘读取

主从复制延迟

# MySQL:查看从库延迟
SHOW SLAVE STATUS\G
# 关注:Seconds_Behind_Master
# 健康值:< 1s
# 超过 10s → 读写分离场景下用户可能读到旧数据

课堂练习

练习:分析一个数据库的健康状况

  1. 连接一个 MySQL 数据库
  2. 查看当前连接数和最大连接数
  3. 开启慢查询日志,执行一些查询后查看慢日志
  4. EXPLAIN 分析一条查询的执行计划
  5. 计算 InnoDB Buffer Pool 命中率

中间件层监控

Redis 监控

指标含义健康范围异常处理
used_memory已使用内存< maxmemory 的 80%检查大 Key、设置合理过期时间
keyspace_hits/misses缓存命中率> 95%低 → 缓存策略有问题
connected_clients当前客户端连接数< maxclients 的 80%连接泄漏 → 检查应用连接池配置
blocked_clients阻塞的客户端数0> 0 → 有 BLPOP 等阻塞操作
大 Key单个 Key 超过 10KB无大 Key拆分大 Key,避免 HGETALL 大 Hash
# Redis 实时监控
redis-cli INFO stats
redis-cli INFO memory

# 命中率计算
# hits / (hits + misses) × 100%

# 查找大 Key
redis-cli --bigkeys

# 实时命令监控(注意:生产环境慎用 MONITOR)
redis-cli MONITOR

消息队列监控(Kafka/RabbitMQ)

指标含义告警条件
消息堆积量(Lag)未消费的消息数持续增长 → 消费速度跟不上生产
消费速度每秒消费消息数低于生产速度 → 需要增加消费者
端到端延迟消息从生产到被消费的时间> 10s 需关注
消费者组状态Rebalancing/Stable频繁 Rebalance → 消费者不稳定

Nginx / 网关监控

指标含义告警条件
Active Connections当前活跃连接数接近 worker_connections 配置
Request Queue等待处理的请求数持续 > 0 → 后端处理不过来
5xx 错误数上游返回 5xx 的频率> 0 就需要排查
upstream_response_time后端实际处理时间和直接访问后端对比判断网关开销
# Nginx 启用 stub_status
# 在 nginx.conf 中添加:
# location /nginx_status {
#     stub_status on;
#     allow 127.0.0.1;
#     deny all;
# }

# 访问查看
curl http://localhost/nginx_status
# Active connections: 350
# server accepts handled requests
#  12456789 12456789 45678901
# Reading: 5 Writing: 120 Waiting: 225

Prometheus + Grafana

架构简介

被监控目标(Exporter) → Prometheus(采集+存储) → Grafana(展示+告警)

Prometheus 定时拉取(Pull)各个 Exporter 暴露的指标数据,存储在时序数据库中。Grafana 从 Prometheus 查询数据并以图表展示。

核心概念

概念说明示例
Metric(指标)被监控的数值http_requests_total
Label(标签)给指标加维度http_requests_total
Counter只增不减的计数器总请求数、总错误数
Gauge可增可减的瞬时值当前连接数、内存使用量
Histogram数据分布(自动计算分位数)请求延迟分布
PromQLPrometheus 查询语言用于查询和聚合指标

常用 PromQL 查询

# QPS(最近 5 分钟的请求速率)
rate(http_requests_total[5m])

# 按接口分组的 QPS
sum(rate(http_requests_total[5m])) by (path)

# P95 延迟
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

# 错误率
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m])) * 100

# CPU 使用率
100 - (avg(irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

# 内存使用率
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

常用 Exporter

Exporter监控对象
Node ExporterLinux 操作系统(CPU/内存/磁盘/网络)
MySQL ExporterMySQL(连接数/查询/缓冲池)
Redis ExporterRedis(内存/命中率/连接数)
Nginx ExporterNginx(连接/请求/上游)
JMX ExporterJava 应用(线程/GC/堆内存)
cAdvisorDocker 容器(CPU/内存/网络)

推荐 Grafana 看板

看板 ID名称用途
1860Node Exporter FullLinux 系统全指标看板
7362MySQL OverviewMySQL 核心指标
763Redis DashboardRedis 核心指标
12708Nginx IngressNginx 核心指标

课堂练习

练习:搭建最小 Prometheus + Grafana 监控

  1. 用 Docker Compose 启动 Prometheus + Grafana + Node Exporter
  2. 配置 Prometheus 抓取 Node Exporter
  3. 在 Grafana 导入看板 ID 1860
  4. 执行一次压测,观察 Grafana 上 CPU/内存/网络的实时变化 提示:Docker Compose 是最快的搭建方式,一个 YAML 文件搞定。

瓶颈定位方法论

5 层定位法

从外到内、逐层排查:

第1层:压测工具 → 第2层:操作系统 → 第3层:应用层 → 第4层:数据库 → 第5层:外部依赖

第1层:看压测工具(客户端视角)

症状初步判断
RT 缓慢上升服务端处理能力饱和
QPS 达到上限不再增长有某个资源是瓶颈
大量 Connection Refused连接数上限(Nginx/应用/DB)
大量 Timeout后端处理超时或外部调用超时
大量 5xx 错误应用抛异常/OOM/进程被 Kill

第2层:看操作系统

症状判断工具
CPU > 80%(user 高)应用计算密集 → 优化代码或扩容top / htop
CPU > 80%(iowait 高)磁盘 IO 瓶颈 → 查慢查询/日志vmstat / iostat
内存持续增长内存泄漏free -h 持续观察
Swap 被使用物理内存不够了free -h
网络带宽打满响应体太大/连接数太多iftop / sar -n
大量 TIME_WAIT短连接太多 → 启用连接复用ss -s

第3层:看应用层

症状判断工具
线程全部 BLOCKED/WAITING锁竞争或等待外部 IOjstack / pprof
连接池耗尽下游处理慢,连接等待超时应用日志/监控
Full GC 频繁内存不够/内存泄漏GC 日志 / jstat
请求队列堆积处理速度跟不上请求速度Tomcat/Netty 监控

第4层:看数据库

症状判断工具
慢查询多缺索引/SQL 不优慢查询日志 + EXPLAIN
锁等待大事务/热点行SHOW PROCESSLIST
连接数打满连接池太大或泄漏SHOW STATUS
主从延迟主库写压力大SHOW SLAVE STATUS

第5层:看外部依赖

症状判断处理方式
第三方 API 响应慢对方服务端问题加超时控制 + 降级策略
第三方返回 429触发限流加队列缓冲 / 协商配额
DNS 解析慢DNS 服务器问题配置本地 DNS 缓存

常见误判案例

💡 误判1:CPU 不高但性能差

**现象:**200 并发时 QPS 不涨了,CPU 只有 40%,内存充足。 **原因:**线程全在等待 IO(慢查询/外部 API/锁等待)。CPU 不忙是因为在"等"而不是在"算"。 **排查:**用 jstack 看线程状态 → 发现 80% 线程在 WAITING 状态等数据库返回。 **解决:**优化慢查询 + 减少外部调用 + 改为异步非阻塞。

💡 误判2:加 CPU 不管用

**现象:**从 4 核升级到 16 核,QPS 只提升了 10%。 **原因:**瓶颈不在 CPU。可能在数据库(锁竞争)、网络(带宽不够)、或应用层(全局锁/单线程处理)。 **教训:**先定位瓶颈再扩容,否则花了钱没效果。

💡 误判3:错误率低但用户投诉体验差

**现象:**错误率 0.01%,看起来完美。但用户投诉"很卡"。 **原因:**平均 RT 正常(200ms),但 P99 高达 8 秒。1% 的用户每次等 8 秒。 **教训:**看 P95/P99,不看平均值。少数用户的极端体验也很重要。

定位工具

工具用途使用场景
火焰图(Flame Graph)可视化 CPU 时间分布找到代码中哪个函数最耗 CPU
慢查询日志记录执行时间超过阈值的 SQL数据库瓶颈定位
Thread Dump(jstack)查看所有线程的调用栈找锁竞争、线程阻塞
Heap Dump(jmap)内存快照分析内存泄漏定位
pprof(Go)CPU/内存/阻塞分析Go 应用的全方位性能分析
py-spy(Python)采样式 CPU 分析Python 应用无侵入式性能分析
Arthas(Java)实时诊断工具在线定位 Java 应用问题
# 生成 Java 火焰图
# 使用 async-profiler
./asprof -d 30 -f flamegraph.html <pid>

# Go pprof
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Python py-spy
py-spy record -o flamegraph.svg --pid <pid>

常见性能问题清单

应用层常见问题(20条)

#问题症状解决方案
1N+1 查询一个列表页执行几百条 SQL使用 JOIN 查询或批量查询
2连接池太小请求排队等连接、超时增多调大连接池并监控使用率
3同步阻塞调用线程在等待外部 IO改为异步非阻塞
4没有缓存每次请求都查数据库加 Redis 缓存热点数据
5缓存穿透查不存在的数据每次打到 DB布隆过滤器 / 缓存空值
6缓存雪崩大量缓存同时过期随机化过期时间
7缓存击穿热点 Key 过期后大量请求打到 DB互斥锁 / 永不过期 + 异步更新
8内存泄漏内存持续增长直到 OOMHeap Dump 分析、修复泄漏点
9频繁 Full GC周期性卡顿调整 JVM 参数、减少对象创建
10日志写太多磁盘 IO 高、磁盘空间不足降低日志级别、异步写日志
11序列化/反序列化慢JSON 解析占大量 CPU换高性能库(Jackson → fastjson2)
12正则表达式灾难性回溯特定输入导致 CPU 100%优化正则或限制输入长度
13全局锁/同步块过大并发提升但 QPS 不涨缩小同步范围、使用无锁数据结构
14线程池满请求被拒绝(RejectedExecution)调大线程池 / 加队列缓冲
15大对象传输带宽高、RT 长分页、压缩、按需加载
16没有超时控制下游慢时级联阻塞设置合理的超时 + 熔断
17重复计算CPU 高,同样的计算做多次结果缓存 / Memoization
18连接泄漏CLOSE_WAIT 持续增长确保 finally 中关闭连接
19无限循环 / 死循环单个请求 CPU 100%代码审查、加循环上限
20依赖服务无降级一个下游挂了整个系统跟着挂熔断器 + 降级策略

数据库层常见问题(15条)

#问题症状解决方案
1缺少索引全表扫描、慢查询分析 EXPLAIN 加索引
2索引失效有索引但没用上避免函数包裹列、隐式类型转换
3大事务锁持有时间长拆分事务、减少事务内操作
4热点行竞争大量请求更新同一行乐观锁 / 分桶计数
5SELECT *返回无用列浪费带宽和内存只查需要的列
6大表无分区数据量上亿后查询极慢按时间/ID 分表分区
7连接数不够Connection Refused加大 max_connections、优化连接池
8Buffer Pool 太小频繁磁盘读取调大 innodb_buffer_pool_size
9深分页(OFFSET 大)OFFSET 10000 时很慢改用游标分页 WHERE id > last_id
10子查询代替 JOIN子查询性能远差于 JOIN改写为 JOIN
11主从延迟高从库读到旧数据写后读走主库、减少主库压力
12临时表/文件排序Using temporary; Using filesort优化 ORDER BY 走索引
13死锁两个事务相互等待统一加锁顺序、缩小事务
14binlog 过大磁盘打满调整 expire_logs_days
15未参数化查询每次都要 hard parse使用 Prepared Statement

网络层常见问题(10条)

#问题症状解决方案
1带宽不足传输速率到顶、RT 变长升级带宽 / 压缩响应
2DNS 解析慢首次请求特别慢本地 DNS 缓存 / 用 IP 直连
3SSL 握手开销HTTPS 首次连接慢启用 TLS Session 复用
4大量短连接TIME_WAIT 堆积启用 Keep-Alive 长连接
5跨区域调用延迟高(50~200ms RTT)就近部署 / CDN
6TCP 丢包重传偶发性延迟飙升检查网络质量
7端口耗尽Cannot assign requested address调大端口范围 / 连接复用
8Nginx worker_connections 不够502 Bad Gateway增大 worker_connections
9负载均衡不均部分节点 CPU 高,部分空闲调整负载均衡算法
10防火墙/安全组限制连接被拒检查安全组端口开放

调优策略与验证

调优的正确流程

①测 → ②找瓶颈 → ③制定方案 → ④改 → ⑤再测 → ⑥对比

⚠️ 调优的第一条铁律:先证明有瓶颈,再优化

"感觉可能慢"不是优化理由。没有数据支撑的优化 = 瞎改。先压测拿到数据,证明确实有问题,再动手。

调优手段分类

类别手段成本效果
代码优化修复 N+1 查询可能提升 10 倍
加缓存低~中减少 DB 压力 80%+
优化算法复杂度视情况而定
改同步为异步中~高并发能力翻倍
配置调优调大连接池极低消除连接等待
调整 JVM/GC 参数减少 GC 停顿
调整数据库参数提升缓冲池命中率
架构优化读写分离读性能翻倍
微服务拆分独立扩容各组件
加消息队列削峰平滑突发流量
硬件升级垂直扩容(加 CPU/内存)有上限(Amdahl 定律)
水平扩容(加机器)线性提升(如果无共享瓶颈)

如何验证调优效果(控制变量法)

📖 控制变量法三步走

  1. **固定环境:**同一台机器、同一份数据、同一份压测脚本
  2. **只改一个变量:**每次只改一处(如只加索引),不要同时改多处
  3. **多次测取均值:**至少跑 3 次取平均值,避免偶发因素干扰
示例

调优效果验证示例

优化项:给 orders 表的 user_id 加索引

测试条件(完全一致):
  → 100 并发、持续 5 分钟、阶梯式加压
  → 80% 查询 + 20% 写入
  → 测试数据:100 万条订单

优化前(3次平均):
  → QPS: 320 | P95: 1,200ms | P99: 3,500ms | 错误率: 0.8%
  → 慢查询数: 456/min | CPU: 45% | iowait: 35%

优化后(3次平均):
  → QPS: 1,150 | P95: 180ms | P99: 450ms | 错误率: 0.01%
  → 慢查询数: 2/min | CPU: 62% | iowait: 5%

结论:
  → QPS 提升 259%
  → P95 降低 85%
  → 慢查询减少 99.6%
  → iowait 从 35% 降到 5%(磁盘 IO 瓶颈消除)
  → CPU 从 45% 升到 62%(说明以前 CPU 在等 IO,现在真正干活了)

课堂练习

练习:完成一次完整的调优验证

  1. 选择一个有数据库查询的接口
  2. EXPLAIN 检查是否有全表扫描
  3. 压测记录优化前的 QPS 和 P95(至少 3 次取均值)
  4. 加索引后,用完全相同的条件再压测 3 次
  5. 对比优化前后数据,计算改善百分比

测试数据管理

数据量要接近真实

⚠️ 测试数据量是最容易被忽略的"失真因素"

生产数据库 100 万条记录 → 测试环境只有 100 条 → 测试结果毫无意义。因为索引在小数据量下几乎不产生差异,分页在小数据量下不会触发全表扫描。

数据类型推荐量级说明
用户表和生产同量级,或至少 10 万+模拟真实的登录/查询场景
业务数据(订单等)至少 100 万条大表查询的性能差异在百万级才体现
搜索索引和生产同量级Elasticsearch/向量数据库的性能和数据量强相关
文件/图片模拟真实大小1KB 的假图片和 5MB 的真图片差别很大

数据脱敏方法

字段类型脱敏方式示例
姓名随机中文名生成张三 → 李明华
手机号保留前 3 后 4 中间随机138****1234
身份证保留地区码,后面随机110101199001011234 → 110101199503076789
邮箱用户名随机化test_user_001@example.com
地址用模板生成XX省XX市XX区XX路XX号
密码统一设为测试密码Test@123456

参数化数据准备

# 生成测试用户数据(Python 示例)
import json
import random
import string

users = []
for i in range(10000):
    users.append({
        "username": f"testuser_{i:05d}",
        "password": "Test@123456",
        "email": f"testuser_{i:05d}@example.com",
        "phone": f"138{random.randint(10000000, 99999999)}"
    })

with open("test-users.json", "w") as f:
    json.dump(users, f, ensure_ascii=False, indent=2)

# 生成搜索关键词数据(覆盖各种长度和类型)
keywords = ["手机", "iPhone 15", "笔记本电脑", "机械键盘", "无线耳机",
            "运动鞋 男", "连衣裙 夏季", "儿童书包", "厨房置物架", "蓝牙音箱"]

数据隔离与清理

  • **压测数据标记:**给压测创建的数据加前缀(如 perf_test_),压测后方便批量清理
  • **使用独立数据库:**压测环境用独立的数据库实例,不和开发/测试环境共享
  • **每轮压测前重置:**确保每次压测的初始数据状态一致,结果才可对比
  • **自动化清理脚本:**压测结束后自动清理测试数据,避免积累

分布式压测

什么时候需要分布式压测?

📖 判断标准:压测机自己是不是瓶颈

如果压测时发现压测机本身的 CPU > 80%、内存不够、或网络带宽打满了 → 说明瓶颈在压测机,需要多台机器分布式发压。

各工具的分布式方案

工具分布式方式注意事项
JMeterMaster-Slave 模式:一台 Master 控制多台 Slave需要配置 remote_hosts ,数据要分发到每台 Slave
k6k6 Cloud(付费)或自建 k6-operator(K8s)开源版单机就很强(Go 语言优势)
LocustMaster-Worker 模式: locust --master + locust --workerWorker 可以在不同机器上启动
# Locust 分布式示例
# 主节点(控制台 + Web UI)
locust -f locustfile.py --master --expect-workers 3

# 工作节点1(另一台机器)
locust -f locustfile.py --worker --master-host 192.168.1.100

# 工作节点2
locust -f locustfile.py --worker --master-host 192.168.1.100

# 工作节点3
locust -f locustfile.py --worker --master-host 192.168.1.100
# JMeter 分布式示例
# 1. 在每台 Slave 上启动 JMeter Server
cd apache-jmeter/bin
./jmeter-server

# 2. 在 Master 上配置 jmeter.properties
# remote_hosts=192.168.1.201,192.168.1.202,192.168.1.203

# 3. Master 启动远程测试
./jmeter -n -t test.jmx -r -l result.jtl
# -r 表示启用所有远程 hosts

分布式压测注意事项

  • **时间同步:**所有压测机的时钟要同步(NTP),否则数据对不上
  • **数据分片:**参数化数据要按 Worker 分片,不能所有 Worker 用同一份(否则所有 Worker 用同一个用户登录)
  • **结果汇总:**最终要把所有 Worker 的结果合并成一份总报告
  • **网络一致:**所有压测机应该在同一网络,到被测服务的延迟要一致
  • **压测机监控:**别忘了监控压测机本身,确认瓶颈不在压测端

测试脚本关键概念

思考时间(Think Time)

📖 思考时间 = 模拟真实用户在两次操作之间的停顿

真实用户浏览页面需要时间、阅读内容需要时间、填写表单需要时间。如果不加思考时间,压测结果会严重偏悲观。

场景推荐思考时间
页面浏览3~10 秒(用户在看内容)
表单填写10~30 秒(输入信息)
搜索后浏览结果2~5 秒
API 自动化调用(无 UI)0~1 秒(模拟系统间调用)
// k6 中加思考时间
import { sleep } from 'k6';

export default function () {
  http.get('https://example.com/page');
  sleep(Math.random() * 7 + 3); // 3~10 秒随机
  http.get('https://example.com/next');
}

# Locust 中加思考时间
class MyUser(HttpUser):
    wait_time = between(3, 10)  # 每次 task 之间等 3~10 秒

Ramp-up(加压策略)

📖 Ramp-up = 并发用户数如何从 0 增长到目标值

策略说明适用场景
阶梯式10 → 保持5分钟 → 50 → 保持5分钟 → ...找拐点(推荐)
斜坡式从 0 线性增长到 500(10分钟内)观察 RT 变化趋势
脉冲式低 → 瞬间拉高 → 降低 → 再拉高模拟秒杀/推送
恒定式直接固定在目标并发稳定性测试
// k6 阶梯式加压
export const options = {
  stages: [
    { duration: '2m', target: 50 },    // 阶段1:加到50
    { duration: '5m', target: 50 },    // 保持50
    { duration: '2m', target: 200 },   // 阶段2:加到200
    { duration: '5m', target: 200 },   // 保持200
    { duration: '2m', target: 500 },   // 阶段3:加到500
    { duration: '5m', target: 500 },   // 保持500
    { duration: '3m', target: 0 },     // 冷却
  ],
};

参数化(Parameterization)

💡 为什么需要参数化?

所有用户查同一个商品 ID → 缓存命中率 100% → RT 50ms。真实场景每个人查不同的商品 → 缓存命中率 30% → RT 可能 500ms。参数化让测试更接近真实。

// k6 参数化示例
import { SharedArray } from 'k6/data';

const products = new SharedArray('products', function () {
  return JSON.parse(open('./product-ids.json'));
  // [1001, 1002, 1003, ..., 9999]
});

export default function () {
  const productId = products[Math.floor(Math.random() * products.length)];
  http.get(`https://api.example.com/product/${productId}`);
}

关联(Correlation)

从上一个响应中提取数据,传给下一个请求。典型场景:登录返回 Token → 后续请求带 Token。

// k6 关联示例
export default function () {
  // Step 1: 登录,获取 token
  const loginRes = http.post('https://api.example.com/login', JSON.stringify({
    username: 'testuser', password: 'testpass',
  }), { headers: { 'Content-Type': 'application/json' } });

  const token = loginRes.json('data.token');  // 提取 token

  // Step 2: 用 token 访问
  http.get('https://api.example.com/profile', {
    headers: { Authorization: `Bearer ${token}` },
  });
}

预热与冷却(Warm-up / Cool-down)

预热(前 1~2 分钟数据不计入结果):

  • JIT 编译还没完成(Java/Go 前几秒性能偏低)
  • 缓存还没建立(Redis/DB Buffer Pool 是冷的)
  • 连接池还在预热 冷却(最后 1 分钟数据不计入结果):
  • 正在减少并发,数据不稳定
  • 有些请求是"尾巴",不代表稳态性能

测试报告模板

完整报告结构

TIP

一、测试概述
  1.1 测试目的
  1.2 测试范围
  1.3 测试时间
  1.4 测试环境配置
      - 服务器配置(CPU/内存/磁盘/OS)
      - 数据库配置
      - 中间件配置
      - 网络拓扑
  1.5 测试工具及版本
  1.6 测试数据说明

二、测试场景与策略
  2.1 场景列表(场景描述 + 接口 + 流量占比)
  2.2 加压策略(并发数 + 持续时间)
  2.3 通过标准(QPS/RT/错误率的目标值)

三、测试结果
  3.1 各场景汇总表
      场景 | QPS | P50 | P95 | P99 | 错误率 | 是否达标
  3.2 核心接口详细数据
  3.3 阶梯式加压数据(并发 vs QPS vs RT 曲线图)
  3.4 服务端资源使用(CPU/内存/磁盘IO/网络 曲线图)
  3.5 数据库指标(连接数/慢查询/缓冲命中率)

四、性能拐点分析
  4.1 拐点并发数
  4.2 系统天花板
  4.3 拐点时的资源瓶颈

五、稳定性测试结果
  5.1 测试时长
  5.2 内存变化曲线
  5.3 GC 情况
  5.4 异常事件

六、瓶颈分析
  6.1 已发现的瓶颈
  6.2 瓶颈证据(截图/日志/监控数据)
  6.3 已执行的优化及效果

七、结论与建议
  7.1 是否满足上线标准(逐条对照通过标准)
  7.2 优化建议(优先级排序)
  7.3 扩容建议
  7.4 风险提示

八、附录
  8.1 测试脚本
  8.2 原始数据
  8.3 监控截图

如何写好"结论与建议"

💡 结论不是列数据,而是回答业务问题

示例

差的结论: "QPS 达到 2000,P95 为 450ms,错误率 0.1%。以上。" 好的结论:

结论:系统满足上线要求,建议按计划发布。

详细说明:
  ✅ QPS 达到 2,000(目标 1,500),有 33% 余量
  ✅ P95 = 450ms(目标 < 800ms),达标
  ✅ 错误率 0.1%(目标 < 0.5%),达标
  ✅ 8 小时稳定性测试无异常

但存在以下风险:
  ⚠️ 当并发超过 2,500 时,订单创建接口 P99 飙升到 5s
     → 原因:order 表 user_id+created_at 联合索引缺失
     → 建议:上线前加索引(预计提升 3 倍)
     → 优先级:高

  ⚠️ Redis 在高并发下内存增长较快(8 小时增长了 500MB)
     → 原因:部分缓存没设过期时间
     → 建议:所有缓存加 TTL
     → 优先级:中

扩容建议:
  → 如果日活超过 50 万,建议应用节点从 3 台扩到 5 台
  → 如果日活超过 100 万,建议数据库做读写分离

课堂练习

练习:写一份完整的性能测试报告

  1. 按照上面的模板结构,为你最近做的一次压测撰写报告
  2. 重点练习"结论与建议"部分:不只列数据,要有分析
  3. 每条建议要包含:问题是什么 → 原因是什么 → 建议做什么 → 优先级
  4. 让一位不了解技术细节的人(如产品经理)看看,他能看懂结论吗?

补充参考答案要点

  • 通用篇练习要能把性能问题分成客户端、服务端、数据库、中间件和网络几层来分析。
  • 压测方案答案应包含流量模型、数据准备、监控指标和瓶颈定位步骤,而不是只写线程数。
  • 如果要给出发布门禁,至少要说明核心 SLA、回归阈值和失败时的回滚标准。