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 用“默认隔离”而不是“默认放行”,丢了标记的请求应当被拒绝写入而不是默默写进真表;③ 压测后抽样核对生产表的写入来源,发现漏标记立刻停压。

关联