双11零点,商品页全是404:一次复盘,让我彻底想明白了“99.99%”

简介: 双11零点,商品页全是404:一次复盘,让我彻底想明白了“99.99%”

双11零点,商品页全是404:一次复盘,让我彻底想明白了“99.99%”

作者:Echo_Wish
说明:本文案例为根据行业常见大促故障综合改编,数字和细节做了脱敏处理。

干运维这些年,我最怕的不是半夜被电话叫醒,而是零点刚过、大屏上一片红,群里只有一个字:“404”。

那天是双11零点。倒计时结束的前三十秒,我们还在群里互相发“稳了稳了”。零点一到,流量像开闸一样涌进来,不到两分钟,监控上商品详情页和下单接口的 404 占比直接冲到 37%。

说实话,当时我第一反应是:这不对啊,404 是“找不到资源”,我们的接口明明都在,怎么可能找不到?

一、404 是个“假象”,真凶在后面

很多人一看 404,就去翻发布记录,看是不是有人把路由配错了。我们当时也这么干,白白浪费了四分钟。

后来拉着网关日志一条条对,才发现真相是这样一条链子:

  1. 零点瞬间,几千个新扩容的容器同时起来,全部去注册中心注册、发心跳;
  2. 注册中心被心跳打成了“半死不活”,响应超时;
  3. 网关拉取路由表时拿到了空列表,而网关的实现很“老实”:拉到什么就刷成什么;
  4. 路由表一空,所有请求匹配不到上游,直接返回 404。

你看,404 根本不是业务问题,是我们自己把自己的“地图”擦掉了。

更惨的是,用户一刷新,客户端又触发重试,每个请求变成三个请求,注册中心更扛不住,形成了典型的重试风暴。最后我们是靠手动重启网关加临时限流,十一分钟后才止住血。

十一分钟,对一个大促零点来说,是什么概念?后面算账你就知道了。

二、复盘:三个坑,一个比一个“常见”

事后复盘会开了三个小时,我把根因压缩成三句话:

  • 依赖失败时,没有兜底:注册中心挂了,网关选择“相信空列表”;
  • 失败时,没有节制:重试没有上限、没有退避,所有人一起冲;
  • 没人知道“多坏算坏”:我们只有“CPU 高了告警”,没有“用户体验坏了告警”。

下面说落地措施,我只讲真上线了的、有代码的。

三、落地措施一:依赖挂了,别把旧地图擦掉

最核心的一条:拿到异常数据时,宁可用旧的,也不要用错的。

import time, logging
log = logging.getLogger("gateway")

class RouteTable:
    def __init__(self, min_ratio=0.5):
        self._routes = {
   }
        self._min_ratio = min_ratio
        self._last_ok = 0

    def refresh(self, fetch):
        try:
            new_routes = fetch()          # 从注册中心拉取
        except Exception as e:
            log.warning("registry down, keep old routes: %s", e)
            return                        # 拉取失败:保持旧路由

        # 关键防线:空列表 / 骤降,一律视为可疑
        if not new_routes:
            log.error("empty routes from registry, ignore")
            return
        if self._routes and len(new_routes) < len(self._routes) * self._min_ratio:
            log.error("routes dropped >50%%, ignore (%d -> %d)",
                      len(self._routes), len(new_routes))
            return

        self._routes = new_routes
        self._last_ok = time.time()

这段代码不复杂,但它背后是个很朴素的道理:监控系统和注册中心是“参考”,不是“真理”。参考出了问题,你得有自己的判断。

另外 Nginx 这一层,我们也把超时和重试收紧了:

upstream order_svc {
   
    server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
    server 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
    keepalive 256;
}

location /api/order/ {
   
    proxy_connect_timeout 300ms;
    proxy_read_timeout    1s;
    proxy_next_upstream   error timeout http_502 http_503;
    proxy_next_upstream_tries   2;     # 最多重试一次
    proxy_next_upstream_timeout 1s;    # 重试总耗时也要封顶
}

四、落地措施二:重试要有“预算”,别搞人海战术

重试本身没错,错的是不讲规矩的重试。我们的做法是:指数退避 + 随机抖动 + 重试预算(重试流量不能超过总流量的 10%)。

import random, time

class RetryBudget:
    """重试请求数不得超过总请求的 ratio"""
    def __init__(self, ratio=0.1):
        self.ratio, self.total, self.retries = ratio, 0, 0

    def on_request(self):
        self.total += 1

    def can_retry(self):
        return self.retries < self.total * self.ratio

    def on_retry(self):
        self.retries += 1


def call_with_retry(fn, budget, max_attempts=3, base=0.1, cap=1.0):
    budget.on_request()
    for attempt in range(max_attempts):
        try:
            return fn()
        except Exception:
            if attempt == max_attempts - 1 or not budget.can_retry():
                raise
            budget.on_retry()
            # 指数退避 + 全抖动,避免所有客户端同一时刻重试
            time.sleep(random.uniform(0, min(cap, base * 2 ** attempt)))

我现在跟开发同学聊天,最爱说一句话:“重试是借钱,不是印钞。”借多了,总有还不上的时候。

五、落地措施三:流量太大?先“有尊严地拒绝”

大促零点的流量,你无论如何都接不完。关键是:宁可明确拒绝 20% 的人,也不要让 100% 的人一起卡死。

import time, threading

class TokenBucket:
    def __init__(self, rate, capacity):
        self.rate, self.capacity = rate, capacity
        self.tokens, self.ts = capacity, time.monotonic()
        self.lock = threading.Lock()

    def allow(self):
        with self.lock:
            now = time.monotonic()
            self.tokens = min(self.capacity,
                              self.tokens + (now - self.ts) * self.rate)
            self.ts = now
            if self.tokens >= 1:
                self.tokens -= 1
                return True
            return False

bucket = TokenBucket(rate=5000, capacity=8000)

def handle_order(req):
    if not bucket.allow():
        # 友好降级:返回排队页,而不是让用户转圈或看到 404
        return {
   "code": 429, "msg": "前方拥挤,请稍后再试", "retry_after": 2}
    return do_order(req)

降级文案也是产品力。“系统繁忙”和“前方有 3 万人在排队,预计 20 秒”,用户的感受完全是两回事。

六、落地措施四:用“错误预算”定义什么叫 99.99%

这是我这次最大的收获。

99.99% 听起来很美,算一下账:一个月 30 天,只允许你坏 4.32 分钟。而我们那次,一次故障就坏了 11 分钟,直接把当月(甚至更长时间)的预算花光了。

所以我们把“可用性”变成了可以告警的东西,而不是 PPT 上的口号:

# Prometheus:基于错误预算燃烧速率的告警(SLO 99.99%)
groups:
- name: slo-order-api
  rules:
  - alert: OrderAPIErrorBudgetBurnFast
    expr: |
      (
        sum(rate(http_requests_total{job="order",code=~"5..|404"}[5m]))
        / sum(rate(http_requests_total{job="order"}[5m]))
      ) > (14.4 * 0.0001)
      and
      (
        sum(rate(http_requests_total{job="order",code=~"5..|404"}[1h]))
        / sum(rate(http_requests_total{job="order"}[1h]))
      ) > (14.4 * 0.0001)
    for: 2m
    labels: {
    severity: page }
    annotations:
      summary: "订单接口错误预算正在快速燃烧,1小时内可能耗尽当月预算的2%"

意思很简单:不是等机器 CPU 红了才叫人,而是用户体验在变坏的时候就叫人。

七、最后,聊聊我的几点感受

做完这一轮,我有几个很朴素的体会:

第一,高可用不是“堆机器”,是“管失败”。机器多了,故障点也多了。真正的功夫在于:依赖挂了怎么办、请求太多怎么办、你不知道的时候怎么办。

第二,99.99% 是一张“预算表”,不是一个“荣誉称号”。它逼着你去算:这个发布值不值得冒这个险?这个功能值不值得牺牲这点稳定性?有了预算,吵架就有了依据。

第三,演练比预案值钱。我们后来每个月做一次故障注入,随机掐掉注册中心、拉高延迟,看路由表会不会被擦掉。头几次翻车翻得很难看,但这些难看,都比大促零点翻车便宜得多。

第四,复盘的目的是让下一个人不用再踩,而不是找人背锅。那次事故里,写那段“拉到什么刷成什么”代码的同学,复盘会上一直低着头。我跟他说:这段代码在没有大促的一年里跑得好好的,它不是你的错,是我们没把“失败”当成需求来设计。

从 404 到 99.99%,我们没有用什么神秘的黑科技,无非是把这些“都知道、但总觉得不会发生在自己身上”的事,一件一件落到了代码和流程里。

运维这行,平静的每一天,都是一堆没被注意到的细节撑起来的。

下次大促零点,愿你的大屏一片绿,群里只有一句:“稳。”

——Echo_Wish

目录
相关文章
|
7天前
|
存储 人工智能 弹性计算
2026年阿里云双十一活动有哪些?详细攻略来了!
2026年阿里云双十一(11.1–11.30)开启!聚焦云计算与AI生产力,覆盖服务器、存储、数据库、大模型等全品类。享满减礼包(最高减1728元)、新人秒杀(2核2G仅38元/年)、普惠ECS(99元起)、Token Plan直省55%、160+款产品免费试用!
129 1
|
7天前
|
缓存 Prometheus 监控
别再只盯着CPU和内存了:用Prometheus + Grafana把业务真正跑起来的样子画出来
别再只盯着CPU和内存了:用Prometheus + Grafana把业务真正跑起来的样子画出来
78 3
|
8天前
|
运维 Kubernetes 开发工具
ArgoCD 实战:多集群同步别只会点同步,冲突才是真正的麻烦
ArgoCD 实战:多集群同步别只会点同步,冲突才是真正的麻烦
64 1
|
14天前
|
存储 弹性计算 运维
云服务器也有“碳账单”:GreenOps不是省电,而是别让资源白烧
云服务器也有“碳账单”:GreenOps不是省电,而是别让资源白烧
71 1
|
19天前
|
Kubernetes 网络协议 数据库
真打起“世界大战”,你的灾备扛得住吗?
真打起“世界大战”,你的灾备扛得住吗?
72 3
|
20天前
|
消息中间件 运维 NoSQL
别等系统被压垮了,才知道容量到底有多大
别等系统被压垮了,才知道容量到底有多大
77 1
|
22天前
|
监控 NoSQL 数据库
别再盯着 CPU 看弹性了:混沌工程真正该测的是“扛揍能力”
别再盯着 CPU 看弹性了:混沌工程真正该测的是“扛揍能力”
108 1
|
27天前
|
缓存 运维 监控
别再靠吵架定优先级了:错误预算才是产品决策的底牌
别再靠吵架定优先级了:错误预算才是产品决策的底牌
90 0
|
1月前
|
消息中间件 缓存 监控
流量一上来就扛不住?别硬撑了,该降级就降级!
流量一上来就扛不住?别硬撑了,该降级就降级!
93 0
|
2月前
|
运维 监控 NoSQL
事故复盘不是写检讨,而是让团队变强的一次机会
事故复盘不是写检讨,而是让团队变强的一次机会
148 1

热门文章

最新文章