一粟 · yisu.app
· 15 min read
高并发稳定性

高并发系统稳定性:隔离、熔断、降级三层防线的工程化落地

一个能扛住双十一的系统,绝不止靠加机器。本篇把稳定性拆成「事前 - 事中 - 事后」三层防线,结合真实事故案例,讲清楚资源隔离、熔断降级、流量调度的工程实现路径。

把稳定性从「口号」变成「三个抽屉」

我过去带团队做过几次大型促销系统的值守,经验是:稳定性不是某次架构升级做完就有的事,而是一套可以反复演练的工程能力。我喜欢把这种能力分成三个抽屉:

  • 事前:容量评估、压测、依赖梳理、应急预案。这一步把「会怎样出问题」提前想明白。
  • 事中:隔离、熔断、降级、限流、流量调度。这是一旦出问题时的「自动与半自动」响应。
  • 事后:监控、告警、复盘、改进。这里负责把每一次事故转化为新的能力。

很多人只关心事中,事实是事前决定了事中的天花板。一个缺乏容量评估的系统,事中再好的熔断也救不回它。今天我们专注在事中这一层的「工程实现」上,因为这是经常被低估的一块。

隔离:用池子把风险「关进小房间」

资源隔离不是新概念,但常常被轻视。所谓隔离,就是不要让一个业务的故障波及另一个业务。它的实现手段很多,按由轻到重排:

  1. 线程池隔离:每个业务独立线程池,一个阻塞不阻塞别人。Hystrix / Resilience4j 的「Bulkhead」就是这个模式。
  2. 进程隔离:用 Sidecar 或独立部署,把 I/O 重请求单独跑。
  3. 机房 / 单元化隔离:核心业务的请求被「圈」在某个机房或某个 ID 哈希单元里。
  4. 流量分组隔离:网关层按业务分组转发,不同组走不同上游。
隔离的本质是「把风险的爆炸半径」压到业务可接受的最小单元。

我们曾遇到一次事故:搜索服务的一个长链路查询让公共线程池打满,牵连详情页也返回 504。这就是典型的「未隔离」反例。改造后变成三个独立线程池,故障被关在搜索内部。

熔断:从「假死等待」到「快速失败」

熔断(Circuit Breaker)的本质是让客户端在依赖异常时「主动放弃」。它有三个状态:

  • CLOSED(关闭):正常请求。
  • OPEN(打开):直接失败,不再调用下游。
  • HALF_OPEN(半开):放少量流量试探下游是否恢复。

常见的触发策略有两种:基于错误率(5 秒窗口内错误率超 50% 则熔断),基于慢调用比例(P99 超过 800ms 占比超 30%)。这两个策略适用于不同依赖:CPU 型应用对慢调用敏感,IO 型应用对错误率敏感。下面是一个 Rust 风格的实现片段,便于团队在自家网关里复用:

pub struct Breaker {
    state: AtomicU8, // 0=Closed, 1=Open, 2=HalfOpen
    failure_count: AtomicU64,
    slow_count: AtomicU64,
    opened_at_ms: AtomicU64,
}

impl Breaker {
    pub async fn call(&self, f: F) -> Result
    where F: Future> {
        if self.is_open() {
            return Err(BreakerError::Open);
        }
        match f.await {
            Ok(v) => { self.record_success(); Ok(v) }
            Err(_) => { self.record_failure(); Err(BreakerError::Underlying) }
        }
    }
}

降级:分清「必需」与「可选」

熔断解决「下游异常」,降级解决「哪怕没异常,我也要放弃一些东西」。降级不是简单「关闭功能」,而是一份分层的设计:

层级策略触发条件
L0 必备登录、支付、风控永不降级
L1 重要商品详情、推荐位CPU/连接池超阈值
L2 加分评价摘要、营销 bannerQPS 超 1.2×基准
L3 锦上添花猜你喜欢、个性化弹窗队列积压超 500ms

每次版本上线前我都会让研发对照这张表打勾:每个 L2/L3 的功能是否有「开关」、是否能在 5 秒内降级到位。开关的实现可以是配置中心动态推送、Redis Feature Flag、Lua 脚本判断。

限流与队列:把多余的请求温柔地拒掉

很多人把限流当成「抗流量」的事。其实它更深层的价值是「让请求的速率与系统的处理速率匹配」。常见的算法在另一篇文章里做过对比,这里只讲两个工程实践:

  • 网关层漏桶:以每秒 N 个的恒定速率处理请求,多余的请求直接拒。
  • 业务层令牌桶:允许突发,适应用户的「点开多次」行为。

队列是另一面:当流量超过处理能力但又不能直接拒时(比如搜索建议、价格刷新),可以用「带超时」的缓冲队列,把过老的请求主动过期。这避免了「前端轮询打到后端堆积」。

流量调度:把系统看作一个「可以被绘制的图」

更高级的稳定性来自「在多个机房、多套集群之间切换流量」。我会画一张「机房 - 集群 - 服务 - 实例」的状态图,每 10 秒刷新一次。当某个集群的健康度下降,自动把 5% 的流量切走;当下降到临界点,把 50% 切走;极端情况下,全站切到备份集群。

# 简化的健康度定义(节选)
cluster_health:
  - cluster: cn-east-1
    weight: 100
    score: 0.98
  - cluster: cn-south-1
    weight: 0
    score: 0.72    # 自动降权
    reason: "errors rate elevated"

这套机制看起来复杂,落地其实只是几行 Lua + 一个简单 weight table。重要的是「让它自动生效」,而不是每次出问题都要找运维。

回到「三天抽屉」:复盘比兜底更重要

事前 - 事中 - 事后最容易被忽视的是「事后」。而事后里最容易被忽视的是「机制化复盘」。我的团队每一份事后总结都会带上三个固定栏目:

  1. 时间线:从告警触发到灰度恢复,每个节点时间 + 操作人 + 截图。
  2. 五年后是否还会出现:把这次事故抽象成一句「系统性判断」——比如「熔断阈值是 50%」就是答案,但「没有考虑线程池互相影响」则是新的教训。
  3. follow-up 项:每项必须有 owner + due date + 验收人。

真正的稳定性是「系统不会再让同样的问题第二次伤害你」。这是事后复盘最重要的事。

几条非工程性建议

  • 把每一项防御措施做成可视化。值班同学能在墙上看到当前熔断策略、最近一次演练、过去 7 天的失败率。
  • 每季度做一次「专项演练」。不是压测,而是「提前注入失败」的演练。
  • 永远不要相信「我们不可能出这种问题」。墨菲定律比所有架构图都更可靠。

稳定性不是某次架构升级可以一劳永逸的,它是无数个「如果……那么……」堆出来的工程哲学。做好它,没有捷径。