十点整,流量像开了闸
老王给 503 咖啡馆三周年策划了「半价日」活动,会员群里预热了三天。10 点整活动上线,App 弹窗推送齐发——一分钟内,下单接口的流量冲到平时的四十倍。监控大盘从绿变黄只用了二十秒,从黄变红又用了二十秒,第三分钟,全站 502,连商品详情页都打不开了。用户投诉像雪片,老王给我打电话时系统还在反复重启反复倒下。这本系列就从这条灾难链讲起——雪崩不是一瞬间的意外,是一条有先后顺序的因果链,每一环都能在代码里找到根源。
第一幕:慢——从连接池打满开始
灾难的第一环在数据库。秒杀下单的接口里有一个热点查询:查库存,走的是一张大表的行级更新,平时 RT(响应时间)50 毫秒。流量冲高四十倍后,数据库 CPU 先顶到 100%,查询排队,RT 从 50 毫秒爬到 500 毫秒、再爬到 5 秒。容量是按「平时流量 × 安全余量」规划的,四十倍流量下,每一个环节都在超卖。应用这头,每个请求都要占一个数据库连接,查询从 50 毫秒变成 5 秒,连接的持有时间翻了一百倍——连接池的 50 个连接瞬间被占满,第 51 个请求开始排队等连接,队伍越排越长。
第二幕:堆积——线程池的连锁窒息
连接池满了,请求在等连接;等连接的是谁?是 Tomcat 的工作线程。每个 HTTP 请求占一个工作线程,线程在数据库查询上阻塞 5 秒,200 个工作线程在 10 秒内全部阻塞在等连接或等查询上。新请求到达时没有线程接待,进入 Tomcat 的等待队列;队列满了,操作系统层面直接拒绝连接。此时用户看到的现象是:下单接口超时,商品详情接口也超时——它们明明是两个接口、两张表,但它们共享同一个 Tomcat 线程池、同一个数据库连接池,这就是连坐的物理基础。
第三幕:连坐——不相关的服务陪葬
更糟的还在后面。订单服务挂了,但用户手机上的 App 不会因此停手:购物车刷不出来会刷新,详情页打不开会重试,失败触发重试,重试变成新的流量,洪峰之上再叠一层洪峰。会员服务调用订单服务查「最近订单」,没有超时设置,默认等到天荒地老——会员服务的线程也全阻塞了。网关后面一排服务,一个倒下,传染一片,这就是雪崩这个词的本义:一块雪松动,整面山坡跟着滑。分布式锁系列第 19 篇的事故五(高频抢锁拖垮注册中心)是这条链的特例,本系列要把整条链系统性拆掉。
第四幕:重启风暴
运维的直觉反应是重启。服务重启要 30 秒,这 30 秒里积压的请求、重试的请求、心急的用户多点几次的请求,全在门外候着——服务刚一起来,积压的流量一齐涌入,刚扩的连接池刚热身就被打满,30 秒后再次倒下。重启一次,倒下一次,系统在死亡边缘反复震荡。老王的半价日最后以「临时下线活动」收场,优惠券没发完,客诉发了一周。
| 时间 | 现象 | 根因 |
|---|---|---|
| 10:00-10:00:20 | 下单 RT 从 50ms 爬升 | DB 容量超卖,热点查询排队 |
| 10:00:20-10:01 | 连接池满,请求排队 | RT 拉长导致连接持有时间暴涨 |
| 10:01-10:02 | 线程池阻塞,全接口超时 | 共享线程池,无隔离无超时 |
| 10:02-10:03 | 不相关服务连坐,全站 502 | 重试风暴 + 无熔断,故障传染 |
| 10:03 之后 | 重启后立刻再倒 | 积压流量冲击,无预热无限流 |
四板斧与一块地基
复盘这条灾难链,每一幕都对应一味药:限流——流量超过容量就果断少接点,宁可得罪十个用户,不能拖垮全站(第 2 到 9 篇);熔断——下游病了就别再问它,快速失败防止线程被拖死(第 10 到 12 篇);降级——接不住的请求给个体面的兜底,而不是裸超时(第 13 篇);隔离——把资源切成舱壁,一舱进水不沉全船(第 14 篇)。这四板斧全都站在同一块地基上:超时——没有超时,任何保护机制都会被「永远等下去」的线程架空(第 15 篇)。下一篇从四板斧的第一把讲起:限流的第一课,固定窗口计数器——最朴素的算法,最经典的突刺。
评论 (0)