高可用(High Availability,HA)描述的是系统在大部分时间都能对外提供服务——即便硬件故障、流量突增或正在升级,用户仍能访问核心能力。业界常用「几个 9」量化可用性,也可用失败请求占比来刻画。
本篇是高可用系列开篇:先讲清可用性怎么量、什么会把系统打挂,再把代码质量、集群冗余、限流、依赖韧性、幂等一致、发布安全、可观测性与混沌演练等手段过一遍地图,最后给出中等规模系统的优先级排序。后续七篇会把其中几块拆开细讲;异步削峰、缓存与压测等见高性能系列。
TL;DR
- 可用性常用「几个 9」衡量:每多一个 9,允许的年停机时间大约缩短一个数量级;也可用失败次数 / 总请求衡量。
- 不可用来源很杂:攻击、硬件故障、流量激增、代码缺陷、关键组件挂掉、灾害等,都可能让服务下线。
- 源头先抓代码质量:内存泄漏、循环依赖等比「事后限流」更伤可用;CodeReview 与静态分析是最便宜的防线。
- 集群消灭单点:同一服务多副本,一台挂了另一台顶上——详见冗余与容灾。
- 限流挡住瞬时洪峰;依赖韧性(超时 / 重试 / 熔断 / 降级 / 隔离)挡住雪崩——见服务限流与依赖韧性。
- 重试与异步要求幂等:否则容错手段本身会制造脏数据——见幂等与一致性。
- 发布与变更安全往往比架构更致命:灰度、金丝雀与可回滚——见发布与变更安全。
- 预案要靠演练验证:混沌工程主动注入故障,证明容错真的生效——见混沌工程。
- 异步、缓存、压测更多在高性能侧展开;本系列收束在可用性与容灾。
- 优先级建议:代码质量 → 集群冗余 → 超时 → 限流 → 熔断隔离 → 幂等 → 监控告警/SLO → 灰度发布 → 混沌演练。
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,允许的年停机时间大约缩短一个数量级。
2. 不可用的常见根因
导致系统不可用的原因可以归纳为以下几类:
- 恶意攻击:DDoS、CC 攻击等将带宽或连接数打满。
- 硬件故障:服务器、磁盘、网卡等物理设备损坏。
- 流量激增:并发量超出系统承载上限,导致整体或部分服务不可用。
- 代码缺陷:内存泄漏、循环依赖、死锁等问题使进程崩溃或性能急剧下降。
- 关键组件故障:Nginx、数据库、缓存等基础设施的单点不可用,拖垮整条链路。
- 自然或人为灾害:火灾、地震、断电等不可抗力因素。
一句话:单点是可用性的最大敌人——无论是代码、组件还是机房,任何没有冗余的环节都是潜在的单点故障(SPOF)。
3. 高可用工具箱
高可用不是单点技巧,而是从代码到发布的一整套工具箱。
下面按源头预防 → 流量防护 → 故障隔离 → 兜底恢复的顺序逐一展开,也是 3.1~3.10 的编号顺序:越靠前的手段投入产出比越高,应优先做扎实。
3.1 代码质量:最便宜的防线
一句话:限流、降级、熔断再花哨,也补不回源头的内存泄漏与坏味道——代码质量是高可用的第一道闸。
内存泄漏、循环依赖等代码缺陷是可用性最隐蔽的破坏者。限流和熔断是流量侧的止损手段,但无法修复应用层的根本性缺陷。应首先通过 CodeReview 和静态分析将问题消灭在源头,而不是依赖运行时保护机制兜底。
以下工具对提升 Java 项目代码质量有实际效果:
- Arthas:Alibaba 开源的 Java 诊断工具,可在不重启的情况下诊断线上问题。
- 阿里巴巴 Java 代码规范(p3c):覆盖命名、并发、异常处理等常见坑点的规约集。
- IntelliJ IDEA 内置代码分析:实时检测 NullPointerException 风险、资源未关闭等常见问题。
测试覆盖要有针对性:单元测试覆盖核心业务逻辑,集成测试验证服务间的交互边界,混沌工程(随机注入延迟、宕机)验证系统的容错路径是否真正生效。高可用的”测试”不只是功能正确性测试,更是”故障下系统的行为”测试。
3.2 集群冗余:消灭单点
单实例一旦挂掉,整条服务链路就会中断。将同一服务部署多副本,并通过负载均衡分发流量,可以在单节点故障时自动切换,大幅缩短不可用时间。
以 Redis 为例:单实例宕机会导致缓存层整体失效;而在 Sentinel 模式或 Cluster 模式下,主节点故障后可在秒级内完成自动切换,对上游几乎无感知。
单点挂掉整条链路不可用;集群里一台挂了,另一台可以顶上。
消灭单点要系统性地排查:不只是应用服务,数据库、缓存、消息队列、网关、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)中,可将读延迟从毫秒级降低到微秒级,同时显著降低数据库负载。
使用缓存时需注意三个经典问题:
- 缓存穿透:查询不存在的数据,每次都透传到数据库。解决:空值缓存或布隆过滤器。
- 缓存击穿:热点 Key 过期瞬间,大量并发请求涌入数据库。解决:互斥锁重建缓存,或设置永不过期 + 后台异步刷新。
- 缓存雪崩:大批 Key 同时过期,或 Redis 宕机,大量请求瞬间打到数据库。解决:Key 的过期时间加随机抖动,Redis 做高可用集群部署。
3.9 缓存高可用:避免新的单点
缓存能大幅降低数据库压力,但缓存层本身也需要做高可用——否则缓存宕机会引发”缓存雪崩”,数据库瞬间面临缓存全量流量的冲击,往往比没有缓存时更难恢复。
主要防护措施:
- Redis 主从 + Sentinel / Cluster:消灭缓存层单点,主节点故障后秒级自动切换到从节点。
- 本地缓存作为兜底:在 Redis 不可用时,JVM 本地缓存(Caffeine、Guava Cache)可短暂承接热点数据的读取,为 Redis 恢复争取时间。本地缓存容量有限、节点间不同步,适合作为”最后一层”而非主缓存。
- 回源限流:当缓存未命中时,使用分布式锁或令牌桶控制同时回源数据库的请求数(“缓存重建限流”),防止缓存击穿时的数据库瞬间过载。
3.10 监控、备份与灰度发布
- 核心链路优先保障硬件资源:关键服务使用更高规格的机器,避免与旁路服务竞争 CPU、内存、磁盘 I/O。在混部场景下,通过 cgroups 或容器 resource limits 为核心服务设置资源保障。
- 监控与告警:对 CPU、内存、连接数、P99 延迟、错误率等核心指标设置告警,做到故障前预警。告警的关键是”有效性”:告警太多会导致告警疲劳,运维人员开始忽略告警;应明确每条告警的 SOP(标准处理流程),确保告警能落地为行动。
- 备份与快速回滚:定期备份数据并演练恢复流程;发布时保留上一版本制品,出问题能在分钟级内回滚。代码回滚要比配置回滚更谨慎——有时数据库已做了 Schema 变更,代码回滚可能导致旧代码无法读取新 Schema。
- 灰度发布:将服务器集群分批次发布,先发一小部分机器(如 5%~10%),观察错误率、延迟等指标稳定后再全量推进;期间若发现问题,只需回滚已发布的那批服务器,影响面可控。金丝雀发布(canary release)是灰度的典型实现,配合自动化的发布流水线可以做到分钟级自动回滚。
- 定期检查硬件:自建机房环境下,应定期检查并替换老化硬件,避免突发故障。
- 故障回顾(Post-mortem):每次生产故障后进行无责回顾(blameless post-mortem),记录根因、影响范围、恢复时间,并跟踪改进措施落地。高可用能力的提升往往来自对历史故障的系统性学习,而不是靠预防性设计全面覆盖所有可能的故障场景。
4. 落地优先级:中等规模系统怎么排
高可用手段很多,但精力和工程资源有限——先做什么、后做什么,直接决定投入产出比。以下建议适用于日活百万量级以下、团队规模 10~50 人的系统:
第一优先级:消除明显缺陷(成本最低,效果最直接)
- 代码 Review 制度化,静态分析工具接入 CI
- 所有远程调用配置 ConnectTimeout + ReadTimeout(哪怕先用一个”够短但不算太短”的默认值)
- 核心组件(数据库、缓存、消息队列)消灭单点,最低做到主备自动切换
第二优先级:防止流量冲击(大促或爆发性流量前必做)
- 压测找到系统承载上限,基于压测结果配置入口限流
- 配置熔断器保护核心依赖,至少对数据库和核心下游服务做基本熔断
- 核心服务使用独立线程池,避免共享池被单个依赖拖垮
第三优先级:提升可观测性(让故障可以被快速发现和定位)
- 关键指标告警(错误率、P99 延迟、连接数),配套 SOP 文档
- 分布式链路追踪(如 SkyWalking、Jaeger),能在 5 分钟内定位跨服务故障
- 结构化日志,支持按 requestId 全链路追查
第四优先级:发布安全与数据保护
- 灰度发布流水线,每次发布先灰度 5%~10%
- 定期演练数据库备份恢复和 Redis 故障切换
不要急着做的(确认前三级稳固后再考虑)
- 异地多活:工程复杂度极高,中等规模系统通常同城多活已足够
- 服务网格(Service Mesh):引入新的运维复杂度,价值在规模较大时才显现
判断标准:用”如果现在有一个核心服务宕机,需要多少分钟才能恢复、需要几个人介入”来评估当前系统的实际高可用水平。如果答案是”30 分钟以上、需要多人”,说明基础工作还没做到位,优先补基础而非上复杂方案。
一个有用的思维框架:高可用的投入应该先解决”概率高的小故障”(单节点宕机、网络抖动、流量小峰值),再解决”概率低的大故障”(机房级故障、大规模 DDoS)。前者每周可能发生,效益立竿见影;后者可能一年都不会遇到,但一旦发生影响巨大,在具备了基础防御能力后再投入。
不同规模系统的可用性重点:
| 系统规模 | 日活/请求量 | 重点 |
|---|---|---|
| 早期 | <10 万 DAU | 代码质量 + 最基本的超时配置 |
| 成长期 | 10~100 万 DAU | 集群冗余 + 限流 + 熔断 + 监控告警 |
| 中等规模 | 100 万~1000 万 DAU | 同城多活 + 细粒度隔离 + 压测常态化 |
| 大规模 | >1000 万 DAU | 异地多活 + 混沌工程 + SLO 驱动的全链路可观测性 |
5. 反模式
5.1 常见反模式
- 只追求最高等级的 9:不区分链路一律上 5 个 9,在低优链路浪费投入、在核心链路却因基础不牢仍然掉链子。先按链路 / 业务重要性分级设 SLO。
- 「开了集群」就以为高可用:部署了多副本但没有健康检查和自动切换,故障时仍需人工介入——这只是「多机部署」,不是高可用。
- 不设超时或超时过长:一个 30s 的读超时在高并发下等于没设,慢下游会拖垮整个线程池(见依赖韧性的共享线程池雪崩与舱壁隔离)。
- 限流阈值拍脑袋:没有压测基准就设限流,要么保护不住要么误杀正常流量(见 §3.3)。
- 缓存 Redis 留单点:把强依赖的 Redis 当成「不会挂」的组件,一挂就雪崩。
- 容灾只在文档里:预案从不演练,真出事时才发现 DNS TTL、客户端重连、告警路径全是坑。
- 可用性只看服务器视角:进程「存活」但用户成功率只有 95%,对用户就是不可用——SLO 要以用户实际成功率为基准。
小结
高可用不是单一技术,而是从代码质量 → 冗余部署 → 流量保护 → 可观测性 → 发布策略逐层叠加的能力体系。每一层都有专篇展开:
- 冗余与容灾:HA 集群、灾备与多活,RTO/RPO 驱动决策
- 服务限流:令牌桶、滑动窗口与分布式落地
- 依赖韧性:超时重试、熔断降级、Failover 与舱壁隔离
- 幂等与一致性:重试与异步的正确性前提
- 发布与变更安全:灰度 / 蓝绿 / 金丝雀与回滚
- 混沌工程:主动注入故障,验证容错真的生效
机房选在哪、跨区多活怎么部署,是可用性决策的地理维度,归在容器化与 K8s 部署系列里的数据中心选型,可作延伸阅读。
从可用性目标反推工程投入,而不是追求所有手段一步到位——这是高可用建设最重要的原则。
高可用是一个持续演进的过程,不是一次性项目。系统的可用性瓶颈会随着规模增长而转移(今天的瓶颈是单点数据库,明天可能是网络带宽,后天可能是服务发现),保持对瓶颈的持续度量和识别,比一次性上齐所有手段更重要。
最后,可用性指标需要从用户视角来定义,而不仅是服务器视角。服务器显示”正常运行”,但用户请求成功率只有 95%,意味着 5% 的用户每 20 次操作就会遇到一次失败——这对用户来说就是”不可用”。以用户实际体感到的成功率作为 SLO 的基准,远比”服务进程是否存活”更有意义。