高可用不是简单增加机器数量,也不是承诺永不故障。它要求系统在组件、网络和依赖必然失败的情况下,仍能在明确目标内提供核心能力,并能被及时检测、隔离和恢复。
问题背景:高可用不是堆机器
两台应用实例如果共享一个没有备份的数据库,单点仍然存在;多个实例部署在同一故障域,也可能同时下线。更隐蔽的风险是逻辑单点:所有请求都依赖同一个配置服务、证书或第三方接口。
设计前先定义目标。可用性指标描述成功请求比例,SLO 给出期望,错误预算允许团队在可靠性和迭代速度之间做可量化取舍。没有口径的“99.99%”只是口号。
先画出故障依赖图
依赖图帮助回答:入口失败是否有备用路径;缓存失败是否拖垮数据库;数据库切换需要多久;外部接口超时时会占满多少线程。每条边都要考虑延迟、错误和部分响应,而不只是“连通或断开”。
冗余与故障转移
冗余要跨越真实故障域,例如进程、主机、机架或可用区。负载均衡器只把流量送给健康实例,但健康检查不能过于敏感,也不能只检查进程端口。
有状态组件的副本还涉及复制延迟、一致性和故障转移。自动切换可能降低恢复时间,却也可能在网络分区时产生双主风险。需要明确仲裁规则、数据恢复点目标和人工介入边界。
备份不等于高可用,但决定灾难后能恢复到哪里。备份必须做恢复演练,仅看到“备份任务成功”并不能证明文件可用。
超时、重试和隔离
没有超时的远程调用可能永久占用连接、线程或协程。调用方的超时应小于上游请求的剩余预算,并在链路中逐层递减。重试只能用于可能恢复且操作幂等的错误,还要使用退避和随机抖动。
如果每一层都重试三次,三层依赖最坏可能把一次请求放大成 27 次调用。更合理的做法是在明确的一层重试,并设置总尝试预算。
隔离可以按线程池、连接池、队列、租户或功能划分资源,避免一个慢依赖耗尽整个进程。熔断器在连续失败时快速拒绝调用,给依赖恢复空间;限流在入口控制系统接受的工作量。
实践:带预算的请求链路
01func FetchProfile(parent context.Context, id string) (*Profile, error) {02 ctx, cancel := context.WithTimeout(parent, 180*time.Millisecond)03 defer cancel()0405 var lastErr error06 for attempt := 0; attempt < 2; attempt++ {07 profile, err := client.Get(ctx, id)08 if err == nil {09 return profile, nil10 }11 lastErr = err12 if !isTransient(err) || ctx.Err() != nil {13 break14 }1516 delay := time.Duration(20+rand.Intn(30)) * time.Millisecond17 select {18 case <-time.After(delay):19 case <-ctx.Done():20 return nil, ctx.Err()21 }22 }23 return nil, lastErr24}
示例最多尝试两次,并且所有尝试共享 180 毫秒总预算。真实系统还要限制并发、记录尝试次数,并确保 Get 不会产生不可重复副作用。不要对认证失败、参数错误等永久错误重试。
降级、恢复与演练
降级先保护核心路径。例如推荐服务不可用时返回空推荐,不应阻止用户查看订单;统计写入失败可以进入缓冲,但扣款结果不能静默丢弃。降级响应要可识别,防止错误数据被当作正常结果长期缓存。
恢复过程同样可能制造冲击:大量实例同时重连数据库、缓存同时失效、积压消息被瞬间消费,都会形成恢复风暴。连接和任务恢复需要限速、抖动和容量保护。
故障演练从小范围开始,明确停止条件、观察指标和回滚方法。演练目的是验证假设和手册,不是制造戏剧性事故。
容量、安全与总结
容量设计要覆盖稳定负载、峰值、单故障后的剩余容量以及依赖退化时的资源占用。压测必须使用隔离环境和脱敏数据,生产环境测试需要严格审批和停止机制。
安全控制也要高可用:密钥轮换、证书更新和权限服务故障都可能中断业务;但不能为了可用性在依赖失败时默认放行。
高可用的实施顺序是:定义目标,画出依赖,消除关键单点,为调用设置预算和隔离,设计可控降级,再通过观测与演练验证。下一步可继续学习限流熔断、分布式一致性和可观测性。