Skip to content

NodeImpl::handle_request_vote_request 中存在竞态条件,可能引发选举延迟或失败 #526

@walterzhaoJR

Description

@walterzhaoJR

Bug 描述

Raft 选举投票处理函数 NodeImpl::handle_request_vote_request 中存在竞态条件,导致本应被授予的合法投票被错误拒绝,可能引发选举延迟或失败。

复现条件

当以下事件序列发生时触发:

节点 A 发起选举,elect_self 中将 term 提升至 T+1,并向其他节点发送 RequestVoteRequest(term=T+1)
节点 B 收到该投票请求,进入 handle_request_vote_request,此时 _current_term = T,saved_current_term = T
节点 B 释放 _mutex(为获取 last_log_id,涉及阻塞 I/O)
在 unlock 窗口期间,节点 A 当选为 leader 后发送心跳(AppendEntries),心跳携带 term=T+1
节点 B 收到心跳,调用 check_term -> step_down,将 _current_term 提升至 T+1
节点 B 重新获取 _mutex,执行 ABA 检查:saved_current_term(T) != _current_term(T+1)
原逻辑直接 break,拒绝了这次投票

根因分析

关键代码路径
投票处理(node.cpp handle_request_vote_request):

Image

心跳中 term 的来源(replicator.cpp _send_heartbeat):

Replicator 在发送心跳时通过 bthread_id_lock 获取独立锁,从 _options.term 读取 term。当 leader 选举成功后 _options.term 已提升至 T+1,心跳因此携带更高的 term。

心跳处理中的 term 推进(node.cpp handle_append_entries_request):

Image

时序图

Image

本质问题

原 ABA 检查过于保守:只要 _current_term 发生变化就无条件拒绝投票。但 _current_term 是单调递增的,真正的 ABA(A->B->A 回退)不可能发生。实际上这是一个 TOCTOU(Time of Check, Time of Use)问题。

当 term 从 T 变为 T+1,恰好等于 request->term()(T+1)时,投票请求仍然是合法的——候选人请求的 term 与当前 term 一致,应继续正常的投票判断流程。

影响

合法投票被拒绝,可能导致候选人无法获得多数票,触发选举超时重试
在网络抖动或心跳与投票并发频繁的场景下,可能反复出现,导致选举延迟
不会造成数据不一致(投票被拒是安全的),但影响可用性

修复方案

细化 ABA 检查逻辑,根据 _current_term 变化后的值与 request->term()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions