跳转到内容

分布式系统与高可用

系统变大后,多出来的不是“更多服务器”,而是更多失败方式。一次本地函数调用变成网络请求,一次数据库事务变成多个服务之间的状态协调,一台机器故障可能影响整条链路。架构设计的目标不是消灭故障,而是让系统在局部失败时仍然可预测、可恢复。

1. 什么时候需要改变架构

跳转到“1. 什么时候需要改变架构”

单体不是落后,微服务也不是成熟标志。选择取决于:

  • 团队规模和所有权边界。
  • 模块是否需要独立发布或扩缩容。
  • 故障是否需要隔离。
  • 数据量、请求量和延迟目标。
  • 部署、观测和运维能力。
  • 变更频率与回滚成本。

如果一个系统只有一个团队、一个数据库、业务边界还不稳定,模块化单体通常比拆成十几个服务更容易演进。拆服务会把本地调用、事务和代码依赖变成分布式问题。

1.1 单体、模块化单体与微服务

跳转到“1.1 单体、模块化单体与微服务”
形态 优势 代价
单体 部署简单、调用直接、事务容易 模块边界易腐化,整体扩缩容
模块化单体 保留单部署优势,边界更清楚 仍共享进程与数据库,隔离有限
微服务 独立部署、按需扩容、团队自治 网络、数据一致性、运维和追踪复杂

拆分应按业务能力和数据所有权,而不是按技术层拆成“用户服务、日志服务、工具服务”。一个服务应能独立回答一组业务问题。

从单体迁移时,不必一次性重写。可以在边界处建立路由,把一部分功能逐步迁移到新服务,验证稳定后再扩大范围。旧的实现继续运行,直到新实现完全替代。这样能控制风险,但短期内需要双系统一致性与流量切换。

2. 分布式系统的基本事实

跳转到“2. 分布式系统的基本事实”

本地调用通常假设:

  • 方法会返回。
  • 返回只有成功或异常。
  • 调用耗时稳定。
  • 双方看到同一份内存。
  • 时钟和顺序一致。

跨网络后,这些假设都不再可靠:

  1. 网络可能延迟、丢包、重复或分区。
  2. 请求可能到达但响应丢失,调用方无法确定结果。
  3. 对方可能很慢,而不是直接失败。
  4. 多个服务可能看到不同的数据版本。
  5. 机器时钟可能漂移。
  6. 节点可能重启,内存状态丢失。

这就是为什么远程调用必须有超时、幂等、重试和可观测性。一个“没有响应”的请求可能根本没到,也可能已经成功执行。

CAP 常被简化成“一致性、可用性、分区容忍三选二”。更准确地说:网络分区无法避免,分区发生时,系统必须在一致性和可用性之间做取舍。

  • CP:分区时拒绝部分请求,保证不返回可能错误的数据。
  • AP:分区时继续响应,但可能返回旧数据,之后通过复制收敛。

正常网络下,系统可以同时追求较好的可用性和一致性;CAP 讨论的是分区期间的保证。

模型 含义
强一致 读取总能看到最新已提交写入
线性一致 所有操作像在单一副本上按实时顺序执行
顺序一致 所有节点看到相同顺序,但不一定与真实时间一致
最终一致 没有新写入后,副本最终收敛到相同状态
读己之写 用户能读到自己刚写入的结果
单调读 用户不会先看到新值又读回旧值

业务通常不需要所有数据都强一致。账户余额、库存扣减和权限可能要求更强保证;头像、统计和推荐可以接受短暂陈旧。关键是逐项声明,而不是给整个系统贴一个标签。

复制提高可用性和读能力,也带来延迟:

  • 主从复制:写主库,异步或同步复制到从库。
  • 多主复制:多个节点都可写,冲突解决更复杂。
  • 无主复制:客户端向多个副本读写,用 quorum 判断一致性。

同步复制延迟更高但减少丢失,异步复制响应更快但主库故障时可能丢最近写入。副本延迟还会导致读到自己刚写数据失败。

当多个节点需要对同一顺序达成一致,需要共识算法,例如 Raft、Paxos。共识能处理领导者选举和日志复制,但需要多数节点可用。三个节点通常能容忍一个故障;节点越多,写协调成本越高。

应用开发者不一定要实现共识,但要理解:依赖的数据库、配置中心或协调服务可能因为失去多数派而拒绝写入。

协调者先让所有参与者准备,再统一提交或回滚。它能提供较强原子性,但协调者故障、锁持有时间和网络分区会让系统阻塞,跨组织系统尤其困难。

把长事务拆成一系列本地事务,每一步都有补偿动作:

创建订单 → 预占库存 → 扣款 → 发货
↘ 失败补偿 ← 释放库存 ← 退款

补偿不一定等于“恢复原状”。例如短信已经发送,无法真正撤回。Saga 需要幂等、可重试、可观测,并明确中间状态对用户意味着什么。

数据库写入和消息发送无法简单放进同一个事务。Outbox 模式先在业务数据库的 outbox 表写入事件,再由后台进程读取并发布:

业务写入 + outbox 写入(同一本地事务)
↓
投递者读取 outbox
↓
消息队列/事件总线

消费者仍需幂等,因为投递通常是至少一次。

“几个 9”表示允许的不可用时间比例:

可用性 年停机时间(约)
99% 3.65 天
99.9% 8.77 小时
99.99% 52.6 分钟
99.999% 5.26 分钟

指标必须对应服务、地区和测量方式。一个 API 返回 500 很快,不代表用户实际可用;前端依赖第三方失败也可能让整页不可用。

  • 多实例:单机故障时由其他实例接管。
  • 多可用区:机房级故障时跨区切换。
  • 多地域:地理灾害或区域网络故障时的容灾。

故障转移要回答:

  • 健康检查怎样判断“可用”?
  • 切换需要多久?
  • 新节点是否有足够容量接管全部流量?
  • 配置、密钥、数据复制是否同步?
  • 切回来是否会引发二次故障?
  • RPO(恢复点目标):最多能接受丢失多长时间的数据。
  • RTO(恢复时间目标):故障后多久必须恢复服务。

备份频率决定 RPO,恢复流程和基础设施决定 RTO。只备份不演练,无法证明 RTO。恢复演练应包含数据库、对象存储、配置、密钥和应用版本。

健康检查不能只看进程是否活着。一个进程可能还在,但数据库连接池耗尽、队列堵塞或依赖不可用。同时,健康检查也不能过度依赖外部系统,否则依赖抖动会把所有实例同时标记为不健康。

常见拆分:

  • 存活检查:进程是否需要重启。
  • 就绪检查:实例是否能接收流量。
  • 启动检查:慢启动应用是否完成初始化。
模式 目的 主要风险
超时 防止无限等待 设得太短会误杀正常请求
重试 处理瞬时故障 放大流量、重复副作用
退避与抖动 避免同时重试 需要最大次数和截止时间
熔断 依赖持续失败时停止调用 恢复时机和误判
舱壁 隔离资源池 容量划分与利用率
限流 控制进入速率 误伤、公平性和重试
降级 保留核心功能 用户是否知道结果不完整
负载卸载 主动拒绝超额请求 阈值和优先级

重试必须结合幂等和预算。一个请求最多重试几次、每次多久、整体截止时间是多少,应在入口统一设计。各层都重试三次,最终可能把一次调用放大成几十次。

高负载时优先级低的请求先失败:

  1. 保障登录、支付、核心读取。
  2. 降低非核心刷新和统计频率。
  3. 暂停推荐、导出、批量重算。
  4. 对非关键操作返回稍后重试。

降级要可观察、可恢复。不能悄悄返回空数据让用户以为真的没有内容。

  • 同步调用适合需要立即结果的请求,但会把上游延迟和故障传下去。
  • 异步消息适合解耦、削峰和长任务,但增加最终一致与重复处理。
  • API 网关负责统一入口、认证、路由、限流和协议转换,不应承载所有业务。
  • 服务发现让调用方找到实例,但注册中心故障也要有缓存和降级。
  • 配置中心统一管理动态配置,必须有校验、版本、审计和回滚。

同步链路越长,尾部延迟越明显。一个请求调用 20 个服务,即使每个服务 p99 都很好,整体仍可能出现长尾。能并行的调用并行,能异步的任务异步,必要的调用设置独立超时。

容量规划至少估算:

  • 峰值 QPS 与并发请求数。
  • 单请求 CPU、内存、数据库和外部依赖消耗。
  • 数据增长与保留期。
  • 故障时剩余实例能否承接全部流量。
  • 扩容需要多久,冷启动和预热成本多大。

压测应使用接近真实的请求比例、数据规模和依赖延迟。只压一个简单健康接口,无法证明业务链路可用。

混沌演练可以主动注入延迟、实例故障、依赖错误和网络分区,但应从受控环境、小范围和小流量开始,有停止条件和监控。

架构选择应记录:

背景与约束:
考虑的方案:
决定:
理由:
代价与风险:
验证指标:
回滚或退出条件:

没有记录的决策会被记忆美化。未来看到“为什么用消息队列”时,至少能知道当时的目标和数据。

  • 当前复杂度是否由真实约束驱动,而不是跟风。
  • 服务边界是否对应业务能力和数据所有权。
  • 每个远程调用是否有超时、重试、幂等和失败语义。
  • 一致性要求是否按数据逐项定义。
  • 复制延迟、分区和共识失去多数派时行为是否清楚。
  • 跨服务流程是否有补偿、中间状态和可查询操作 ID。
  • 可用性目标是否有指标、测量范围和实际用户视角。
  • RPO/RTO、备份恢复和故障转移是否演练。
  • 是否有熔断、限流、降级和按优先级卸载。
  • 架构决策是否留下背景、代价和退出条件。