跨地域延迟优化:Paxos Make-Lease 机制在元数据集群中的工程实践

跨地域延迟优化:Paxos Make-Lease 机制在元数据集群中的工程实践

一、跨地域 RTT 100ms 下的 Paxos 写入,一次共识需要 300ms

在一个跨越北京、上海、广州三地的元数据集群中。光纤延迟实测为单程 50ms。标准 Multi-Paxos 的一次写入流程涉及:Proposer → Acceptors(Prepare 阶段)= 1 个 RTT(100ms)。Proposer → Acceptors(Accept 阶段)= 1 个 RTT(100ms)。加上广播 commit 消息 = 半个 RTT(50ms)。总计至少 250ms。这还不算磁盘 fsync 的时间。

对于元数据操作来说,这个延迟勉强可用。但当出现 Leader 故障或网络分区时。新的 Leader 需要在完整的 Prepare 阶段中收集多数派的承诺。这个过程的延迟在上限上翻倍。

问题的本质是 Paxos 协议设计时假定节点之间延迟的差异不大。但在跨地域部署中,延迟是差异化的(同城 1ms,跨城 50ms)。需要一种机制在保证一致性的前提下。尽可能减少跨地域的消息往返次数。Make-Lease 正是基于这个目标设计的优化。

二、Make-Lease 机制的租约续约与快速路径

Make-Lease 的核心思想是:在当前 Leader 持有有效租约时。跳过 Paxos 的 Prepare 阶段,直接进入 Accept 阶段。将跨地域的延迟从 2 RTT 降到 1 RTT。

sequenceDiagram participant L as Leader(北京) participant A1 as Acceptor(北京) participant A2 as Acceptor(上海) participant A3 as Acceptor(广州) Note over L,A3: Make-Lease 流程(Leader 持有有效租约) L->>A1: Accept 请求 (ballot=N, 数据) L->>A2: Accept 请求 (ballot=N, 数据) L->>A3: Accept 请求 (ballot=N, 数据) A1-->>L: Accepted (承诺不接收 ballot<N 的 Prepare) A2-->>L: Accepted A3-->>L: Accepted L->>A1: Commit (异步广播) L->>A2: Commit L->>A3: Commit Note over L,A3: 多数派确认后即对外返回,总延迟约 100ms Note over L,A3: 租约到期,Leader 续约 L->>A1: 续约请求 (延长租约) L->>A2: 续约请求 L->>A3: 续约请求 A1-->>L: 续约确认 A2-->>L: 续约确认 A3-->>L: 续约确认

租约的本质是 Acceptor 在承诺的时间内不响应更高 ballot 的 Prepare。在租约有效期内,Leader 可以安全地跳过 Prepare 阶段。因为知道没有其他 Proposer 能够在多数派中收集到承诺。

租约的时长选择是关键设计决策。太短会导致频繁续约,抵消快速路径的收益。太长则故障转移时间增加。合理的租约时长是网络最大 RTT 的 3 倍。在 RTT=100ms 的场景中,租约设为 300ms。Leader 在租约过半(150ms)时提前续约。

Make-Lease 需要处理的一个微妙情形是租约过期同时出现网络分区。如果 Leader 认为租约仍然有效(它向自身发出的续约请求超时了)。但实际上多数 Acceptor 已经释放租约。此时 Leader 的 Accept 请求将收到拒绝(ballot 过期)。Leader 必须回退到完整 Paxos 流程。

三、Make-Lease 的核心实现

use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::RwLock; /// Acceptor 侧的状态 #[derive(Clone)] struct AcceptorState { /// 当前承诺的最大 ballot promised_ballot: u64, /// 已接受的 ballot 和对应的值 accepted_ballot: u64, accepted_value: Option<Vec<u8>>, /// Make-Lease 租约:在此时间之前拒绝更高 ballot 的 Prepare lease_expires_at: Option<Instant>, } impl AcceptorState { /// 处理 Prepare 请求 /// /// 如果当前在租约期内且请求的 ballot 更高, /// 必须拒绝 Prepare——这是 Make-Lease 安全性的核心保证 fn handle_prepare(&mut self, ballot: u64) -> Result<PrepareResponse, String> { // 租约检查:如果在租约期,拒绝更高 ballot 的 Prepare // 为什么需要这个检查? // 如果没有此检查,其他 Proposer 可在租约期获得更高 ballot, // 导致 Leader 的 Accept 请求被拒绝而不知情——破坏安全性 if let Some(expires_at) = self.lease_expires_at { if Instant::now() < expires_at { return Err(format!( "在租约期内,拒绝 ballot={} 的 Prepare", ballot )); } } if ballot > self.promised_ballot { self.promised_ballot = ballot; Ok(PrepareResponse { promised: true, accepted_ballot: self.accepted_ballot, accepted_value: self.accepted_value.clone(), }) } else { Err(format!( "ballot {} 不大于已承诺的 {}", ballot, self.promised_ballot )) } } /// 处理 Accept 请求 fn handle_accept(&mut self, ballot: u64, value: Vec<u8>) -> Result<(), String> { if ballot < self.promised_ballot { return Err(format!("ballot {} 小于承诺的 {}", ballot, self.promised_ballot)); } self.accepted_ballot = ballot; self.accepted_value = Some(value); Ok(()) } /// 授予租约 fn grant_lease(&mut self, duration: Duration) { self.lease_expires_at = Some(Instant::now() + duration); } /// 释放租约 fn revoke_lease(&mut self) { self.lease_expires_at = None; } } struct PrepareResponse { promised: bool, accepted_ballot: u64, accepted_value: Option<Vec<u8>>, } /// Proposer/Leader 侧的状态 /// /// 管理 Make-Lease 的快速路径和租约续约 struct LeaderState { current_ballot: u64, /// 租约到期时间(Leader 自己的记录) lease_expires_at: Instant, /// 租约总时长 lease_duration: Duration, /// 上次续约时间,用于判断是否需要续约 last_renewal: Instant, } impl LeaderState { fn new(ballot: u64, lease_duration: Duration) -> Self { Self { current_ballot: ballot, lease_expires_at: Instant::now() + lease_duration, lease_duration, last_renewal: Instant::now(), } } /// 判断当前是否可以使用 Make-Lease 快速路径 /// /// 条件:租约未过期,且在过期前有足够的续约窗口 /// 后半程续约策略:租约过半时自动续约 fn can_use_fast_path(&self) -> bool { Instant::now() < self.lease_expires_at } /// 判断是否需要续约 /// /// 在租约过半时触发续约,留出足够时间给续约 RPC fn should_renew(&self) -> bool { let elapsed = self.last_renewal.elapsed(); elapsed > self.lease_duration / 2 } /// 处理一次成功的 Accept 响应(续约相关) /// /// 每次收到多数派 Accept 时都重置租约计时器 /// 这是一个保守的策略:只要共识在正常进行,租约就是有效的 fn on_successful_accept(&mut self) { self.lease_expires_at = Instant::now() + self.lease_duration; } /// 处理租约续约成功的响应 fn on_renewal_success(&mut self) { self.lease_expires_at = Instant::now() + self.lease_duration; self.last_renewal = Instant::now(); } }

实现中安全性最关键的检查在handle_prepare方法中。如果 Acceptor 当前在租约期内。即使收到更高 ballot 的 Prepare 也必须拒绝。这确保了在 Leader 持有租约时。没有其他节点可以通过 Prepare 阶段获取更高 ballot。从而保护了 Leader 的快速路径写入不会因 ballot 变化而失败。

四、Make-Lease 的失效场景与运维权衡

Make-Lease 的优化效果依赖于稳定的网络和可靠的时钟。

首先是时钟偏移(Clock Skew)的风险。租约的判断基于各节点的本地时钟。如果跨地域节点之间存在显著的时钟偏移。可能出现 Leader 认为租约有效但 Acceptor 早已过期的情形。建议在所有节点上部署 NTP 同步。时钟偏移控制在 10ms 以内。

其次是网络分区的误判。当 Leader 与多数 Acceptor 之间网络中断时。Leader 可能仍认为租约有效。但续约请求已经无法送达。Acceptor 侧租约到期后。其他节点可以成功发起完整 Paxos。这会导致两个 Leader 同时存在(Split Brain)的风险。需要结合 Fencing Token 机制来防止。

第三是租约时长的逐地域差异化。同城的 Acceptor 可以使用更短的租约(如 50ms)。因为 Leader 可以高频续约。跨地域的 Acceptor 需要更长的租约(300ms~1s)。但这会在故障转移时引入对应的延迟。

最后是单点续约的脆弱性。Leader 负责所有续约操作。当 Leader 本身负载过高时。续约 RPC 可能被延迟处理。导致租约意外过期、回退到慢路径。建议续约操作使用独立的异步任务。与业务写入不在同一调度队列。

五、总结

  1. Make-Lease 通过租约机制将 Paxos 的跨地域写入延迟从 2 RTT 降低到 1 RTT。在 RTT=100ms 场景中节省约 100ms。
  2. 租约的安全性依赖于 Acceptor 在租约期内拒绝更高 ballot 的 Prepare。这是快速路径不会产生一致性冲突的核心保证。
  3. 租约时长建议为网络最大 RTT 的 3 倍。续约在租约过半时触发(后半程续约策略)。
  4. 时钟偏移和网络分区是 Make-Lease 的主要风险来源。需要 NTP 同步和 Fencing Token 双重保障。
  5. 续约操作应与业务写入解耦。使用独立的异步任务调度,避免 Leader 负载高时租约意外过期。