☰
高并发流量治理实战(10):大促 48 小时:一次全链路压测与扩容演练复盘
2026/9/28 4:49:42 网站建设 项目流程

最后一篇:把九篇机制装进一个真实的 48 小时

系列完结篇不再引入新机制,而是把前九篇按进一次真实节奏里:某电商平台"年货节",T-48h 全链路压测,T-0 零点峰值,T+24h 复盘。作者是当晚值班 SRE,复盘文档脱敏后如下。看的时候不妨拿着前九篇当检查表——你会发现每一个惊险时刻,背后都对应某一篇里"平时不做、战时必炸"的那件事。

T-48h:压测之夜,两颗雷

压测按第 9 篇的剧本执行:压测标贯穿、影子库、阶梯加压。第一颗雷在 T-46h:数据组报告影子表里混进真实订单号段。排查半小时定位——补偿定时任务丢标,正是第 9 篇实验里泄漏点的全部来源:主链路同步调用和 MQ 透传都正确,坏在"凌晨重放待办"的新上下文。修复走"出身随数据走":待办表加traffic_origin列,离线任务按列分流。第二颗雷在最后一轮"极限压":入口推到预估峰值的 1.4 倍时,券服务的熔断器参数暴露问题——慢调用阈值还是按日常 RT 配的,压测流量一来大面积误熔断。这对应第 3 篇参数扫描那张表:检出延迟和误伤是硬交换,而"健康画像"本身随负载漂移,阈值必须在压测态下重标定,不能只在平静期看。

T-24h:扩容与预热的算术

压测定了容量,接下来是钱怎么花。把各服务压测得到的单机安全水位、当前节点数、今晚预估峰值(入口 9000 rps 按调用放大比折算)放进计算器:

importmath# 扩容账: 压测单机安全水位 -> 今晚预估需求(放大比 x 冗余系数) -> 节点数与限流反推# 入口今晚预估峰值 BASE_PEAK = 9000 rpsBASE_PEAK=9000svc={# 名称: (单机安全rps[压测得出], 当前节点, 调用放大比, 冗余系数)"gateway":(900,12,1.00,1.2),"order":(160,24,0.75,1.3),"coupon":(420,8,1.25,1.3),"inventory":(700,16,1.10,1.2),"risk":(150,10,0.15,1.1),# 抽样评估, 放大比 < 1}print("服务 单机 当前 需求 应配 动作")total_add=0plans=[]forname,(per_node,nodes,amp,coef)insvc.items():target=BASE_PEAK*amp*coef need=math.ceil(target/per_node)add=max(0,need-nodes)total_add+=add act="扩 %d 台"%addifaddelse("持平"ifnodes==needelse"冗余 %d 台, 不缩"%(nodes-need))plans.append((name,per_node,need,target))print("%-12s %5d %5d %7.0f %6d %s"%(name,per_node,nodes,target,need,act))print("合计新增 %d 台; 冗余只扩不缩, 缩容决策留到复盘周会"%total_add)print("\n限流反推(网关接口窗口 = 扩容后集群容量 x 0.9, 压的是总量不是需求):")forname,per_node,need,targetinplans:cap=per_node*need*0.9print(" %-12s 容量 %6.0f rps -> 窗口 %6.0f rps | 今晚需求峰值 %.0f"%(name,cap,cap,target))

运行输出:

服务 单机 当前 需求 应配 动作 gateway 900 12 10800 12 持平 order 160 24 8775 55 扩 31 台 coupon 420 8 14625 35 扩 27 台 inventory 700 16 11880 17 扩 1 台 risk 150 10 1485 10 持平 合计新增 59 台; 冗余只扩不缩, 缩容决策留到复盘周会 限流反推(网关接口窗口 = 扩容后集群容量 x 0.9, 压的是总量不是需求): gateway 容量 9720 rps -> 窗口 9720 rps | 今晚需求峰值 10800 order 容量 7920 rps -> 窗口 7920 rps | 今晚需求峰值 8775 coupon 容量 13230 rps -> 窗口 13230 rps | 今晚需求峰值 14625 inventory 容量 10710 rps -> 窗口 10710 rps | 今晚需求峰值 11880 risk 容量 1350 rps -> 窗口 1350 rps | 今晚需求峰值 1485

三个读表要点。瓶颈不在入口在服务链中间:网关和风控持平,券服务要扩 27 台——只看入口容量的规划会在最薄弱环节爆炸(这也是第 1 篇"每层都要有闸"的另一面:每层都要有容量账)。限流窗口刻意压在容量线上而不是需求线上:gateway 窗口 9720 低于 1.2 倍冗余后的需求 10800,意思很直白——如果流量真到 1.2 倍预测值,宁可拒一部分,也不让系统进非线性恶化区(第 9 篇那条"悬崖不是山坡"的曲线)。热点 Key 的功课在这天收尾:晚八点开抢的爆款提前两小时按第 5 篇流程预热进 L1/L2,探测器的名单下发演练了一遍——预热没演练过等于没预热。

T-0:20:03,缓存分片惊魂

零点峰值平稳过渡,所有人开始松懈时,真正的考题来了:20:03,缓存集群一个分片被变更脚本误重启,命中率从 96% 掉到 61%,回源洪峰砸向数据库。把当晚的两套走向各回放一遍(DB 服务力 600 rps,虚拟分钟时钟):

# 大促当晚回放(虚拟分钟时钟): 20:00 基线 2000 rps 线性爬坡, 21:00 峰值 ~13400# 20:03 缓存一个分片被误重启 -> 命中率 96%->61%, 20:06 切回; DB 服务力上限 600 rpsSLOPE,POOL=190,600HIT0,HIT_BAD=0.96,0.61defsim(protect):backlog=0.0print("场景: %s"%("开启 DB 入口自适应拒绝"ifprotectelse"DB 不设防(现状)"))forminrange(8):q=2000+SLOPE*m hit=HIT_BADif3<=m<=5elseHIT0 demand=q*(1-hit)served=min(demand,POOL)ifprotect:print("20:%02d 前端 %5d rps | 要DB %5d | DB扛 %4d | 拒绝 %4d"%(m,q,demand,served,demand-served))else:backlog+=(demand-served)*60# 一分钟排进的队print("20:%02d 前端 %5d rps | 要DB %5d | DB扛 %4d | 累计排队 %6d 请求"%(m,q,demand,served,backlog))returnbacklog b=sim(False)print("-> DB 侧最多堆出 %d 个请求, 队尾等待 %.0f 秒: 上游全超时+重试放大, 雪崩成立\n"%(b,b/POOL))sim(True)print("-> 排队变拒绝: DB 存活, 超额流量在前端表现为排队页/降级页(第4篇预案)")

运行输出:

场景: DB 不设防(现状) 20:00 前端 2000 rps | 要DB 80 | DB扛 80 | 累计排队 0 请求 20:01 前端 2190 rps | 要DB 87 | DB扛 87 | 累计排队 0 请求 20:02 前端 2380 rps | 要DB 95 | DB扛 95 | 累计排队 0 请求 20:03 前端 2570 rps | 要DB 1002 | DB扛 600 | 累计排队 24138 请求 20:04 前端 2760 rps | 要DB 1076 | DB扛 600 | 累计排队 52722 请求 20:05 前端 2950 rps | 要DB 1150 | DB扛 600 | 累计排队 85752 请求 20:06 前端 3140 rps | 要DB 125 | DB扛 125 | 累计排队 85752 请求 20:07 前端 3330 rps | 要DB 133 | DB扛 133 | 累计排队 85752 请求 -> DB 侧最多堆出 85752 个请求, 队尾等待 143 秒: 上游全超时+重试放大, 雪崩成立 场景: 开启 DB 入口自适应拒绝 20:00 前端 2000 rps | 要DB 80 | DB扛 80 | 拒绝 0 20:01 前端 2190 rps | 要DB 87 | DB扛 87 | 拒绝 0 20:02 前端 2380 rps | 要DB 95 | DB扛 95 | 拒绝 0 20:03 前端 2570 rps | 要DB 1002 | DB扛 600 | 拒绝 402 20:04 前端 2760 rps | 要DB 1076 | DB扛 600 | 拒绝 476 20:05 前端 2950 rps | 要DB 1150 | DB扛 600 | 拒绝 550 20:06 前端 3140 rps | 要DB 125 | DB扛 125 | 拒绝 0 20:07 前端 3330 rps | 要DB 133 | DB扛 133 | 拒绝 0 -> 排队变拒绝: DB 存活, 超额流量在前端表现为排队页/降级页(第4篇预案)

同一个故障、同一份流量,两个世界的分岔只有一处:超额的部分是排队还是被当场拒绝。不设防时 DB 的"服务率"没变(每分钟照样 600),变化的是队列悄悄堆到 8.6 万——队尾等待 143 秒,早已越过所有上游的超时线,超时触发重试,重试再注入新流量,这就是雪崩的动力学。而自适应拒绝版里,三分钟内 1428 个请求被当场弹回(用户看到的是第 1 篇精心设计的排队页,不是第 3 篇最坏情况的白屏),换来 DB 全程水位 600 一条直线。当晚真实走向是第二幕:20:05 值班长按预案第 2 档降级(非核心推荐切快照、下单链路保持),20:06 分片切回,全程无订单丢失,代价是一千余人次用户在窗口内收到"前方拥挤"。

T+24h:复盘与系列的最后一条建议

复盘会按无指责(blameless)原则过三个问题:探测为什么慢(命中率监控 2 分钟粒度,故障 90 秒即自愈型事件几乎不可见——改为 10 秒粒度加环比告警);预案为什么只动到第 2 档(第 4 篇的分级表里,"自动触发第 3 档"的条件写的是 DB 连接数而不是排队深度,这次排队深度恰好先于连接数越线——参数联动清单更新);压测为什么没压出这颗雷(压测剧本里没有"依赖组件运行中被杀"这一章——第 9 篇的常态化压测清单里加入故障注入)。

复盘产出的不是文档,是带 owner 和期限的账,当晚的行动项后来长成四件事:一是监控资产化——“缓存命中率环比骤降”“DB 排队深度”"fast-fail 计数"这三个当晚救命的信号,从个人经验变成全员订阅的标准面板,每个治理机制(限流拒绝数、熔断开闸事件、位点等待超时降级次数)都必须有自己的指标,"熔而不觉、限而不查"等于没做;二是开关认领制——第 4 篇降级矩阵里的每一档开关登记唯一 owner 与 backup,演练时随机点名,答不出"这个开关现在什么值、拉下去影响哪个指标"的当场记缺;三是参数回灌例会——每次大促和故障后的实测数字(单机安全水位、读 P99、复制 lag 峰值)回填进限流窗口、双删 delay、粘主时长这三张配置表,参数改动走发布流程而不是口头共识;四是混沌常态化——每月在低峰期随机杀一个缓存分片、注入一次从库延迟、制造一轮时钟回拨(第 6 篇的老朋友),让"预案被触发"从年度大事件变成月度演习科目,真正的故障来临时,所有人的肌肉记忆来自上个月而不是去年。

这就是系列压轴的观点:限流、熔断、降级、缓存、复制、压测不是六堆各自独立的开关,而是一张互相咬合的网——第 1 到 5 篇管流量的形状(总量分层拦、单键多级接、坏依赖快刀断、撑不住主动舍),第 6 到 8 篇管数据的秩序(写有唯一的名字、改有收敛的路径、读有可预期的新鲜度),第 9、10 篇负责在和平时期演练前者、在战争时期兑现后者。这张网没有终态:每次大促的曲线、每次故障的时间线,都要回灌成下一版容量表和预案表。系列到此完结,愿你的大促复盘文档,永远只有"预案生效"这一种剧情。

参考来源

  • Google SRE Workbook: Postmortem Culture: Learning from Failure: https://sre.google/workbook/postmortem-culture/
  • Google SRE Book: Handling Overload(过载处理与限流拒绝): https://sre.google/sre-book/handling-overload/
  • Wikipedia: Chaos engineering(故障注入): https://en.wikipedia.org/wiki/Chaos_engineering
  • Wikipedia: Postmortem documentation: https://en.wikipedia.org/wiki/Postmortem_documentation

本系列已结集为免费专栏:高并发流量治理实战:从限流到全链路压测;进阶推荐付费专栏:提示词工程实战:从入门到生产级 Prompt 设计(限时 ¥19.9,首篇免费试读)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询