Raft 协议在 Nacos 中的具体实现(Leader 选举、日志复制) 源码详解

在 Nacos 中,Raft 协议的实现是其 CP 模式(强一致性) 的核心机制,主要用于 配置管理模块(Config Module) 的数据同步与一致性保障。Nacos 并未直接使用 etcd 的 Raft 库,而是基于 Raft 论文思想,自研了一套轻量级的 Raft 实现,通过 HTTP 协议进行节点通信。

本文将从 源码角度 深入解析 Nacos 中 Raft 协议的 Leader 选举日志复制 两大核心流程,结合关键类、方法调用链和代码片段,带你彻底理解其实现原理。


一、前置知识:Nacos Raft 的整体架构

  • 模块位置com.alibaba.nacos.core.distributed.raft
  • 通信方式:基于 HTTP 的自定义 RPC(非 gRPC)
  • 数据存储
    • 日志:{nacos.home}/data/protocol/raft/{group}/log.data
    • 快照:{nacos.home}/data/protocol/raft/{group}/snapshot/
  • 核心角色类
    • RaftPeer:表示一个 Raft 节点(Follower/Leader/Candidate)
    • RaftCore:Raft 的核心调度引擎
    • LogProcessor:日志提交后的状态机处理器(如更新配置)
    • RaftStore:日志和快照的持久化存储

二、Leader 选举源码详解

1. 触发选举的入口:GlobalTaskScheduler

Nacos 使用定时任务调度器启动选举超时机制。

// com.alibaba.nacos.core.distributed.raft.RaftCore#start
public void start() {
    GlobalExecutor.submitLeaderTask(new MasterElectionCore());
    GlobalExecutor.submitHeartBeat(new HeartBeatCore());
}

其中 MasterElectionCore 是选举核心任务。


2. 选举超时逻辑:MasterElectionCore

class MasterElectionCore implements Runnable {
    @Override
    public void run() {
        try {
            // 如果当前是 Follower 且长时间未收到心跳,触发选举
            if (peers.isLeader(null) || peers.getLeader() == null) {
                Loggers.RAFT.info("No leader is available, start leader election.");
                requestVote();
            }
        } catch (Exception e) {
            Loggers.RAFT.error("Exception while master election", e);
        }
    }
}
  • peers.isLeader(null):判断当前是否为 Leader。
  • 若无 Leader 或自己不是 Leader,且超时未收到心跳,则调用 requestVote() 发起投票。

⏱️ 选举超时时间默认为 500ms ~ 1000ms 随机值,防止脑裂。


3. 发起投票:RaftCore#requestVote

public synchronized void requestVote() {
    // 提升 term
    long term = peers.term.incrementAndGet();
    
    // 变为 Candidate
    RaftPeer local = peers.get(NetUtils.localServer());
    local.voteFor = local.ip;
    local.state = RaftPeer.State.CANDIDATE;
    local.term.set(term);

    Loggers.RAFT.info("leader timeout, start voting, me: {}", local.ip);

    // 向其他节点广播投票请求
    final long start = System.currentTimeMillis();
    List<CompletableFuture<Void>> votes = new ArrayList<>();
    for (final String server : peers.allServers()) {
        if (NetUtils.localServer().equals(server)) {
            continue;
        }
        final String url = "http://" + server + ":8848/nacos/v1/raft/vote";
        // 构造投票请求参数
        Map<String, String> params = new HashMap<>();
        params.put("term", String.valueOf(term));
        params.put("candidate", local.ip);
        params.put("voteGranter", local.ip);

        // 异步发送 HTTP 请求
        votes.add(HttpAsyncClient.INSTANCE.get(url, params).thenRun(() -> {
            // 收到投票响应
        }));
    }

    // 等待多数节点响应
    CompletableFuture.allOf(votes.toArray(new CompletableFuture[0]))
        .thenRun(() -> {
            // 统计得票数
            int voteCount = 1; // 自己投自己
            for (RaftPeer peer : peers.all()) {
                if (peer.term.get() == term && peer.voteFor.equals(local.ip)) {
                    voteCount++;
                }
            }
            // 如果获得多数票,成为 Leader
            if (voteCount > peers.all().size() / 2) {
                becomeLeader();
            }
        });
}

关键点解析:

  • term 自增:每个 Candidate 提出更高 term。
  • 广播 RequestVote:通过 HTTP 向所有其他节点发送 /nacos/v1/raft/vote
  • 异步等待多数响应:使用 CompletableFuture 并行处理。
  • 多数派判定voteCount > N/2 才能成为 Leader。

4. 接收投票请求:VoteRequestHandler

// com.alibaba.nacos.core.distributed.raft.processor.impl.CandidateHandler
public class CandidateHandler extends RequestHandler {

    @Override
    public HttpServerResponse handle(HttpRequest request, HttpServerResponse response) {
        String termStr = request.getParam("term");
        String candidateIP = request.getParam("candidate");
        long term = Long.parseLong(termStr);

        RaftPeer local = peers.get(NetUtils.localServer());
        
        // 如果收到的 term 更高,且日志不落后,则投票
        if (term > local.term.get()) {
            local.term.set(term);
            local.voteFor = candidateIP;
            local.state = RaftPeer.State.FOLLOWER;
            // 重置选举定时器
            resetLeaderTimeout();
            return HttpResponseBuilder.create().entity("OK").build();
        }
        
        return HttpResponseBuilder.create().entity("DENY").build();
    }
}
  • 投票条件
    1. term >= local.term
    2. 日志“至少一样新”(Nacos 中简化为 term 比较)
  • 投票后重置选举超时,防止重复选举。

5. 成为 Leader:becomeLeader()

private void becomeLeader() {
    RaftPeer local = peers.get(NetUtils.localServer());
    local.state = RaftPeer.State.LEADER;
    local.leader = local.ip;
    
    // 启动心跳任务
    GlobalExecutor.submitHeartBeat(new HeartBeatCore());
    
    Loggers.RAFT.info("Became the LEADER: {}", local.ip);
}
  • 设置状态为 LEADER
  • 启动 HeartBeatCore 定时发送心跳

三、日志复制源码详解

1. 写请求入口:RaftController#write

// com.alibaba.nacos.naming.controllers.RaftController
@PostMapping("/write")
public ResponseEntity<String> write(@RequestBody String value) {
    try {
        // 提交日志条目
        raftCore.submit(value);
        return ResponseEntity.ok("OK");
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

所有写请求最终都会调用 raftCore.submit()


2. 日志提交:RaftCore#submit

public synchronized boolean submit(String value) {
    // 只有 Leader 能处理写请求
    if (!isLeader()) {
        // 转发给 Leader
        return forwardToLeader(value);
    }

    // 封装为日志条目
    LogEntry logEntry = new LogEntry();
    logEntry.setTerm(peers.get(NetUtils.localServer()).term.get());
    logEntry.setData(value);
    logEntry.setType(LogEntry.Type.COMMAND);

    // 写入本地日志
    logStore.write(logEntry);

    // 复制日志到其他节点
    replicate(logEntry);

    return true;
}
  • 非 Leader 节点:调用 forwardToLeader() 转发请求。
  • Leader 节点:写本地日志 → 调用 replicate() 复制。

3. 日志复制:replicate()

private void replicate(LogEntry entry) {
    for (String server : peers.allServers()) {
        if (server.equals(NetUtils.localServer())) {
            continue;
        }
        // 获取该节点的 nextIndex
        int nextIndex = getNextIndex(server);
        LogEntry prevLog = logStore.read(nextIndex - 1);

        // 构造 AppendEntries 请求
        Map<String, Object> request = new HashMap<>();
        request.put("leaderId", NetUtils.localServer());
        request.put("term", peers.get(NetUtils.localServer()).term.get());
        request.put("prevLogIndex", nextIndex - 1);
        request.put("prevLogTerm", prevLog != null ? prevLog.getTerm() : 0);
        request.put("entries", Collections.singletonList(entry));
        request.put("leaderCommit", commitIndex);

        // 发送 HTTP 请求
        String url = "http://" + server + ":8848/nacos/v1/raft/heartbeat";
        HttpAsyncClient.INSTANCE.post(url, request, new WriteCallback() {
            @Override
            public void onSuccess(String result) {
                // 更新 nextIndex 和 matchIndex
                matchIndex.put(server, entry.getIndex());
                nextIndex.put(server, entry.getIndex() + 1);
                
                // 检查是否可以提交
                checkCommit();
            }

            @Override
            public void onFail(Throwable e) {
                // 重试或回退 nextIndex
                retryReplicate(server);
            }
        });
    }
}
  • prevLogIndex / prevLogTerm:用于一致性检查。
  • 异步回调:成功后更新 matchIndex,失败则重试。

4. 接收 AppendEntries:FollowerHandler

// com.alibaba.nacos.core.distributed.raft.processor.impl.FollowerHandler
public class FollowerHandler extends RequestHandler {

    @Override
    public HttpServerResponse handle(HttpRequest request, HttpServerResponse response) {
        Map<String, Object> body = parseBody(request);
        long prevLogIndex = (Long) body.get("prevLogIndex");
        long prevLogTerm = (Long) body.get("prevLogTerm");
        List<LogEntry> entries = (List<LogEntry>) body.get("entries");

        RaftPeer local = peers.get(NetUtils.localServer());

        // 一致性检查
        LogEntry prevLog = logStore.read(prevLogIndex);
        if (prevLog == null || prevLog.getTerm() != prevLogTerm) {
            return HttpResponseBuilder.create().status(400).entity("Log not matched").build();
        }

        // 追加新日志(覆盖冲突日志)
        for (LogEntry entry : entries) {
            if (logStore.read(entry.getIndex()) == null) {
                logStore.write(entry);
            } else {
                // 日志冲突,覆盖
                logStore.deleteFrom(entry.getIndex());
                logStore.write(entry);
            }
        }

        // 更新 commitIndex
        long leaderCommit = (Long) body.get("leaderCommit");
        if (leaderCommit > commitIndex) {
            commitIndex = Math.min(leaderCommit, logStore.getLastIndex());
            // 提交日志到状态机
            applyLogToStateMachine();
        }

        return HttpResponseBuilder.create().entity("OK").build();
    }
}
  • 日志冲突处理:若 prevLog 不匹配,拒绝复制。
  • 覆盖机制:Leader 强制覆盖 Follower 的冲突日志。
  • 更新 commitIndex:并尝试应用日志。

5. 提交日志:checkCommit()applyLogToStateMachine()

private void checkCommit() {
    List<Long> matchIndexList = new ArrayList<>(matchIndex.values());
    Collections.sort(matchIndexList);
    int N = matchIndexList.size();
    long newCommitIndex = matchIndexList.get(N - (N / 2 + 1)); // 中位数

    if (newCommitIndex > commitIndex) {
        commitIndex = newCommitIndex;
        applyLogToStateMachine();
    }
}

private void applyLogToStateMachine() {
    while (lastApplied < commitIndex) {
        lastApplied++;
        LogEntry entry = logStore.read(lastApplied);
        // 通知状态机处理器(如配置管理)
        for (LogProcessor processor : logProcessors) {
            processor.onApply(entry);
        }
    }
}
  • 多数派确认:取 matchIndex 的中位数作为可提交索引。
  • 应用到状态机:触发 LogProcessor(如更新 Config 数据)

四、关键类图与调用链总结

1. Leader 选举调用链

GlobalExecutor → MasterElectionCore.run() 
    → RaftCore.requestVote() 
        → HTTP POST /vote 
            → CandidateHandler.handle()
                → 投票逻辑
                    → becomeLeader() → 启动心跳

2. 日志复制调用链

RaftController.write() 
    → RaftCore.submit() 
        → replicate() 
            → HTTP POST /heartbeat 
                → FollowerHandler.handle()
                    → 日志追加 + commit 更新
                        → applyLogToStateMachine() → 更新配置

五、源码级总结

机制源码实现要点
Leader 选举基于定时任务 + term 递增 + HTTP 投票 + 多数派判定
日志复制Leader 写本地 → 广播 AppendEntries → Follower 检查 prevLog → 追加日志 → Leader 提交
一致性保证多数派复制 + 日志匹配 + 强 Leader 覆盖
状态机应用LogProcessor.onApply() 回调更新配置数据
持久化LogStore 写磁盘,支持快照

六、建议阅读源码路径

  1. RaftCore.java:核心调度
  2. RaftPeer.java:节点状态
  3. LogStore.java:日志文件读写
  4. RaftController.java:HTTP 接口
  5. CandidateHandler.java / FollowerHandler.java:RPC 处理器
  6. LogProcessor.java:状态机接口(如 ConfigLogProcessor
### NacosRaft 协议的配置与实现 Nacos 是一个用于动态服务发现、配置管理和服务管理的开源项目。为了支持高可用性和强一致性,Nacos 在其内部实现中引入了 Raft 协议来处理集群中的 Leader 选举和 CP 数据的一致性同步[^2]。 #### 1. **Raft 协议的作用** Raft 协议是一种分布式共识算法,主要解决在分布式系统中如何达成一致的问题。它通过领导者选举日志复制和安全性机制,确保了分布式环境下的数据一致性。在 Nacos 中,Raft 主要被应用于以下几个方面: - 集群内的 Leader 选举。 - 确保服务注册表和服务实例信息的数据一致性。 这些功能由 Nacos 借助 JRaft实现,JRaft 提供了一个高性能的 Java 版本 Raft 实现。 --- #### 2. **Nacos 使用 Raft 的场景** Nacos 将一致性协议的能力下沉到内核模块中,使其能够很好地服务于服务注册发现模块和配置管理模块[^3]。具体来说: - **服务注册发现**:当客户端向 Nacos 注册服务时,Nacos 需要在多个节点之间保持服务实例的状态一致性。此时,Raft 被用来保证服务注册表的信息能够在所有副本间同步并达到一致性。 - **配置管理**:对于全局共享的配置文件,Nacos 同样依赖于 Raft 来保障不同节点上的配置内容是一致的。 --- #### 3. **Raft 协议具体实现方式** 以下是 NacosRaft 协议的主要实现细节: ##### a) **Leader 选举** 在启动阶段或者发生网络分区的情况下,Nacos 集群会触发 Leader 选举过程。每个候选者都会尝试获取多数派的支持成为新的 Leader。一旦某个节点成功当选为 Leader,则其他节点进入 Follower 或 Candidate 状态[^2]。 ##### b) **日志复制** Leader 接收到写请求后,先将其记录到本地的日志中,随后通知所有的 Followers 追加相同的条目。只有当大多数节点确认接收该命令之后,这条指令才会被认为已提交,并应用到状态机上。 ##### c) **实例信息持久化** `RaftConsistencyServiceImpl.put()` 方法负责完成实例信息的持久化操作。此方法对应的是 `consistencyService.put(key, instances)` 的调用路径[^5]。这意味着每次更新都需要经过完整的 Raft 流程才能生效。 --- #### 4. **NacosRaft 的配置方法** 如果希望启用基于 Raft 的一致性模型,可以通过修改 Nacos 的配置文件来进行设置。以下是一个典型的配置示例: ```yaml nacos: core: consistency-model: raft # 设置一致性模型为 Raft cluster: nodes: node1:8848,node2:8848,node3:8848 # 定义集群成员列表 ``` 在此基础上,还需要确保 JVM 参数正确加载以及相关依赖库(如 JRaft)已被引入至项目的 classpath 下。 另外需要注意的是,默认情况下 Nacos 可能采用更简单的 AP 类型协议(例如 Distro),因此显式指定上述参数非常重要。 --- #### 5. **总结** 综上所述,NacosRaft 协议的应用不仅限于理论层面,而是深入到了实际业务逻辑当中。无论是服务注册还是配置分发,都离不开这一底层支撑技术的帮助。借助成熟的第三方工具(比如 JRaft),开发者可以更加专注于构建高层次的功能特性而无需过多关心复杂的分布式协调问题。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值