别等系统被压垮了,才知道容量到底有多大
我是 Echo_Wish。
做运维久了,你会发现一个挺有意思的现象:
很多系统都有容量指标,但真正到了极限的时候,没人知道它到底能扛多少。
文档里写着:
最大支持 10 万 QPS。
监控面板里写着:
CPU 最高 70%。
架构图上写着:
Redis 集群、MySQL 主从、消息队列、K8s 自动扩容,一应俱全。
看起来很稳。
可一旦真的来了流量洪峰,可能第一件事不是扩容,而是:
CPU 直接干到 100%,数据库连接池耗尽,Redis 开始抖,消息队列堆积,接口响应时间从 200ms 变成 20 秒。
然后大家开始问:
“为什么之前压测明明没问题?”
这时候往往才发现:
我们测试的是系统“能不能跑”,而不是系统“跑到哪里会撑不住”。
所以我越来越觉得,容量测试不能只盯着传统压测。
有时候,真正应该做的是:
用混沌工程主动把系统推向边界,甚至故意把它推过边界。
这就是我今天想聊的——容量边界测试,也可以理解成一次黑天鹅预演。
一、容量测试最大的误区:测出一个数字,就以为知道容量了
先说一个特别常见的场景。
某个订单接口,压测结果如下:
QPS CPU RT 错误率
-----------------------------------------
2000 35% 80ms 0%
4000 48% 95ms 0%
6000 63% 120ms 0%
8000 76% 180ms 0%
10000 88% 350ms 0%
于是测试报告写:
系统最大支持 10000 QPS。
看起来非常专业。
但问题来了:
为什么是 10000?
是因为测试到这里结束了?
还是因为 10000 已经是系统真正的极限?
如果继续压到 12000、15000、20000,会发生什么?
没人知道。
更麻烦的是,生产环境里的流量从来不是这么优雅的。
正常情况下可能是:
3000
3200
3500
3300
突然某一天:
3500
4800
7200
11000
16000
这时候你会发现,系统最重要的不是:
“平时能处理多少请求?”
而是:
“从什么时候开始,它会明显变差?”
这两个问题完全不是一回事。
二、真正应该找的,是“容量悬崖”
我特别喜欢把容量问题理解成一条曲线。
刚开始流量增加的时候,系统可能非常从容。
例如:
QPS RT
----------------
1000 50ms
2000 60ms
3000 70ms
4000 85ms
5000 100ms
6000 130ms
7000 180ms
8000 300ms
9000 800ms
10000 3000ms
你会发现一个非常明显的现象:
系统不是到了某个数字以后突然挂掉,而是先进入一个越来越危险的区域。
7000 QPS的时候,也许还可以正常工作。
8000 QPS开始出现明显延迟。
9000 QPS已经非常难受。
10000 QPS直接雪崩。
这中间存在一个非常关键的东西:
容量悬崖。
也就是:
再增加一点负载,系统性能突然发生断崖式下降。
而混沌工程真正有价值的地方,就是帮我们主动找到这条悬崖在哪里。
三、混沌工程不是“把生产环境搞挂”
很多人一听混沌工程,第一反应就是:
“这不就是故意搞事情吗?”
其实不是。
混沌工程真正的核心思想,我觉得可以用一句特别接地气的话解释:
平时主动找问题,总比事故的时候被问题找上门强。
传统压测更多关注:
“系统在正常压力下表现怎么样?”
混沌工程关注的是:
“如果突然发生一些我们预料不到的事情,系统还能不能扛住?”
比如:
- 流量突然增加 2 倍
- 流量突然增加 5 倍
- 某个节点 CPU 持续 100%
- 数据库连接池耗尽
- Redis 响应变慢
- 消息队列大量堆积
- 下游接口响应时间突然增加
- 某个服务实例大量重启
- 网络延迟突然增加
这些事情平时可能觉得:
“应该不会发生吧。”
但黑天鹅最大的特点就是:
你觉得它不会发生,它偏偏可能发生。
所以容量边界测试,其实就是在问:
如果今天真的发生了,我的系统会怎么死?
四、第一步:别急着压,先定义“什么叫撑不住”
这是很多测试容易忽略的地方。
如果你只是不断增加 QPS,然后看服务器什么时候 CPU 100%,其实意义并没有那么大。
因为:
CPU 100% ≠ 系统不可用。
有些服务 CPU 90% 还能稳定跑。
有些服务 CPU 60%,数据库已经开始排队。
所以容量边界必须有明确的业务指标。
比如:
成功率 < 99.9%
P95 > 500ms
P99 > 2s
数据库连接池使用率 > 90%
消息队列堆积 > 10000
CPU > 90% 持续 5 分钟
这些指标组合起来,才是我们的“红线”。
例如:
def is_capacity_boundary(metrics):
return (
metrics.success_rate < 0.999
or metrics.p95 > 0.5
or metrics.p99 > 2
or metrics.db_pool_usage > 0.9
)
这时候测试的目标就很清晰了:
不是把服务器打爆,而是找到业务开始不可接受的那个点。
五、第二步:不要一步干到最大,要逐步加压
容量边界测试最忌讳:
一上来直接 5 倍流量。
这样虽然很刺激,但得到的信息其实很少。
你只知道:
“系统挂了。”
但你不知道:
“它到底从什么时候开始不行?”
更合理的方式是阶梯式加压。
比如:
1000 QPS
↓
2000 QPS
↓
3000 QPS
↓
4000 QPS
↓
5000 QPS
↓
6000 QPS
↓
7000 QPS
↓
8000 QPS
每一个阶段持续一段时间。
同时观察:
CPU
Memory
GC
线程池
连接池
RT
P95
P99
错误率
Redis
DB
MQ
然后继续增加。
甚至可以把压测程序写成自动探测:
qps = 1000
while True:
result = run_test(qps)
print(
f"QPS={qps}, "
f"P95={result.p95}, "
f"P99={result.p99}, "
f"Error={result.error_rate}"
)
if is_capacity_boundary(result):
print(f"发现容量边界:{qps} QPS")
break
qps += 500
这样做以后,测试就不再是:
“压一下看看。”
而变成了:
“自动寻找系统容量边界。”
这才有点混沌工程的味道。
六、第三步:真正有意思的,是给系统增加“黑天鹅”
如果只做阶梯式 QPS 压测,其实还不够。
因为生产环境最危险的情况,往往不是单纯流量增加。
而是:
流量增加 + 某个依赖同时变慢。
例如:
正常:
用户请求
↓
订单服务
↓
库存服务
↓
数据库
平时一切正常。
但是突然:
用户流量 × 3
↓
订单服务
↓
库存服务响应时间 × 5
↓
线程大量等待
↓
连接池被占满
↓
请求开始排队
↓
超时
↓
重试
↓
流量进一步增加
↓
系统雪崩
这才是现实世界里非常典型的事故链。
所以容量边界测试真正应该做的是:
负载 + 故障组合。
例如:
experiment = {
"traffic": {
"qps": 8000
},
"dependency": {
"inventory_api_latency": 2000
},
"duration": 300
}
然后观察:
8000 QPS + 下游延迟 2 秒的时候,系统还能不能活?
如果还能活,再测试:
8000 QPS + 下游延迟 5 秒
8000 QPS + Redis 延迟
8000 QPS + DB连接池限制
10000 QPS + 下游异常
这时候你测到的,才越来越接近真正的生产事故。
七、还有一个特别容易被忽略的问题:恢复能力
我一直认为:
容量测试不能只测试“什么时候挂”,还必须测试“挂了以后能不能回来”。
比如:
10:00 正常
10:05 流量增加
10:10 达到容量边界
10:12 开始大量超时
10:15 降低流量
然后问题来了:
10:20系统恢复了吗?
有时候并没有。
因为系统已经产生了:
MQ堆积
线程堆积
数据库慢查询
连接池耗尽
缓存击穿
大量重试
任务积压
流量恢复正常以后,这些历史债务还在那里。
于是出现一种特别经典的情况:
流量已经恢复了,系统为什么还没恢复?
这就是容量测试中非常值得关注的:
恢复边界。
可以进一步定义:
def recovery_check(metrics):
return (
metrics.error_rate < 0.001
and metrics.p95 < 0.5
and metrics.queue_depth < 100
)
最终你应该得到的不只是:
最大容量 8000 QPS。
而应该是:
稳定容量:6000 QPS
风险区间:6000 ~ 7500 QPS
容量边界:约 7500 QPS
超过边界后的主要故障:
- DB连接池耗尽
- P99急剧上升
- MQ开始堆积
恢复时间:
流量恢复后约 8 分钟
这份结果对生产运维的价值,比单纯写一句“最大支持 8000 QPS”大得多。
八、最终要算的,其实不是“最大容量”,而是“安全容量”
这里我特别想强调一个观点。
生产环境不应该把系统跑在理论最大容量附近。
如果你的测试结果是:
最大容量:10000 QPS
然后生产环境规划:
长期运行:9500 QPS
我只能说:
胆子挺大。
因为生产环境还有很多你测试时没有考虑到的变量:
- 突发流量
- 网络抖动
- GC
- 数据库慢查询
- 第三方接口异常
- 节点故障
- 发布
- 定时任务
- 批处理任务
- 流量分布变化
所以我更推荐一个简单的容量模型:
安全容量 = 理论边界 × 安全系数
例如:
capacity = 10000
safety_factor = 0.7
safe_capacity = capacity * safety_factor
print(f"建议生产安全容量:{safe_capacity:.0f} QPS")
最后得到:
理论边界:10000 QPS
建议安全容量:7000 QPS
这样即使突然来一波流量,也还有喘息空间。
九、我更希望看到的,是一张“系统死亡地图”
如果让我设计一套完整的容量边界测试,我不会只要一份压测报告。
我更想要这样一张东西:
流量增加
↑
│
稳定区 │
│
┌──────────────┐
│ │
│ 正常运行 │
│ │
└──────┬───────┘
│
↓
性能下降区
│
P99开始上涨
DB开始排队
MQ开始堆积
│
↓
容量危险区
│
超时 / 重试
连接池耗尽
│
↓
系统雪崩区
然后再给每一个区域标注:
安全容量:6000 QPS
预警容量:7000 QPS
危险容量:7500 QPS
理论极限:8000 QPS
恢复时间:8分钟
第一故障点:数据库连接池
第二故障点:线程池
第三故障点:MQ堆积
这时候运维人员才真正知道:
这个系统到底能跑到哪里。
十、真正的黑天鹅预演,不是预测事故,而是提前认识自己的脆弱
最后说点我的感受。
很多公司做容量规划的时候,特别喜欢问:
“我们的系统能支撑多少用户?”
但我觉得这个问题本身就有点问题。
因为用户数并不能直接代表系统压力。
真正应该问的是:
“当压力超过设计能力以后,我们的系统会以什么方式失败?”
这是两个完全不同的问题。
系统不可能永远不出问题。
再牛的架构,也有容量上限。
再完善的高可用,也不可能覆盖所有故障组合。
真正成熟的运维,不应该追求:
“我们的系统绝对不会挂。”
而应该追求:
“我知道它什么时候会开始出问题,也知道它最先从哪里开始出问题,更知道出问题以后多久能恢复。”
这才是混沌工程真正有价值的地方。
我们不是为了把系统搞挂,而是趁它还没有真的挂之前,先问一遍:
如果今天真的来了一只黑天鹅,我们扛得住吗?
如果不知道答案,那就别等事故告诉你。
自己先把它压到边界。
因为生产事故最贵的地方,从来不是服务器挂了。
而是:
你明明有机会提前知道,却选择了相信“应该不会发生”。
这可能才是容量治理里,最值得我们警惕的一句话。