OceanBase源码解读:Palf选主入口 election_impl.cpp

在 OceanBase 的日志流(Palf)体系中,每个日志流(LS)都由一套 Paxos 风格的选举协议选出唯一 Leader,本文解读这套协议的总控实现 election_impl.cpp。它位于 logservice/palf/election/algorithm/ 目录下,与同目录的 election_proposer.cpp(提议者)、election_acceptor.cpp(接受者)共同构成选主协议的算法层。ElectionImpl 自身不实现投票细节,而是扮演”门面 + 调度器”角色:对外提供初始化、成员变更、切主等接口,对内持有 proposer 与 acceptor 两个核心角色,并把五种选举消息统一分发下去。

整体架构:门面 + 双角色

ElectionImpl 继承自对外暴露的 Election 抽象类,每个 LS 独享一个实例。核心成员关系如下:

//【节选】election_impl.h 关键成员
ElectionProposer proposer_;   // Paxos 提议者:发起投票、争取成为 leader
ElectionAcceptor acceptor_;   // Paxos 接受者:响应投票、记录承诺
ElectionMsgSender *msg_handler_; // 外部注入的消息收发模块
ObOccamTimer *timer_;         // 外部共享定时器
ElectionPriority *priority_;  // 外部注入的选举优先级策略
uint64_t inner_priority_seed_;// 内部优先级种子(位掩码)

几个设计点值得注意:消息的实际网络收发通过外部注入的 ElectionMsgSender 完成,定时器复用外部传入的 ObOccamTimer,本类不自己起线程——大量 LS 共享一套定时器与网络资源,避免线程爆炸。所有消息处理与配置变更由单一大锁 lock_ 串行化,简化了并发正确性;选主消息量很小(每次选举几十条),锁竞争代价可接受。

初始化与停机:init_and_start / stop

初始化接口在一步内完成 init 与 start,每个参数都承担明确职责:

//【节选】init_and_start 关键流程
if (OB_UNLIKELY(is_inited_)) {
  ret = OB_INIT_TWICE;                    // 防重复初始化
} else if (CLICK_FAIL(proposer_.init(restart_counter))) {
  // restart_counter:重启计数,参与 ballot 比较,
  // 防止重启后用旧状态干扰投票
} else if (CLICK_FAIL(prepare_change_leader_cb_.assign(...))) {
  // 切主前的准备确认回调
} else if (CLICK_FAIL(role_change_cb_.assign(...))) {
  // 角色变化通知回调
} else {
  timer_ = election_timer;
  id_ = id;
  if (CLICK_FAIL(proposer_.start())) {
  } else if (CLICK_FAIL(acceptor_.start())) {
    // init 里就把双方的定时任务跑起来,"init_and_start"是一体的
  } else if (inner_priority_seed_ & SEED_NOT_NORMOL_REPLICA_BIT) {
    event_recorder_.set_need_report(false); // 非普通副本不参与事件上报
  }
}

stop 的实现则藏着一个经典的并发陷阱:先在锁内清空优先级、置运行标志为 false,然后必须在锁外停 proposer/acceptor 的定时任务。原因写在源码注释里:定时任务执行中也要抢这把选举锁,如果在持锁状态下停定时任务并等待其结束,就会互相等锁而死锁。”先退锁、再停任务”的顺序在读任何停机代码时都值得记住。

消息处理总入口:handle_message

五种选举消息——Prepare 请求/响应、Accept 请求/响应、ChangeLeader——全部经由 handle_message 重载进入,统一骨架是:记录处理时间戳(统计网络延迟)→ 加大锁 → 更新跨版本水位与消息计数 → 分发给 proposer 或 acceptor。其中两个请求类消息的处理最见功力:

//【节选】Prepare 请求处理
{
  LockGuard lock_guard(lock_);
  handle_message_base_(msg);        // 更新集群版本水位
  msg_counter_.add_received_count(msg);
  if (msg.get_sender() != self_addr_) {
    proposer_.on_prepare_request(msg, &need_register_devote_task);
  }
  acceptor_.on_prepare_request(msg); // 承诺不再接受更小 ballot
}
if (need_register_devote_task) {     // "我让贤了",锁外重新发起选举
  proposer_.reschedule_or_register_prepare_task_after_(
      CALCULATE_MAX_ELECT_COST_TIME());
}

Prepare 阶段体现了选举的”礼让”逻辑:若对方 ballot 更大,本地让步更新状态;若对方优先级更高且自己还不是 leader,则置 need_register_devote_task,表示让贤,并在锁释放后尽快重新发起一轮投票(让贤任务注册放在锁外,避免递归抢锁)。同时 acceptor 侧完成 Paxos 第二承诺:不再接受更小 ballot 的提案。

//【节选】Accept 请求处理——"贴线抢跑"策略
if (msg.get_ballot_number() > proposer_.ballot_number_) {
  proposer_.advance_ballot_number_and_reset_related_states_(
      msg.get_ballot_number(), "receive bigger accept request");
}
acceptor_.on_accept_request(msg, &us_to_expired);
// us_to_expired:当前 leader 租约剩余时间
if (OB_LIKELY(us_to_expired > 0)) {
  proposer_.reschedule_or_register_prepare_task_after_(
      us_to_expired - CALCULATE_TRIGGER_ELECT_WATER_MARK());
}

Accept 阶段有两个独有逻辑。其一,收到更大 ballot 时主动推进自己的 ballot 号并重置相关状态,保证后续自己发起选举不会复用过期的小编号。其二也是更精彩的:acceptor 返回当前 leader 的租约剩余时间 us_to_expired,候选人用”租约到期时间减去一个提前量水线”来重排自己的下一次 Prepare 定时任务——让自己恰好在现任 leader 租约将到期时开抢。这样既减少了与现任 leader 的无效投票冲突,又把 leader 故障后的选主空窗期压到最短,是 OceanBase 选举低抖动的关键。

成员变更与优先级体系

成员变更走 can_set_memberlist(预检查当前投票状态是否允许变更)+ set_memberlist(写入新成员列表)两步。新列表携带 LogConfigVersion 配置版本号,只有版本更新的列表才能生效,防止乱序的旧配置消息覆盖新配置——这是 Paxos 成员变更安全性的关键。手动切主 change_leader_to 则复用同一套投票流程:目标副本优先级被临时抬高后走正常的 Prepare/Accept 当选,因此仍需多数派确认,不破坏一致性。

优先级分两层:inner_priority_seed_ 是内部位掩码(非普通副本位、临时降权位等),priority_ 是外部注入的策略对象。临时降权的实现很轻——temporarily_downgrade_protocol_priority 只记录一个降权截止时间戳与原因,真正生效在 generate_inner_priority_seed_:生成优先级种子时检查是否仍在降权期内,若是则叠加降权位。该种子随 Prepare 消息携带,其他副本据此比较”谁更该当选”。

小结

election_impl.cpp 是理解 OceanBase 选主的最佳切入点:它把”门面接口、双角色分发、单锁串行化、优先级体系”四件事讲得清清楚楚。两个最值得带走的设计:一是 Accept 消息里的”贴线抢跑”——用 leader 租约剩余时间反推下一次发起投票的时机,兼顾稳定与快速故障接管;二是 stop 时”锁外停定时任务”的死锁规避。要继续深挖,可以顺着 proposer_.on_prepare_request 进入 election_proposer.cpp 看完整的两阶段投票状态机。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注