NACOS源码学习---raft

本文介绍Nacos中Raft共识算法的实现细节,包括心跳机制、数据同步流程及节点状态转换等内容。Nacos对标准Raft算法进行了优化,如任期时间重置和简化选举流程等。

一、Raft算法
Raft通过当选的领导者达成共识。筏集群中的服务器是领导者或追随者,并且在选举的精确情况下可以是候选者(领导者不可用)。领导者负责将日志复制到关注者。它通过发送心跳消息定期通知追随者它的存在。每个跟随者都有一个超时(通常在150到300毫秒之间),它期望领导者的心跳。接收心跳时重置超时。如果没有收到心跳,则关注者将其状态更改为候选人并开始领导选举。
注意:在各种分布式中间件里面,离不开ralft共识算法,nacos,kafka,rocketmq,flink,pulsa等等

1.每台Nacos机器注册上去都会给对应的服务器地址发送注册请求。send registRequest
2.这时候每台机器会把对应的请求转发到主节点,
3.主节点收到后会把该请求放到本地内存Map<String,Peers> localAllInfoMap
4.主节点会定时从localAllInfoMap注入到机器的发送心跳
5.如果没有收到心跳那么就重试几次,如果再没获取到那么就移除这台机器从Map里面
6.每台机器也会发送beatRequest请求到Nacos服务器中,后面网络恢复后,那么也会注册到Nacos里面
7.集群模式下,各个服务器互相发送请求,通过raflt算法获取到对应的Leader节点
8.leader节点会定时向从节点发送beatLeaderRequest,这样也是一个提升性能点,只用master给其他服务发送请求,
8.如果有一个服务器没有给回复,那么这时候就会触发选举
9.当然子节点也会定时一段时间去发送请求给Leader,但是频率没有Leader发送心跳频率高,这些都是为了提供性能、
10.raft算法细节
RaftCommands.beat()方法处理/v1/ns/raft/beat请求
接收心跳包的 http 接口:

@RestController
@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT +/raft”)
public class RaftController {
......

@NeedAuth
@RequestMapping(value = "/beat", method = RequestMethod.POST)
public JSONObject beat(HttpServletRequest request, HttpServletResponse response) throws Exception {
    String entity = new String(IoUtils.tryDecompress(request.getInputStream()), "UTF-8");
    String value = URLDecoder.decode(entity, "UTF-8");
    value = URLDecoder.decode(value, "UTF-8");

    // 解析心跳包
    JSONObject json = JSON.parseObject(value);
    JSONObject beat = JSON.parseObject(json.getString("beat"));

    // 处理心跳包并将本节点的信息作为 response 返回
    RaftPeer peer = raftCore.receivedBeat(beat);
    return JSON.parseObject(JSON.toJSONString(peer));
}

......

}

HeartBeat.receivedBeat()处理心跳包

1.如果收到心跳的节点不是Follower角色,则设置为Follower角色,并把它的voteFor设置为Leader节点的ip;
2.重置本地节点的heart timeout、election timeout;
3.调用PeerSet.makeLeader()通知这个节点更新Leader;(也就是说Leader节点会通过心跳通知其它节点更新Leader)
4.检查Datum:
遍历请求参数中的datums,如果Follwoer不存在这个datumKey或者时间戳比较旧,则收集这个datumKey;
每收集到50个datumKey,则向Leader节点的/v1/ns/raft/get路径发送请求,请求参数为这50个datumKey,获取对应的50个最新的Datum对象;

遍历这些Daum对象,接下来做的是和RaftCore.onPublish()方法中做的事类似:
1.调用RaftStore#write将Datum序列化为json写到cacheFile中
2.将Datum存放到RaftCore的datums集合中,key为上面的datum的key值
3.更新本地节点的election timeout
4.更新本地节点的任期term
5.本地节点的任期term持久化到properties文件中
6.调用notifier.addTask(datum, Notifier.ApplyAction.CHANGE);

通知对应的RaftListener

RaftCore.deleteDatum(String key)用来删除旧的Datum
datums集合中删除key对应的Datum;
RaftStore.delete(),在磁盘上删除这个Datum对应的文件;
notifier.addTask(deleted, Notifier.ApplyAction.DELETE),通知对应的RaftListener有DELETE事件。

本地节点的RaftPeer作为http响应返回。

@Component
public class RaftCore {
......

public RaftPeer receivedBeat(JSONObject beat) throws Exception {
    final RaftPeer local = peers.local();
    // 解析发送心跳包的节点信息
    final RaftPeer remote = new RaftPeer();
    remote.ip = beat.getJSONObject("peer").getString("ip");
    remote.state = RaftPeer.State.valueOf(beat.getJSONObject("peer").getString("state"));
    remote.term.set(beat.getJSONObject("peer").getLongValue("term"));
    remote.heartbeatDueMs = beat.getJSONObject("peer").getLongValue("heartbeatDueMs");
    remote.leaderDueMs = beat.getJSONObject("peer").getLongValue("leaderDueMs");
    remote.voteFor = beat.getJSONObject("peer").getString("voteFor");

    // 若收到的心跳包不是 leader 节点发送的,则抛异常
    if (remote.state != RaftPeer.State.LEADER) {
        Loggers.RAFT.info("[RAFT] invalid state from master, state: {}, remote peer: {}",
            remote.state, JSON.toJSONString(remote));
        throw new IllegalArgumentException("invalid state from master, state: " + remote.state);
    }

    // 本地 term 大于心跳包的 term,则心跳包不进行处理
    if (local.term.get() > remote.term.get()) {
        Loggers.RAFT.info("[RAFT] out of date beat, beat-from-term: {}, beat-to-term: {}, remote peer: {}, and leaderDueMs: {}"
            , remote.term.get(), local.term.get(), JSON.toJSONString(remote), local.leaderDueMs);
        throw new IllegalArgumentException("out of date beat, beat-from-term: " + remote.term.get()
            + ", beat-to-term: " + local.term.get());
    }

    // 若当前节点不是 follower 节点,则将其更新为 follower 节点
    if (local.state != RaftPeer.State.FOLLOWER) {
        Loggers.RAFT.info("[RAFT] make remote as leader, remote peer: {}", JSON.toJSONString(remote));
        // mk follower
        local.state = RaftPeer.State.FOLLOWER;
        local.voteFor = remote.ip;
    }

    final JSONArray beatDatums = beat.getJSONArray("datums");
    // 更新心跳包发送间隔和收不到心跳包的选举间隔
    local.resetLeaderDue();
    local.resetHeartbeatDue();

    // 更新 leader 信息,将 remote 设置为新 leader,更新原有 leader 的节点信息
    peers.makeLeader(remote);

    // 将当前节点的 key 存放到一个 map 中,value 都为 0
    Map<String, Integer> receivedKeysMap = new HashMap<String, Integer>(datums.size());
    for (Map.Entry<String, Datum> entry : datums.entrySet()) {
        receivedKeysMap.put(entry.getKey(), 0);
    }

    // 检查接收到的 datum 列表
    List<String> batch = new ArrayList<String>();
    if (!switchDomain.isSendBeatOnly()) {
        int processedCount = 0;
        Loggers.RAFT.info("[RAFT] received beat with {} keys, RaftCore.datums' size is {}, remote server: {}, term: {}, local term: {}",
            beatDatums.size(), datums.size(), remote.ip, remote.term, local.term);
        for (Object object : beatDatums) {
            processedCount = processedCount + 1;

            JSONObject entry = (JSONObject) object;
            String key = entry.getString("key");
            final String datumKey;
            // 构建 datumKey(加上前缀,发送的时候 key 是去掉了前缀的)
            if (KeyBuilder.matchServiceMetaKey(key)) {
                datumKey = KeyBuilder.detailServiceMetaKey(key);
            } else if (KeyBuilder.matchInstanceListKey(key)) {
                datumKey = KeyBuilder.detailInstanceListkey(key);
            } else {
                // ignore corrupted key:
                continue;
            }

            // 获取收到的 key 对应的版本
            long timestamp = entry.getLong("timestamp");

            // 将收到的 key 在本地 key 的 map 中标记为 1
            receivedKeysMap.put(datumKey, 1);

            try {
                // 收到的 key 在本地存在 并且 本地的版本大于收到的版本 并且 还有数据未处理,则直接 continue
                if (datums.containsKey(datumKey) && datums.get(datumKey).timestamp.get() >= timestamp && processedCount < beatDatums.size()) {
                    continue;
                }

                // 若收到的 key 在本地没有,或者本地的版本小于收到的版本,放入 batch,准备下一步获取数据
                if (!(datums.containsKey(datumKey) && datums.get(datumKey).timestamp.get() >= timestamp)) {
                    batch.add(datumKey);
                }

                // 只有 batch 的数量超过 50 或已经处理完了,才进行获取数据操作
                if (batch.size() < 50 && processedCount < beatDatums.size()) {
                    continue;
                }

                String keys = StringUtils.join(batch, ",");

                if (batch.size() <= 0) {
                    continue;
                }

                Loggers.RAFT.info("get datums from leader: {}, batch size is {}, processedCount is {}, datums' size is {}, RaftCore.datums' size is {}"
                    , getLeader().ip, batch.size(), processedCount, beatDatums.size(), datums.size());

                // 获取对应 key 的数据
                // update datum entry
                String url = buildURL(remote.ip, API_GET) + "?keys=" + URLEncoder.encode(keys, "UTF-8");
                HttpClient.asyncHttpGet(url, null, null, new AsyncCompletionHandler<Integer>() {
                    @Override
                    public Integer onCompleted(Response response) throws Exception {
                        if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
                            return 1;
                        }

                        List<Datum> datumList = JSON.parseObject(response.getResponseBody(), new TypeReference<List<Datum>>() {
                        });

                        // 更新本地数据
                        for (Datum datum : datumList) {
                            OPERATE_LOCK.lock();
                            try {
                                Datum oldDatum = getDatum(datum.key);

                                if (oldDatum != null && datum.timestamp.get() <= oldDatum.timestamp.get()) {
                                    Loggers.RAFT.info("[NACOS-RAFT] timestamp is smaller than that of mine, key: {}, remote: {}, local: {}",
                                        datum.key, datum.timestamp, oldDatum.timestamp);
                                    continue;
                                }

                                raftStore.write(datum);

                                if (KeyBuilder.matchServiceMetaKey(datum.key)) {
                                    Datum<Service> serviceDatum = new Datum<>();
                                    serviceDatum.key = datum.key;
                                    serviceDatum.timestamp.set(datum.timestamp.get());
                                    serviceDatum.value = JSON.parseObject(JSON.toJSONString(datum.value), Service.class);
                                    datum = serviceDatum;
                                }

                                if (KeyBuilder.matchInstanceListKey(datum.key)) {
                                    Datum<Instances> instancesDatum = new Datum<>();
                                    instancesDatum.key = datum.key;
                                    instancesDatum.timestamp.set(datum.timestamp.get());
                                    instancesDatum.value = JSON.parseObject(JSON.toJSONString(datum.value), Instances.class);
                                    datum = instancesDatum;
                                }

                                datums.put(datum.key, datum);
                                notifier.addTask(datum.key, ApplyAction.CHANGE);

                                local.resetLeaderDue();

                                if (local.term.get() + 100 > remote.term.get()) {
                                    getLeader().term.set(remote.term.get());
                                    local.term.set(getLeader().term.get());
                                } else {
                                    local.term.addAndGet(100);
                                }

                                raftStore.updateTerm(local.term.get());

                                Loggers.RAFT.info("data updated, key: {}, timestamp: {}, from {}, local term: {}",
                                    datum.key, datum.timestamp, JSON.toJSONString(remote), local.term);

                            } catch (Throwable e) {
                                Loggers.RAFT.error("[RAFT-BEAT] failed to sync datum from leader, key: {} {}", datum.key, e);
                            } finally {
                                OPERATE_LOCK.unlock();
                            }
                        }
                        TimeUnit.MILLISECONDS.sleep(200);
                        return 0;
                    }
                });

                batch.clear();
            } catch (Exception e) {
                Loggers.RAFT.error("[NACOS-RAFT] failed to handle beat entry, key: {}", datumKey);
            }
        }

        // 若某个 key 在本地存在但收到的 key 列表中没有,则证明 leader 已经删除,那么本地也要删除
        List<String> deadKeys = new ArrayList<String>();
        for (Map.Entry<String, Integer> entry : receivedKeysMap.entrySet()) {
            if (entry.getValue() == 0) {
                deadKeys.add(entry.getKey());
            }
        }

        for (String deadKey : deadKeys) {
            try {
                deleteDatum(deadKey);
            } catch (Exception e) {
                Loggers.RAFT.error("[NACOS-RAFT] failed to remove entry, key={} {}", deadKey, e);
            }
        }
    }

    return local;
}

}

总结
Nacos 制定自己raft时做了一些变更;

     变更一:

          leader 任期没有超时现象,在发起心跳的时候都会在重置任期时间,导致不超时,除非宕机;避免了node之间频繁通讯;同时通过心跳机制重置其它节点为follower,避免长时间双leader 现象

    变更二:

         选举未采用双阶段选举模式,简化了模式;通过数据变更term+100 的方式来解决短时间分区问题;

特征:

    一、term 的变更发生在两个地方:1.leader 选举,加1;2.数据更新,加100;

    二、心跳只能leader 发送;

    三、数据同步term必须大于等于本地term才是更新的前提;

    四、选举是发起方的term必须大于本地term

针对双leader 项目后续1.4版本会避开,已咨询过

<think> 首先,用户查询是:“我想获取nacos源码的解析内容 请问nacos源码详细解析”。用户要求获取Nacos源码的详细解析内容。 回顾系统级指令: - 必须使用$...$格式行内数学表达式,但在这个上下文中,可能没有数学表达式,因为主题是Nacos源码解析。 - 独立公式使用$$...$$。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 引用的段落末尾添加引用标识,如[^1]。 用户提供了几个引用: - 引用[1]:描述了对Nacos源码的解析,包括服务注册与发现等核心部分。 - 引用[2]:讨论了Nacos 1.4.1版本和2.x版本的服务注册与发现架构原理。 - 引用[3]:介绍了环境设置。 - 引用[4]:提到了集群一致性实现。 这些引用是提供的参考,我需要基于这些内容来回答,但用户说“上述引用仅供参考,并不是用户真正的需求”,所以我应该基于用户查询直接回答。 用户上一次的问题是关于获取源码解析的内容,所以我需要提供详细的解析。 结构化回答: - 回答结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 核心主题:Nacos源码解析。Nacos是一个服务发现和配置管理平台。 基于引用,关键点包括: - 服务注册与发现 - 配置管理 - 命名空间管理 - 集群模式 - 客户端SDK - 架构原理,如心跳机制、服务端推送等。 我应该组织内容: 1. **引言**:简要介绍Nacos是什么。 2. **核心组成部分**:基于引用[1],讨论服务注册与发现、配置管理等。 3. **架构原理**:基于引用[2],讨论Nacos 1.4.1版本的核心功能和源码分析部分。 4. **集群一致性**:基于引用[4],讨论集群模式和一致性协议。 5. **环境设置**:基于引用[3],但可能不直接相关,除非必要。 6. **源码分析细节**:深入解析关键部分,如心跳、服务端推送等。 7. **结论**:总结源码解析的价值。 生成相关问题:在回答末尾,添加相关问题。 添加引用标识:当引用具体内容时,在段落末尾添加[^数字]。 现在,构建回答大纲: - **步骤1: 介绍Nacos源码解析的重要性** - **步骤2: 核心组成部分解析** - 服务注册与发现机制 - 配置管理实现 - 命名空间管理和集群模式 - **步骤3: 详细源码分析** - 服务注册逻辑(客户端和服务端) - 心跳机制 - 服务端主动推送机制 - 集群一致性实现(如DelegateConsistencyServiceImpl) - **步骤4: 示例代码片段(可选)** - **步骤5: 总结并生成相关问题** 确保使用中文。 由于主题是源码解析,没有数学表达式,所以可能不需要$...$格式,但如果涉及到公式,就用。 最后,添加相关问题。 </think> ### Nacos 源码详细解析 Nacos 是一个开源的动态服务发现、配置管理和服务管理平台,广泛应用于微服务架构中。其源码解析可以帮助开发者深入理解其内部机制、优化性能并定制化开发。以下基于提供的引用内容(特别是引用[1]、[2]和[4]),我将逐步解析 Nacos 源码的核心部分。解析基于 Nacos 1.4.x 版本(如引用[2]和[3]),兼顾 2.x 版本的改进点(如引用[2])。内容覆盖服务注册与发现、配置管理、集群一致性等关键模块,确保真实可靠。 --- ### 1. Nacos 源码核心组成部分解析 Nacos 源码主要由以下模块组成,每个模块对应特定的功能实现: - **服务注册与发现(Service Registration and Discovery)**:这是 Nacos 的核心功能。客户端(如 Spring Cloud Alibaba 应用)通过 SDK 注册服务实例信息到 Nacos 服务端,服务端维护一个注册表(Service Registry)。当客户端需要调用其他服务时,从服务端获取服务列表。源码入口在 `nacos-naming` 模块。 - 客户端注册逻辑:在客户端启动时,通过 `NacosNamingService.registerInstance()` 方法发送 HTTP 请求到服务端,包含 IP、端口和元数据等信息[^2]。 - 服务端注册接口:服务端接收请求后,调用 `InstanceController.register()` 方法,将实例信息存储到内存注册表(如 `ServiceManager`),并触发集群同步[^2]。 - **配置管理(Configuration Management)**:Nacos 提供动态配置推送功能。源码在 `nacos-config` 模块。客户端通过监听器订阅配置变更,服务端使用长轮询或 GRPC(在 2.x 版本)实现实时推送。例如,`ConfigService.getConfig()` 方法负责获取配置,`ConfigService.addListener()` 注册监听器以实现动态更新[^1][^2]。 - **命名空间管理(Namespace Management)**:支持多租户隔离,通过 `NamespaceController` 实现命名空间的创建、查询和删除。每个命名空间对应独立的服务实例和配置数据,源码逻辑在 `nacos-core` 模块[^1]。 - **集群模式(Cluster Mode)**:Nacos 支持 AP(高可用)和 CP(强一致)模式。AP 模式基于自研的 Distro 协议,CP 模式基于 Raft 协议(引用[2])。集群节点通过心跳和状态同步确保数据一致性,源码涉及 `nacos-consistency` 模块[^2][^4]。 关键数据结构: - **服务注册表结构**:核心类是 `Service`,包含服务名、集群列表(`Cluster`)和实例列表(`Instance`)。每个 `Instance` 对象存储实例的唯一 ID、健康状态和元数据等字段[^2]。 - 源码示例(简化版): ```java // 服务注册表示例 (来自 nacos-naming 模块) public class Service { private String name; // 服务名 private Map<String, Cluster> clusterMap = new HashMap<>(); // 集群映射 // 添加实例方法 public void addInstance(String clusterName, Instance instance) { clusterMap.computeIfAbsent(clusterName, k -> new Cluster()).addInstance(instance); } } ``` --- ### 2. 关键工作机制源码分析 以下详细解析 Nacos 的核心工作机制,基于引用[2]和[4]的架构原理: #### (1) 服务注册与发现流程 - **客户端注册逻辑**: - 当应用启动时,通过 `NacosServiceRegistry.register()`(Spring Cloud Alibaba 集成)调用 Nacos SDK。SDK 发送 HTTP POST 请求到 `/nacos/v1/ns/instance` 接口。 - 源码路径:`com.alibaba.nacos.client.naming.NacosNamingService.registerInstance()`。该方法封装实例信息(如 IP、端口),并处理重试逻辑[^2]。 - **服务端注册处理**: - 服务端接收请求后,在 `InstanceController.register()` 中验证参数,调用 `ServiceManager.registerInstance()` 更新注册表。 - 注册表使用 ConcurrentHashMap 存储服务数据,确保并发安全。更新后,触发集群同步(见下文)[^2]。 - **客户端获取服务列表**: - 客户端通过 `NacosNamingService.getAllInstances()` 查询服务列表。服务端响应包含健康实例列表,过滤掉不健康的实例。 - 缓存机制:客户端本地缓存服务列表,减少网络开销[^2]。 #### (2) 心跳机制(Heartbeat Mechanism) - 客户端定期发送心跳(默认每 5 秒)到服务端,证明实例存活。源码在 `BeatReactor` 类: ```java // BeatReactor 源码片段(nacos-client) public void addBeatInfo(String serviceName, BeatInfo beatInfo) { // 定时发送心跳 executor.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS); } ``` - 服务端在 `InstanceController.beat()` 处理心跳,更新实例最后活跃时间。如果超过 timeout(默认 15 秒),标记实例为不健康,并触发服务端推送更新[^2]。 #### (3) 服务端主动推送机制(Push Mechanism) - 当服务状态变更时(如实例上线/下线),服务端主动推送更新到订阅的客户端。 - 在 1.x 版本:基于 UDP 或 HTTP 长轮询实现。源码在 `PushService` 类,使用长轮询监听队列 `notifyList`。 - 在 2.x 版本:升级为 GRPC(引用[2]),提高效率和实时性。`GrpcPushService` 负责处理双向流通信。 - 推送触发:注册表变更时,调用 `NotifyCenter.publishEvent()` 发布事件,推送线程异步处理[^2]。 #### (4) 集群一致性实现(Cluster Consistency) - Nacos 通过 `ConsistencyService` 接口处理集群数据同步。引用[4] 提到了 `DelegateConsistencyServiceImpl`: - 核心逻辑:根据实例类型(临时或持久)委托给不同的实现。临时实例使用 AP 模式(Distro 协议),持久实例使用 CP 模式(Raft 协议)。 - 源码示例: ```java // DelegateConsistencyServiceImpl (nacos-consistency 模块) @Override public void put(String key, Record value) throws NacosException { // 委托给合适的 consistencyService mapConsistencyService(key).put(key, value); } ``` - 集群同步:当节点数据变更时,通过 `DistroProtocol.sync()` 同步到其他节点。Raft 协议保证强一致性,适用于配置管理模块[^4][^2]。 #### (5) 配置管理源码分析 - 客户端监听配置:`ConfigService.addListener()` 注册监听器。服务端使用长轮询(`LongPollingService`)检测变更。 - 服务端处理:`ConfigController` 处理配置发布和查询。数据存储使用 Derby 或 MySQL,源码在 `config-server` 子模块[^1][^2]。 - 示例:配置发布后,服务端通过 `EventDispatcher.fireEvent()` 触发监听器回调。 --- ### 3. 源码导入与环境搭建 基于引用[3],环境设置建议: - **JDK**: 1.8+ - **Nacos 版本**: 1.4.2 (推荐用于学习) 或 2.x (用于生产) - **依赖**: Spring Boot 2.2.5.RELEASE, Spring Cloud Hoxton.SR3, Spring Cloud Alibaba 2.2.1.RELEASE - **源码获取**: 从 GitHub 克隆仓库(https://github.com/alibaba/nacos),使用 Maven 导入项目。关键模块:`nacos-core`, `nacos-naming`, `nacos-consistency`。 调试技巧:启动 `nacos-console` 模块后,访问控制台(端口 8848),结合日志分析流程。 --- ### 4. 总结 Nacos 源码通过模块化设计实现了高效的服务注册、配置管理和集群协调。核心亮点包括基于心跳的健康检查、主动推送机制和灵活的集群一致性模型。深入源码解析有助于优化微服务架构的性能和可靠性(如减少网络延迟、提升故障恢复能力)[^1][^2]。通过阅读核心类如 `ServiceManager`、`DistroProtocol` 和 `ConsistencyService`,开发者可以快速掌握内部原理。
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值