Skip to content

高可用套路:冗余、故障转移与降级

本文是系统设计系统学习的 L2 核心篇。前置:31. high-performance-patterns。 学完可以配合面试题食用:19-multi-region-active-active-system07-限流系统设计

为什么单个服务不够用:可用性的数学

一台服务器挂掉只是时间问题。硬件故障率是物理规律:一台普通服务器年故障率约 2-5%,10 台机器里有 1 台在一年内出问题的概率接近 20%。

可用性(Availability)用几个 9 衡量:

可用性年停机时间月停机时间典型场景
99% (2 个 9)87.6 小时7.3 小时内部工具
99.9% (3 个 9)8.76 小时43.8 分钟一般 SaaS
99.99% (4 个 9)52.56 分钟4.38 分钟电商/支付
99.999% (5 个 9)5.26 分钟26.3 秒电信/金融核心

从 3 个 9 到 4 个 9,不是优化 10 倍,而是把年故障时间从 8.7 小时压到 52 分钟。单靠"不出错"做不到,唯一正解是冗余——让一个组件挂了,另一个立刻顶上。

冗余的三种半径

机器冗余(N+1)

每台服务器挂掉的概率是 p,N 台同时挂的概率是 p^N。N+1 部署让单台故障不影响整体。典型做法:每层服务至少 2 节点,流量经过 LB 分发,一台下线自动摘除。

同城双活

主备机房间物理距离 10-50km,光纤互连延迟 < 1ms。流量同时写入两个机房,任意一个挂掉能全量切到另一个。代价是流量翻倍(双写)和机房间带宽成本。

异地多活

机房跨城市(上海-北京-深圳),距离 1000km 以上,延迟 10-30ms。核心挑战是数据一致性——跨城延迟强制你做最终一致。典型做法:每个地域有独立副本,本地写本地读,异步复制到其他地域,用业务层的"对账"兜底。

mermaid
graph TB
    subgraph "Zone A (上海)"
        LB1[Load Balancer]
        APP1[App Cluster]
        DB1[(DB Primary)]
    end
    subgraph "Zone B (北京)"
        LB2[Load Balancer]
        APP2[App Cluster]
        DB2[(DB Standby)]
    end
    subgraph "Zone C (深圳)"
        LB3[Load Balancer]
        APP3[App Cluster]
        DB3[(DB Read Replica)]
    end
    
    DNS[Global DNS] --> LB1
    DNS --> LB2
    DNS --> LB3
    DB1 -.->|异步复制| DB2
    DB1 -.->|异步复制| DB3
    DB2 -.->|仲裁| CP[Consensus/仲裁]

故障转移三要素:检测、决策、执行

检测:别等用户报故障

心跳(Heartbeat)是最基础的检测手段:每 1-5 秒发一次 ping,连续 3 次没收到就标记疑似故障。问题在于误判——网络抖动可能让健康节点被摘掉。

好的做法是双向检测:节点也主动上报 "I'm alive"(带 load 信息),超时 + 高负载 同时触达才判定为挂。配合探针(Probe)做业务层面的健康检查,不做 ping 而做一次小查询。

决策:多人投票 vs 一人裁决

  • Quorum 模式:多个节点互相投票,少数服从多数,需要至少 n/2+1 个节点一致。etcd 和 ZK 用这个,前提是节点数必须是奇数。
  • Fencing 模式:发现节点挂掉后,先确保它真的不能再干活(用 STONITH 或租约过期),再切流量。防止"以为挂了但其实还在写数据"导致的脑裂。
  • Raft 的 Lease 机制:Leader 定期续约,不续约就自动降级,Follower 超时后发起选举。不需要额外节点做仲裁。

执行:切换不是简单改 IP

切换涉及:

  1. DNS 刷新:切换域名指向,但 DNS 有 TTL 缓存,生效需要 30-300 秒
  2. VIP 漂移:虚拟 IP 从故障节点漂到新的主节点,推 ARP 通知交换机
  3. 连接迁移:已建立的 TCP 连接会断,需要客户端重连(重试+退避)

依赖治理:弱依赖就该弱处理

一个服务往往依赖多个下游(DB、Cache、MQ、外部 API)。一个弱依赖的雪崩能把整个系统拖死

强依赖 vs 弱依赖梳理

画调用链路图,对每个依赖打标签:

  • 强依赖:依赖挂了,服务不可用(如支付查余额依赖 DB)
  • 弱依赖:依赖挂了,功能降级但不影响核心(如商品详情页的推荐列表)

弱依赖必须加超时+熔断+降级。超时默认 200ms,超过就返回兜底数据(空列表或缓存快照),不阻塞线程。

熔断器三状态

正常 -> 请求失败次数超阈值 -> 熔断开启(直接拒绝请求)
                                    |
                                    v 等待超时(如 5s)
                                    |
                             半开 -> 允许少量请求探测
                                    |
                          成功 -> 恢复,失败 -> 回熔断
java
// 熔断器简化实现
public class CircuitBreaker {
    enum State { CLOSED, OPEN, HALF_OPEN }
    
    private State state = State.CLOSED;
    private int failureCount = 0;
    private int threshold = 5;          // 连续失败 5 次熔断
    private long lastFailureTime;
    private long timeout = 5000;        // 5 秒后尝试半开
    
    public boolean allowRequest() {
        if (state == State.OPEN) {
            if (System.currentTimeMillis() - lastFailureTime > timeout) {
                state = State.HALF_OPEN;
                return true;
            }
            return false;
        }
        return true;
    }
    
    public void recordSuccess() {
        if (state == State.HALF_OPEN) {
            state = State.CLOSED;
        }
        failureCount = 0;
    }
    
    public void recordFailure() {
        failureCount++;
        lastFailureTime = System.currentTimeMillis();
        if (failureCount >= threshold) {
            state = State.OPEN;
        }
    }
}

过载保护:入口限流 + 优先级丢弃

排队 vs 拒绝

系统过载时,继续接受请求只会让事情更糟(队列积压,RT 飙升,最终所有请求都超时,雪崩)。两种策略:

  • 放弃式:直接返回 503,客户端重试退避。适合短时间峰值的场景(如秒杀抢购)。
  • 排队式:返回 202 + 排队号,处理完异步通知。适合可以等几秒的场景(如订单处理)。

优先级丢弃

有限资源留给高价值请求。典型的分级:

  1. P0(交易请求):保,降级任何其他依赖也要保
  2. P1(核心查询):能保尽量保,允许降级为缓存数据
  3. P2(辅助功能):超时即降级,不做重试
  4. P3(非核心):直接丢弃或限流,不计入 SLA

容灾演练:不演练的预案等于没有

运维界有句话:"你不演练,故障就帮你演练"。每个季度至少做一次:

  1. DB 主从切换:手动切一次,记录切换时间 + 数据一致性损失
  2. 机房断网模拟:拔一根核心交换机线,看流量是否自动切到另一个机房
  3. 依赖宕机模拟:关掉 Redis 集群,看服务降级逻辑是否生效,有没有未被兜底的查询打到 DB 把 DB 打挂
  4. 流量突增模拟:压测工具制造 3 倍峰值流量,看限流和熔断是否正常

每次演练产出:问题清单 + 改进项。演练发现问题比出故障时才发现好 100 倍。

常见误区与小结

  • 误区:99.99% 靠优化代码达到。实际上代码优化最多把停机时间从 10 小时缩到 8 小时,从 8 小时到 52 分钟只能靠冗余。
  • 误区:熔断和重试一起用。熔断后重试只会让熔断器一直开着,正确做法是熔断期间不重试,等恢复探测。
  • 误区:异地多活所有节点都写。跨城无可靠的强一致方案,异地多活必须接受最终一致,业务层做冲突检测和对账。
  • 误区:降级就是砍功能。好的降级不砍功能,而是用脏数据/缓存数据代替实时数据,用户感知不到。
  • 误区:故障转移越快越好。切换太快容易误切换(网络抖动被判断为节点挂),建议设置 3 次心跳超时(约 15 秒)后再触发切换。

小结:高可用没有银弹,本质是冗余 + 自动检测 + 快速切换 + 依赖隔离的组合拳。设计任何系统都要问清楚"单一故障点在哪里"、"挂了怎么恢复"。下一篇(33)用 27 个案例串讲读多/写多/低延迟/强一致四类场景,看高可用和高性能怎么绑在一起用。

参考

参考:Google SRE 白皮书(第 4-6 章) 参考:Martin Kleppmann《Designing Data-Intensive Applications》第 8 章 参考:Nginx 健康检查与故障转移配置文档

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1