Skip to content
Charles Shao
Go back

高可用系统设计:从几个 9 到限流、依赖韧性与发布安全

views

高可用(High Availability,HA)描述的是系统在大部分时间都能对外提供服务——即便硬件故障、流量突增或正在升级,用户仍能访问核心能力。业界常用「几个 9」量化可用性,也可用失败请求占比来刻画。

本篇是高可用系列开篇:先讲清可用性怎么量、什么会把系统打挂,再把代码质量、集群冗余、限流、依赖韧性、幂等一致、发布安全、可观测性与混沌演练等手段过一遍地图,最后给出中等规模系统的优先级排序。后续七篇会把其中几块拆开细讲;异步削峰、缓存与压测等见高性能系列

TL;DR

Table of contents

Open Table of contents

1. 可用性:几个 9 与失败率

高可用指系统在大部分运行时间内持续对外提供服务的能力,包括在硬件故障或系统升级时仍能保持核心功能可用。

业界通常以**「几个 9」**衡量可用性,每多一个 9,对架构容错能力的要求就上一个台阶:

可用性年允许停机时间月允许停机时间典型适用场景
99%(2 个 9)≈ 87.6 小时≈ 7.3 小时内部工具、低频后台服务
99.9%(3 个 9)≈ 8.7 小时≈ 43 分钟中等规模 B 端 SaaS
99.99%(4 个 9)≈ 52 分钟≈ 4.4 分钟中大型 C 端平台
99.999%(5 个 9)≈ 5 分钟≈ 26 秒金融核心交易、电信基础设施

这张表传递的最重要信息是**“每多一个 9 的代价”**:从 3 个 9 升到 4 个 9,允许的年停机时间从 8.7 小时压缩到 52 分钟,意味着每一次 30 分钟的计划维护窗口都需要做到零中断发布,每一次非计划故障都必须在分钟内恢复——对应的是完全不同量级的架构投入。在设定可用性目标时,先看业务真正需要什么级别,再估算达到该级别的工程成本,而不是默认追求最高等级。

此外,可用性也可用失败请求占比来刻画:若对某接口发起 1000 次请求,其中 10 次失败,则该接口可用性为 99%。这种按请求粒度的衡量方式在微服务场景下更精确,可以对每个关键接口单独设定 SLO(Service Level Objective)。

把「几个 9」换算成错误预算(Error Budget),可用性目标就从一个抽象数字变成了可支配的容错额度:

// 从「几个 9」换算月度错误预算
availability     = 0.999                 // 3 个 9
errorBudget      = 1 - availability       // 0.1% 允许失败
minutesPerMonth  = 30 * 24 * 60           // 43200
budgetMinutes    = errorBudget * minutesPerMonth   // ≈ 43 分钟 / 月

// 按请求粒度:设月请求量 1e9
allowedFailures  = 1e9 * errorBudget      // = 100 万次失败预算 / 月

这份预算既是「还能出多少故障」的余量,也是发布决策的开关——预算充足时可以大胆灰度,预算耗尽时应冻结变更。

可用性几个 9 与年停机时间对照:每多一个 9,允许的不可用时间大约缩短一个数量级

每多一个 9,允许的年停机时间大约缩短一个数量级。

2. 不可用的常见根因

导致系统不可用的原因可以归纳为以下几类:

  1. 恶意攻击:DDoS、CC 攻击等将带宽或连接数打满。
  2. 硬件故障:服务器、磁盘、网卡等物理设备损坏。
  3. 流量激增:并发量超出系统承载上限,导致整体或部分服务不可用。
  4. 代码缺陷:内存泄漏、循环依赖、死锁等问题使进程崩溃或性能急剧下降。
  5. 关键组件故障:Nginx、数据库、缓存等基础设施的单点不可用,拖垮整条链路。
  6. 自然或人为灾害:火灾、地震、断电等不可抗力因素。

一句话:单点是可用性的最大敌人——无论是代码、组件还是机房,任何没有冗余的环节都是潜在的单点故障(SPOF)。

3. 高可用工具箱

高可用手段全景:从代码质量、集群冗余到限流、超时重试、熔断、异步、缓存与灰度发布

高可用不是单点技巧,而是从代码到发布的一整套工具箱。

下面按源头预防 → 流量防护 → 故障隔离 → 兜底恢复的顺序逐一展开,也是 3.1~3.10 的编号顺序:越靠前的手段投入产出比越高,应优先做扎实。

3.1 代码质量:最便宜的防线

一句话:限流、降级、熔断再花哨,也补不回源头的内存泄漏与坏味道——代码质量是高可用的第一道闸。

内存泄漏、循环依赖等代码缺陷是可用性最隐蔽的破坏者。限流和熔断是流量侧的止损手段,但无法修复应用层的根本性缺陷。应首先通过 CodeReview静态分析将问题消灭在源头,而不是依赖运行时保护机制兜底。

以下工具对提升 Java 项目代码质量有实际效果:

测试覆盖要有针对性:单元测试覆盖核心业务逻辑,集成测试验证服务间的交互边界,混沌工程(随机注入延迟、宕机)验证系统的容错路径是否真正生效。高可用的”测试”不只是功能正确性测试,更是”故障下系统的行为”测试。

3.2 集群冗余:消灭单点

单实例一旦挂掉,整条服务链路就会中断。将同一服务部署多副本,并通过负载均衡分发流量,可以在单节点故障时自动切换,大幅缩短不可用时间。

以 Redis 为例:单实例宕机会导致缓存层整体失效;而在 Sentinel 模式或 Cluster 模式下,主节点故障后可在秒级内完成自动切换,对上游几乎无感知。

单点故障 vs 集群对比:单点 Redis 挂掉导致服务不可用,集群中一节点故障可自动切换

单点挂掉整条链路不可用;集群里一台挂了,另一台可以顶上。

消灭单点要系统性地排查:不只是应用服务,数据库、缓存、消息队列、网关、DNS 都可能存在单点。可以用”单点风险清单”逐项梳理:每个核心组件是否有冗余实例?故障时是否有自动切换机制?切换后是否已经验证过?这项工作对新系统尤为重要,很多线上事故都源于”被遗忘的单点”。冗余设计的详细策略见冗余与容灾

集群冗余能生效的前提是健康检查真的反映业务健康:负载均衡器只有在探测到实例不健康时才会把它摘除。一个只返回 200 的「浅健康检查」会漏判——进程还活着,但下游 Redis 已经连不上了。健康检查应校验关键依赖,并把「存活(liveness)」与「就绪(readiness)」分开:

// /health:深度健康检查,校验关键依赖,供 LB / 编排系统决定是否摘流量
@GetMapping("/health")
public ResponseEntity<Health> health() {
    boolean redisOk = redis.ping();          // 竞价强依赖:预算 / 频控快照
    boolean dbOk    = db.validate(200);       // 200ms 内能否拿到连接
    if (redisOk && dbOk) {
        return ResponseEntity.ok(Health.up());
    }
    // 依赖不可用时返回 503:LB 立即摘流量,而不是让请求进来后再失败
    return ResponseEntity.status(503).body(Health.down(redisOk, dbOk));
}

注意别把健康检查做得过重:探测过慢或过于严格,会在依赖轻微抖动时把整个集群同时判死、引发「健康检查风暴」。就绪探针可以适当宽松,存活探针只判进程本身,避免因外部依赖抖动而被反复重启。

3.3 限流:挡住瞬时洪峰

**流量控制(Rate Limiting)**通过监控 QPS 或并发线程数等指标,在达到阈值时对超额流量进行拒绝或排队,防止系统被瞬时流量峰值冲垮,从而保障应用的高可用性。

常用框架:Sentinel(Alibaba,功能丰富)、Resilience4j(轻量级,Spring Boot 友好)。限流算法与实现细节见服务限流

限流的配置需要配合压测数据:限流阈值不是凭感觉设置的——应该先通过压测找到系统的承载上限(饱和点),再把限流阈值设置在饱和点的 70%~80%,为抖动留出余量。在没有压测基准的情况下设限流,要么阈值过高保护不住,要么过低误拒正常流量。阈值设定的完整方法与分布式限流落地见服务限流

3.4 超时与重试:防止请求堆积

远程调用必须设置超时:请求超过阈值未得到响应则主动取消,避免连接堆积拖垮整个服务。超时分连接超时(ConnectTimeout)和读取超时(ReadTimeout)两类,缺一不可。

重试配合超时使用,针对瞬态故障提高成功率;次数建议不超过 3 次,并结合幂等设计防止重复执行。在调用链中还需注意超时预算的层级传递——每一跳的超时必须小于上游,否则上游已超时断连,下游仍在空跑浪费资源。详见依赖韧性

最常见的错误是”不设超时”或”设了但太长”:一个 30 秒的读超时,在高并发下等于没设——每个慢请求都会占用线程 30 秒,很快就把线程池填满。超时是”强制止血”,不是”礼貌等待”,要设得让人觉得”是不是有点短”才可能接近合理值。

超时预算要沿调用链逐跳递减:入口拿到的总预算(如竞价的 tmax)应在每一跳扣掉已耗时间后再传给下游,而不是每层各自写死一个固定值。否则下游用满自己的超时时,上游其实早已断连、白白空跑:

// 逐跳传递「剩余预算」,每一跳的超时都不超过上游剩余时间
long remainingMs(long deadlineNs) {
    return Math.max(0, (deadlineNs - System.nanoTime()) / 1_000_000L);
}

Response callDownstream(Request req, long deadlineNs) {
    long budget = remainingMs(deadlineNs);
    if (budget <= MIN_USEFUL_MS) {          // 剩余时间已不足以完成调用
        return Response.degraded();          // 直接降级,不再发起注定超时的请求
    }
    // 给下游的超时 = min(本跳上限, 剩余预算),确保逐跳收敛
    int timeout = (int) Math.min(HOP_TIMEOUT_MS, budget);
    return client.call(req, timeout);
}

3.5 熔断:切断故障扩散

当下游服务持续超时或失败率超过阈值,熔断器(Circuit Breaker)会主动切断对该服务的调用,直接返回 fallback 结果,快速释放资源、防止雪崩。一段时间后熔断器进入半开状态试探恢复,成功则关闭。常用框架:Sentinel、Resilience4j。详见依赖韧性

熔断的价值在于主动”止血”:超时是被动等待,熔断是主动切断。当下游已经明确处于故障状态,继续发请求只是白白占用资源——熔断器通过统计错误率,在检测到故障后立即把后续请求转向 fallback,让线程资源得以快速回收,上游服务保持可用。

熔断器的 fallback 设计和参数调优同样关键:阈值太低会频繁误触发,fallback 设计不当会让用户感受到大范围功能缺失。作为地图,这里只点出关键参数就三个——错误率 / 慢调用率阈值、统计窗口大小,以及最小请求量(下一节)。完整的熔断配置示例(Resilience4j YAML / Sentinel)、Half-Open 探测节奏与 fallback 编排,见依赖韧性

3.6 冷启动:样本不足时的误熔断

一个容易被忽视的场景:服务刚启动或滚动发布的新实例请求量很低,几个超时就可能凑到「错误率 50%」的阈值而被误熔断,陷入「刚起来就被熔断」的尴尬。解法是配 最小请求量(Resilience4j 的 minimumNumberOfCalls、Sentinel 的 minRequestAmount)——统计窗口内样本不足时不参与错误率判定。广告竞价接入层扩容或滚动发布时新实例频繁冷启动,这个参数尤其关键。

3.7 异步化:削峰与解耦

将非核心处理逻辑从请求链路中剥离,通过消息队列(如 Kafka、RocketMQ)异步消费,可以:

异步与削峰的完整模式(队列缓冲、背压、适用边界)见异步与削峰。需注意:异步化后业务流程需相应调整——例如订单提交不能立即返回”成功”,应等消费者真正处理完毕后再通过通知告知用户。同时,消息队列本身也需要做高可用部署(多副本、持久化),否则队列宕机会导致所有异步流程停顿。

适合异步化的场景:发送通知(短信/邮件/推送)、更新统计数据、触发下游系统同步、写审计日志。不适合异步化的场景:需要在当次请求中给用户返回处理结果、对数据一致性有强实时要求的操作。

3.8 缓存:降低读路径压力

高并发场景下,大量读请求直接打到数据库容易将其压垮。将热点数据缓存在内存(Redis、Memcached)中,可将读延迟从毫秒级降低到微秒级,同时显著降低数据库负载。

使用缓存时需注意三个经典问题:

3.9 缓存高可用:避免新的单点

缓存能大幅降低数据库压力,但缓存层本身也需要做高可用——否则缓存宕机会引发”缓存雪崩”,数据库瞬间面临缓存全量流量的冲击,往往比没有缓存时更难恢复。

主要防护措施:

3.10 监控、备份与灰度发布

4. 落地优先级:中等规模系统怎么排

高可用手段很多,但精力和工程资源有限——先做什么、后做什么,直接决定投入产出比。以下建议适用于日活百万量级以下、团队规模 10~50 人的系统:

第一优先级:消除明显缺陷(成本最低,效果最直接)

第二优先级:防止流量冲击(大促或爆发性流量前必做)

第三优先级:提升可观测性(让故障可以被快速发现和定位)

第四优先级:发布安全与数据保护

不要急着做的(确认前三级稳固后再考虑)

判断标准:用”如果现在有一个核心服务宕机,需要多少分钟才能恢复、需要几个人介入”来评估当前系统的实际高可用水平。如果答案是”30 分钟以上、需要多人”,说明基础工作还没做到位,优先补基础而非上复杂方案。

一个有用的思维框架:高可用的投入应该先解决”概率高的小故障”(单节点宕机、网络抖动、流量小峰值),再解决”概率低的大故障”(机房级故障、大规模 DDoS)。前者每周可能发生,效益立竿见影;后者可能一年都不会遇到,但一旦发生影响巨大,在具备了基础防御能力后再投入。

不同规模系统的可用性重点

系统规模日活/请求量重点
早期<10 万 DAU代码质量 + 最基本的超时配置
成长期10~100 万 DAU集群冗余 + 限流 + 熔断 + 监控告警
中等规模100 万~1000 万 DAU同城多活 + 细粒度隔离 + 压测常态化
大规模>1000 万 DAU异地多活 + 混沌工程 + SLO 驱动的全链路可观测性

5. 反模式

5.1 常见反模式

小结

高可用不是单一技术,而是从代码质量 → 冗余部署 → 流量保护 → 可观测性 → 发布策略逐层叠加的能力体系。每一层都有专篇展开:

机房选在哪、跨区多活怎么部署,是可用性决策的地理维度,归在容器化与 K8s 部署系列里的数据中心选型,可作延伸阅读。

从可用性目标反推工程投入,而不是追求所有手段一步到位——这是高可用建设最重要的原则。

高可用是一个持续演进的过程,不是一次性项目。系统的可用性瓶颈会随着规模增长而转移(今天的瓶颈是单点数据库,明天可能是网络带宽,后天可能是服务发现),保持对瓶颈的持续度量和识别,比一次性上齐所有手段更重要。

最后,可用性指标需要从用户视角来定义,而不仅是服务器视角。服务器显示”正常运行”,但用户请求成功率只有 95%,意味着 5% 的用户每 20 次操作就会遇到一次失败——这对用户来说就是”不可用”。以用户实际体感到的成功率作为 SLO 的基准,远比”服务进程是否存活”更有意义。


views
Share this post on:

Previous Post
冗余与容灾:HA 集群、同城异地灾备与多活选型