主题
界面
主题
项目 B 的秒杀架构讲的是「一个场景怎么防」;本节把高并发变成可计算的工程:几个机器、几个池子、几把锁、限流用哪种。
★ 四组一手实测 ✓✓:
容量估算:日活 100 万 → 峰值 6667 QPS → 9 台(加冗余 14 台)→ 并发下限 333
排队曲线:负载率 0.90 → 0.95(只涨 5 个点),总延迟 100ms → 200ms(翻倍)✗✗
利特尔定律:并发 = QPS × RT;池子只配 50 → 上限 1000 QPS(差 6.7 倍)✗✗
限流实测:临界突发下「固定窗口 20/20 ✗✗ | 滑动窗口 10/20 | 令牌桶 11/20 | 漏桶 11/20(排队)」★ 高并发项目最容易翻车的两件事 ✗✗:
| 翻车方式 | 代价 |
|---|---|
| 拍脑袋配机器/池子 ✗ | 上线即瓶颈(池子 50 却要 6667 QPS) |
| 等到压测拐点再扩容 ✗✗ | 拐点之后延迟已经失控,扩容窗口以分钟计 |
★★ 而两件事都可以提前算出来 ✓✓✓:算量 → 留水位 → 限流兜底(T2-015 的压测就是用来标定「单机能力」的)✓。
日活 1000000 人 × 人均 20 次 = 日请求 20000000 次
均匀分布:QPS = 20000000 / 86400 ≈ 231
口径 A(20% 请求集中在 10 分钟峰值段):6667 QPS
口径 B(经验峰值系数 5):1157 QPS
★ 取更大者做设计输入:**6667 QPS**(估算宁可偏大)
单机能力(必须用压测标定):800 QPS/台
机器数 = 6667 / 800 ≈ 8.3 → 向上取整 **9 台**
按 1.5 倍冗余(9 × 1.5 = 13.5)→ **14 台** ✓✓★★ 三个要点 ✓✓✓:
负载率 ρ | 平均排队 Wq | 总延迟 | 平均排队长度
0.50 | 10.0 ms | 20.0 ms | 0.5
0.70 | 23.3 ms | 33.3 ms | 1.6
0.80 | 40.0 ms | 50.0 ms | 3.2
0.90 | 90.0 ms | 100.0 ms | 8.1
0.95 | 190.0 ms | 200.0 ms | 18.0
0.98 | 490.0 ms | 500.0 ms | 48.0★★★ 一句话 ✓✓✓:系统接近饱和时,延迟对流量是「非线性」的 —— ρ 从 0.90 到 0.95(+5 个点),延迟翻倍;到 0.98 时已经是 5 倍 ✗✗。
★ 这就是 T2-014 压测里「并发 ×64 而 QPS 只 +25%、P99 涨 36 倍」的根源 ✓✓✓ —— 拐点不是 bug,是排队论的必然:所以容量水位要留在 70% 以下 ✓✓。
目标 6667 QPS,平均响应 50 ms
并发处理中请求 = QPS × RT = 6667 × 0.050s = **333 个**
-- 反例:下游连接池只配了 50 --
池子上限 = 50 / 0.05s = **1000 QPS** ✗✗(远低于目标 6667)★★ 三个「连乘」检查 ✓✓✓(任何一环不够都会卡住整条链):
| 环节 | 下限 | 说明 |
|---|---|---|
| 入口线程池 | ≥ 333 | 不足 → 请求排队在入口 |
| 下游连接池 | ≥ 333 | 不足 → 上游线程全在等连接(《线程池与 ThreadPoolExecutor》) |
| 数据库 max_connections | ≥ 333 × 实例数 | 最容易忽略的一环 ✗✗ |
★ 反直觉推论 ✓:下游慢一倍(RT 100ms),需要的池子就翻一倍 —— 「扩容池子」是治标,「让下游变快」才是治本 ✓✓。
同一段「窗口临界突发」(t=900~1090ms 共 20 个请求,限流 10/秒):
| 算法 | 通过 | 实测行为 | 结论 |
|---|---|---|---|
| 固定窗口 | 20/20 ✗✗ | 窗口 0 放满 10 个、窗口 1 又放 10 个 | 临界双倍(瞬时 105/秒)✗✗ |
| 滑动窗口 | 10/20 ✓ | 任何 1 秒内都不超 10 个 | 精确,但要存时间戳 |
| 令牌桶 | 11/20 ✓✓ | 前 10 个花掉满桶,之后按速率补充 | 允许蓄力突发,O(1) |
| 漏桶 | 11/20 ✓ | 入桶后恒定速率流出,最长排队 990ms | 保护下游,入口不限突发 |
★★ 选型 ✓✓✓:网关/服务端默认用令牌桶(Guava RateLimiter / Sentinel 的选择)✓; 单机限流只保自己,全局限流要上 Redis 或网关层 ✓(呼应 《CAP 与 BASE 理论》 的 CP 取舍)。
★ 固定窗口的教训 ✓✓:它的「超额」不是 10%,而是瞬时 2 倍 —— 任何「按周期重置」的设计都要问一句:周期边界会发生什么? ✓✓。
结合 《项目 B:高并发场景与架构设计》 的秒杀链路,把「每层拦多少」写清楚:
10 万请求 → CDN/网关(拦静态与恶意)→ 限流(只放 1 万)→ 缓存预扣(挡掉 9000 无库存)
→ MQ 削峰(异步落库 1000)→ 数据库(实际写入 1000)
★ 核心:**每一层都把量级降一个数量级**,让数据库只面对「真实成交」✗✓① 业务量(DAU × 人均请求)
② 峰值 QPS(两个口径取大)
③ 单机能力(压测标定)
④ 机器数(向上取整 + 冗余)
⑤ 并发下限(Little 定律)→ 反向校验池子配置boolean tryAcquire(long nowMs) {
tokens = Math.min(capacity, tokens + (nowMs - lastRefillMs) * ratePerMs);
lastRefillMs = nowMs;
if (tokens >= 1) { tokens -= 1; return true; }
return false;
}if (count++ < N) 单机限流 × 10 台 = 实际上限 10N ✗✗(每台都以为自己只有 N)
正解:Redis 计数器(Lua 原子)或网关统一限流 ✓✓代码:labs/java-deep/src/main/java/cn/kb/learn/deep/architecture/CapacityEstimationLesson.java、labs/java-deep/src/main/java/cn/kb/learn/deep/architecture/RateLimiterLesson.java
实测输出(节选):
===== 2. 排队模型(M/M/1,服务时间 10ms):拐点从哪来 =====
0.90 | 90.0 ms | 100.0 ms | 8.1
0.95 | 190.0 ms | 200.0 ms | 18.0
★★ 这就是压测「拐点」的数学解释:**接近饱和时,一点流量增长会被延迟放大数倍**
===== 3. 利特尔定律:池子该配多大 =====
并发处理中请求 = QPS × RT = 6667 × 0.050s = **333 个**
池子上限 = 50 / 0.05s = **1000 QPS** ✗✗(远低于目标 6667)
===== 1~4. 四种限流:同一段临界突发 =====
t=990ms 通过 ✓(窗口 0: 10/10) ← 固定窗口
t=1000ms 通过 ✓(窗口 1: 1/10) ← 边界另一侧重新计数
通过 20/20:**20 个请求在 190ms 内全部通过** ✗✗(瞬时约 105/秒)
...
滑动窗口:通过 10/20 ✓ 令牌桶:通过 11/20 ✓ 漏桶:接受 11/20(最长排队 990ms)| # | 错误 | 说明 |
|---|---|---|
| 1 | 把「日活 × 请求 / 86400」当设计 QPS ✗✗ | 峰值是均匀值的 5~30 倍 ✓✓ |
| 2 | 等压测拐点到了才扩容 ✗✗✗ | 拐点后延迟已非线性失控 ✓✓ |
| 3 | 只配线程池、忘了连接池/DB 连接 ✗✗ | 连乘检查缺一不可 ✓✓ |
| 4 | 限流用固定窗口扛突发 ✗✗ | 瞬时 2 倍额度,等于没限 ✓✓ |
| 5 | 单机限流当全局限流 ✗✗✗ | 实际上限 = N × 单机额度 ✓✓ |
| 6 | 容量水位不留(跑到 90%+) ✗✗ | 一点波动就进非线性区 ✓✓ |
CapacityEstimationLesson 三段,把 DAU 换成你自己项目的数字 ✓✓✓;RateLimiterLesson,把突发序列改成你自己的业务模式(如整点开抢)✓✓;RateLimiter 或 Sentinel 复现令牌桶行为,对拍本节实现 ✓✓;验收方式:给定一个业务量与单机能力,能算出机器数、水位、并发下限与限流方案。
扩展任务:为项目 B 写《容量与限流方案》:目标 QPS、机器水位、池子配置、限流算法与阈值来源 ✓✓
《高可用系统设计》 高可用系统设计 —— 承接本节的「容量与限流」,讲故障与重试的数学。 监控侧呼应:T2-016 服务端监控与瓶颈分析。