57.1 压测方法论
一句话
压测的结论只有一句有用:“在多少并发下,P99 是多少、错误率是多少”——而并发怎么给,取决于你用“闭环”还是“开环”模型:闭环会自我限速,开环才能压出真实过载。
两种模型
| 模型 | 怎么发压 | 行为 | 适合 |
|---|---|---|---|
| 闭环(closed) | 固定 N 个并发用户,响应完再发下一个 | 服务变慢 → 发压变慢,永远不会过载 | 模拟“真实用户数” |
| 开环(open) | 固定速率发(如每秒 1000 次),不等响应 | 服务变慢 → 请求堆积,能压出崩溃点 | 模拟“流量洪峰” |
🔑 只做闭环压测,你会得到“我们 QPS 能到 5000”的假结论——那只是“N 个用户 × 每用户速率”,一旦真实流量是无情的开环,系统会在同样的 QPS 下崩掉。
代码:并发、QPS 与延迟的关系
package main
import "fmt"
func main() {
fmt.Println("=== Little 定律:并发 = QPS × 延迟 ===")
for _, qps := range []float64{1000, 5000, 10000} {
for _, latMs := range []float64{10, 50, 200} {
conc := qps * latMs / 1000
fmt.Printf("QPS %-6.0f × 延迟 %-5.0fms → 需要并发 %.0f\n", qps, latMs, conc)
}
}
fmt.Println("\n=== 闭环压测的天花板 ===")
const users = 200.0
fmt.Printf("%-12s %-14s %-14s\n", "实际延迟", "闭环可达QPS", "说明")
for _, latMs := range []float64{10, 50, 100, 200} {
qps := users / (latMs / 1000)
note := ""
if qps < 5000 {
note = "被并发数卡住,不是系统上限"
}
fmt.Printf("%-12.0fms %-14.0f %s\n", latMs, qps, note)
}
fmt.Printf("\n结论:200 并发 + 50ms 延迟 → 最多 %.0f QPS;想压到 1 万 QPS 必须提高并发或用开环模型\n",
users/0.05)
}
本机实测(macOS,Apple M1,Go 1.27):
=== Little 定律:并发 = QPS × 延迟 ===
QPS 1000 × 延迟 10 ms → 需要并发 10
QPS 1000 × 延迟 50 ms → 需要并发 50
QPS 1000 × 延迟 200 ms → 需要并发 200
QPS 5000 × 延迟 10 ms → 需要并发 50
QPS 5000 × 延迟 50 ms → 需要并发 250
QPS 5000 × 延迟 200 ms → 需要并发 1000
QPS 10000 × 延迟 10 ms → 需要并发 100
QPS 10000 × 延迟 50 ms → 需要并发 500
QPS 10000 × 延迟 200 ms → 需要并发 2000
=== 闭环压测的天花板 ===
实际延迟 闭环可达QPS 说明
10 ms 20000
50 ms 4000 被并发数卡住,不是系统上限
100 ms 2000 被并发数卡住,不是系统上限
200 ms 1000 被并发数卡住,不是系统上限
结论:200 并发 + 50ms 延迟 → 最多 4000 QPS;想压到 1 万 QPS 必须提高并发或用开环模型
读数:
| 目标 | 需要并发 |
|---|---|
| 1,000 QPS @ 50ms | 50 |
| 10,000 QPS @ 50ms | 500 |
| 10,000 QPS @ 200ms | 2,000 |
🔑 两个必看指标:P99 延迟(不是平均值)与错误率随 QPS 的拐点。拐点前的最大 QPS 才是容量,拐点后加并发只会让延迟暴涨、吞吐下降。
压测的五个必备设置
| 设置 | 为什么 |
|---|---|
| 数据量与生产同量级 | 小数据集走缓存,测不出真实 IO |
| 压测流量打真实链路(含网关、DB) | 只压应用层会漏掉瓶颈 |
| 预热 | JIT/连接池/缓存需要热身,否则前几分钟数据失真 |
| 观察下游 | 压测把 DB/缓存打挂了,应用指标看起来还行 |
| 有明确的“停手线” | P99 超阈值或错误率超阈值就停,别等雪崩 |
全链路压测:在线上压,怎么不污染线上
压测环境永远不是生产。「搭一套一样的测试环境 + 造数据 + JMeter 打同一个商品 ID」这套做法有三个致命失真:
| 做法 | 为什么失真 |
|---|---|
| 用测试环境 | 网络、数据量、外部依赖延迟都不同,瓶颈出现在最弱一环,结论迁移不过去 |
| 用模拟请求(同一个商品 ID) | 第一次请求回种缓存,后续全命中——测的是缓存,不是 DB |
| 从一台机器发压 | 压测机自己先到瓶颈,QPS 上不去,结论是“压测机容量”不是“系统容量” |
所以真压测要在生产压、用线上流量、分布式发压。代价是必须解决两件事:流量隔离与风险控制。标准架构是四个模块:
线上入口流量
│ 拷贝(访问日志 / GoReplay 劫持端口)
▼
流量数据工厂(清洗 → 打标记 → 存 HBase / MongoDB / S3)
│ 切分多份,下发到多个发压节点(异地/近用户,绝不同机房)
▼
发压节点 ──带标记的请求──> 生产链路 ──> 影子库(上行)
└─> Mock 下游(下行)
▲ │
└────── 监控熔断:超阈值自动降流量 ←──────┘
| 环节 | 具体做法 | 关键细节 |
|---|---|---|
| 流量生产 | 拷贝入口流量,清洗无效请求后落 NoSQL/S3;GoReplay 可劫持端口录制并回放 | 直接拷访问日志要自己写解析脚本,成本高;发压节点必须远离服务所在机房 |
| 染色 | 在 HTTP header 加标记(如 is-stress-test: 1),全链路透传 | 没有标记就没法做任何隔离,这是整套方案的地基 |
| 下行隔离 | 压测流量打到 Mock:行为日志不写、推荐服务不走真实召回 | Mock 要部署在真实服务同机房,否则测出来的网络延迟是假的 |
| 上行隔离 | 写请求进影子库:MySQL 用同实例不同 Schema 并导入线上数据、Redis 加统一 key 前缀、ES 用独立索引 | 只做 Mock 不做影子库,压测数据会污染报表和推荐结果 |
| 加压节奏 | 目标 20 万 QPS 就按每次 +1 万递增,每档稳定运行观察 | 出瓶颈先回退到上一档再扩容,别在崩着的系统上继续加压 |
| 自动熔断 | 预置阈值:容器 CPU 60%–70%、平均响应 1s、慢请求比例 1% | 触线自动降流量并告警,人不用盯着 |
🔑 全链路压测的价值不止“找瓶颈”:它同时产出容量数据(扩容该加几台)和预案演练场(低峰期压测时顺手降级几个服务,验证降级是否真的生效)。所以它是周期性动作,不是上线前的一次性关卡。
⚠️ 两个最容易被跳过、又最贵的环节:影子库(很多团队因为存储隔离做不了,最后退化成“固定几个账号压一压”,结论可信度极低)和染色透传(中间件、异步线程池、MQ 消费端只要有一处丢了 header,压测流量就漏进生产)。
反模式
| 反模式 | 后果 |
|---|---|
| 只做闭环压测就宣布容量 | 真实洪峰下崩得比预期早 |
| 只看平均延迟 | 平均值掩盖长尾,用户感知是 P99 |
| 压测数据只有几千行 | 缓存全命中,DB 压力测不出来 |
| 压测环境与生产配置不同 | 结论不可迁移 |
| 压测时不看下游 | 应用没事,DB 先挂了 |
| 没有基线,每次结果不可比 | 优化前后无法判断是否有效 |
| 生产压测不做染色,直接放大真实流量 | 压测数据写进真实表,报表、推荐、账单全被污染 |
| 压测时一步把流量拉到目标值 | 系统来不及暴露拐点,直接雪崩,还得回滚重来 |
自检
Q1 为什么闭环压测“永远不会把系统压垮”?
▸ 答案:因为闭环模型里每个虚拟用户必须等上一次响应才会发下一次请求。服务变慢 → 用户等待变长 → 发送速率自动下降,系统永远停留在“可持续”的状态。它测的是“N 个用户时的体验”,测不出“固定速率下的过载崩溃”。开环模型按固定速率发,服务变慢只会让队列变长,才能压出崩溃点。
Q2 为什么容量结论要看 P99 而不是平均延迟?
▸ 答案:因为用户感知与 SLO 都基于分位数。平均 50ms 可能意味着“90% 的请求 20ms、10% 的请求 300ms”,那 10% 的用户体验极差;而且排队系统的延迟分布是长尾的,平均值会严重低估尾部。容量规划要保证“P99 在阈值内”的最大 QPS,而不是“平均延迟好看”的 QPS。
Q3 压测时应用指标正常、但线上仍然崩,可能漏了什么?
▸ 答案:漏了下游与共享资源:数据库连接池、缓存带宽、消息队列积压、第三方接口限流、DNS、负载均衡连接数。压测时应用层可能因为缓存命中而很快,但真实流量下缓存 miss 会把压力打到 DB;或者压测只压了一个服务,线上是多个服务共享同一个 DB。所以压测必须观察“整条链路”,并至少压一次“缓存失效”的场景。
Q4 为什么 “压测环境与生产不一致” 会得出错误容量?
▸ 答案:因为容量瓶颈往往在最弱的一环:压测环境可能网络更好、数据量更小、没有真实的长尾查询与外部依赖延迟。测出来的 QPS 是 “理想链路” 的上限,生产一上量就撞到 “慢查询/连接池/带宽” 这些压测里没出现的瓶颈。
Q5 怎么设计一个 “能复现线上故障” 的压测?
▸ 答案:三条:按真实流量比例(读/写比、热点分布、长尾请求)、注入真实依赖延迟(下游慢、超时、重试)、跑够时长(覆盖 GC、连接老化、缓存失效)。只压 “平均请求” 的压测,几乎不可能复现线上那种 “偶发超时引发雪崩”。
Q6 用同一个商品 ID 压商品详情接口,为什么测出来的 QPS 偏高、而线上同 QPS 却扛不住?
▸ 答案:因为第一次请求把数据回种进了缓存,之后全部命中缓存,压测根本没打到 DB。线上真实流量访问的是不同商品,命中率按 Zipf 分布,总有一部分落到 DB。正确做法是回放线上真实流量的参数分布(或至少准备与生产同量级、同分布的数据集),并且专门压一轮“缓存冷启动/缓存失效”的场景。
Q7 压测流量染色在某一环丢了(比如 MQ 消费端),会发生什么?怎么防?
▸ 答案:压测流量会伪装成真实请求写进生产库——报表被灌水、推荐位被占、账单错乱,而且事后很难区分哪些是脏数据。防护是三层:① 染色信息随上下文透传(HTTP header → RPC 上下文 → MQ 消息属性 → 异步线程池),任何跨线程/跨进程都要显式传递;② 影子库与 Mock 用“默认隔离”而不是“默认放行”,丢了标记的请求应当被拒绝写入而不是默默写进真表;③ 压测后抽样核对生产表的写入来源,发现漏标记立刻停压。