跳转到内容

协调服务与实践模式

共识(→ 共识:Paxos 与 Raft)之上,工程界把常见协调模式做成了服务与套路:协调服务(ZooKeeper/etcd)、租约、幂等与重试、Saga。本章是分布式系统的工程模式集。

基于共识的协调服务提供三类原语:

  • ZooKeeper(ZAB 共识)、etcd(Raft 共识):小数据量(元数据级)、强一致、高可用的键值存储;
  • 提供:配置存储(强一致的配置分发)、领导者选举(临时节点 + 会话)、分布式锁(顺序节点)、服务发现(注册与心跳)。

用对场景:协调服务是元数据工具,不是数据存储——数据量大或写入频繁(监控指标、消息)会压垮它(共识的写代价,每写一轮多数派)。锁与选举、配置、命名是它的正确用武之地。

租约(lease)解决"持锁者死了"的问题:锁/授权带有效期,过期自动失效——持锁者崩溃后,系统最多等租约过期(不用等它"承认死亡")。

  • 租约的正确性依赖时钟:过期判定依赖时间(或心跳共识)——时钟漂移会让"以为过期"实际没过期;
  • ** fencing token**:分布式锁需要 fencing token 才安全——每次授予锁时递增的编号,下游校验"只有最新 token 的写有效"——防止过期的持锁者复活后继续写(GC 停顿让它"死而复生")。没有 fencing token 的分布式锁不是锁,是"大概互斥"。
持锁者 A 拿到锁,token=33 → 长时间 GC 停顿
租约过期 → B 拿到锁,token=34
A 醒来继续写(带着 token=33)→ 存储层拒绝(33 < 34)✔

分布式通信必然失败:超时后你不知道"对方做没做"——重试是标准应对,而重试必须幂等:

  • 幂等(idempotency):同一操作执行多次与一次效果相同——转账"加 100"不幂等,"设置余额为 100"幂等;
  • 实现幂等:幂等键(idempotency key)——客户端生成唯一键,服务端去重(支付系统的标准做法);条件写(CAS、带版本号);
  • 重试必须配退避与抖动:**指数退避(exponential backoff)**避免重试风暴,**抖动(jitter)**避免同步重试的波峰——没有退避的重试是自我 DoS。

精确一次(exactly-once)投递不存在于纯网络层:两将军问题——你无法确认"恰好一次"到达。实际的等价物:

精确一次=至少一次(at-least-once)+幂等\text{精确一次} = \text{至少一次(at-least-once)} + \text{幂等}
  • 至少一次:重试直到确认——消息可能重复,但不会丢;
  • 至多一次:不重试——不重复,但可能丢;
  • 精确一次 = 至少一次 + 消费端幂等去重(Kafka 的事务与幂等生产者也是这个思路)。

"精确一次 = 至少一次 + 幂等"——工程上的"恰好一次"都是这个组合,不要在网络上找奇迹。

微服务间没有 2PC(跨服务的分布式事务代价太高,→ 复制与分片),Saga 模式替代:长事务拆成一系列本地事务,每个配一个补偿操作——失败时逆序执行补偿:

订机票 ✓ → 订酒店 ✓ → 订租车 ✗
补偿:退酒店 ← 退机票
  • 代价:中间状态可见(订了机票没订成车)——业务要接受最终一致;
  • 补偿的语义:补偿不是回滚(已发生的事无法假装没发生),是语义上的撤销(退票 ≠ 没订过);
  • 事件驱动 + Saga + 幂等消费是微服务事务的标准组合(→ 架构与权衡)。
  • 协调服务(ZooKeeper/etcd)是元数据工具:配置、选举、锁——不是数据存储
  • 分布式锁需要 fencing token 才安全:过期持锁者复活后的写要被拒绝
  • 重试必须配退避与抖动;幂等键让重试安全
  • 精确一次 = 至少一次 + 幂等:网络层不存在精确一次
  • Saga = 本地事务序列 + 逆序补偿:中间状态可见,补偿是语义撤销