分布式系统讨论“稳定性”时,经常把高可用与高可靠混在一起。两者确实共享不少指标和工程手段,但关注点并不相同:可用性强调系统有多少时间能够提供服务,可靠性更关注故障发生得是否频繁。一个系统可能每小时故障一次,却能在一秒内恢复,因此可用率看起来很高,但用户仍会持续遇到请求失败。
同样是 99.9%,故障体验可能完全不同
可用性通常可以近似表示为:
Availability = MTBF / (MTBF + MTTR)
其中:
MTBF是平均无故障时间,即两次故障之间平均运行多久。MTTR是平均恢复时间,即故障发生后平均多久恢复服务。
这个公式揭示了两条提高可用性的路径:减少故障频率,或者缩短恢复时间。前者更接近可靠性建设,后者则是高可用架构经常重点解决的问题。
例如,下面两个系统在一天内都中断了约 86 秒:
| 系统 | 故障次数 | 单次持续时间 | 日可用率 |
|---|---|---|---|
| A | 1 次 | 86 秒 | 约 99.9% |
| B | 86 次 | 1 秒 | 约 99.9% |
单看日可用率,两者几乎相同;从可靠性和用户体验看,系统 B 明显更糟。频繁抖动会导致连接重建、请求重试、消息重复消费和告警风暴,还可能把局部故障放大成级联故障。
因此,生产环境不能只保留一个可用率数字。至少还应观察故障次数、故障持续时间、错误率,以及受影响请求数。
高可用战术要同时覆盖检测、隔离和恢复
分布式系统不会因为部署了多个副本就自动获得高可用。完整的故障处理链路通常包括四个动作:
- 检测故障:通过健康检查、超时、心跳和业务指标判断实例是否还能服务。
- 隔离故障:把异常实例移出流量路径,利用熔断或舱壁限制影响范围。
- 恢复服务:重启进程、切换副本、重新调度实例或执行主备切换。
- 修复原因:排查资源耗尽、代码缺陷、依赖抖动和容量不足,降低故障频率。
前三项主要缩短 MTTR,第四项主要提升 MTBF。只做自动重启,系统可能拥有不错的可用率,却长期处于“不断失败、不断拉起”的状态;只追求代码零缺陷,又可能因为缺少自动切换而让一次普通故障持续数小时。
冗余也有明确边界。多个副本如果位于同一台宿主机、共享同一个数据库,或者依赖同一条网络链路,就仍然存在共同故障点。设计时需要按故障域检查副本是否真正独立,包括进程、节点、可用区和外部依赖。
可以这样实践:分别计算可用率与故障频率
下面的 Python 脚本读取一组故障事件,同时计算观察窗口内的可用率、故障次数、平均恢复时间和近似 MTBF。它只使用标准库,可以直接运行。
将以下内容保存为 availability.py,再执行 python3 availability.py:
from dataclasses import dataclass
@dataclass(frozen=True)
class Incident:
started_at: float
recovered_at: float
@property
def duration(self) -> float:
return self.recovered_at - self.started_at
def report(window_seconds: float, incidents: list[Incident]) -> None:
downtime = sum(item.duration for item in incidents)
uptime = max(window_seconds - downtime, 0)
count = len(incidents)
availability = uptime / window_seconds if window_seconds else 0
mttr = downtime / count if count else 0
mtbf = uptime / count if count else uptime
print(f"availability: {availability:.5%}")
print(f"incident count: {count}")
print(f"total downtime: {downtime:.1f}s")
print(f"MTTR: {mttr:.1f}s")
print(f"approximate MTBF: {mtbf:.1f}s")
DAY = 24 * 60 * 60
print("System A: one longer incident")
report(DAY, [Incident(10_000, 10_086)])
print("\nSystem B: many short incidents")
report(DAY, [Incident(i * 900, i * 900 + 1) for i in range(86)])
两个样例的总停机时间和可用率接近,但系统 B 的故障次数远高于系统 A。实际接入监控平台时,可以沿用同样的思路,把事件数据替换为告警记录、探针状态变化或服务端错误窗口。
需要注意,这个脚本假设故障区间互不重叠。生产统计应先合并重叠区间,否则多个实例同时告警可能造成停机时间重复计算。系统级可用性也不应直接由单实例存活时间推导,而应根据用户是否能完成关键请求来判断。
从指标到部署策略
以 Kubernetes 为例,可以这样实践服务副本、健康探针和滚动更新。下面的配置假设应用在 8080 端口提供 /live 与 /ready 接口;部署前需要把镜像地址替换为自己的镜像。
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-api
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: order-api
template:
metadata:
labels:
app: order-api
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: order-api
containers:
- name: api
image: registry.example.com/order-api:1.0.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
failureThreshold: 2
livenessProbe:
httpGet:
path: /live
port: 8080
periodSeconds: 10
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: order-api
spec:
minAvailable: 2
selector:
matchLabels:
app: order-api
kubectl apply -f deployment.yaml
kubectl rollout status deployment/order-api
kubectl get pods -l app=order-api -o wide
kubectl get poddisruptionbudget order-api
这里的就绪探针负责阻止未准备好的实例接收流量,存活探针用于触发失效进程重启,拓扑分布约束减少副本集中在同一节点的风险,PodDisruptionBudget 则约束自愿中断期间的可用副本数量。
这些配置不能单独证明系统已经高可用。探针路径如果依赖所有下游服务,短暂的依赖抖动可能让全部副本同时退出流量;存活探针过于敏感,也会制造重启循环。探针阈值必须结合启动时间、请求延迟和依赖特征通过故障演练校准。
落地时检查这五件事
高可用建设不应止于“部署三个副本”。上线前可以逐项确认:
- SLO 是否基于用户关键请求,而不是仅统计进程存活时间。
- 是否同时记录可用率、错误率、故障次数、MTTR 和受影响请求数。
- 副本是否跨越真实故障域,并识别数据库、缓存和网络等共同依赖。
- 超时、重试、熔断和限流是否配套,重试是否带退避与随机抖动。
- 是否定期执行实例退出、节点故障和依赖超时演练,并验证告警与恢复链路。
高可用与可靠性可以使用相同的冗余、监控和故障处理手段,但不能只用一个百分比评价。缩短恢复时间能减少停机,减少故障次数才能降低系统抖动。成熟的分布式系统需要同时改善 MTTR 与 MTBF,并用用户请求的实际结果验证这些改进。