Nacos源码分析之leader选举(一)

Nacos一致性协议

分布式一致性协议有很多,例如Paxos协议,Zab协议,Raft协议,而Nacos采用的是Distro协议和Raft协议。对于非临时数据,Nacos采用的是Raft协议,而临时数据Nacos采用的是Distro协议。简单说一下Distro,Distro协议被定位为临时数据的一致性协议:该类型协议不需要把数据存储到磁盘或者数据库,因为临时数据通常和服务器保持一个session会话,该会话只要存在,数据就不会丢失。本篇文章主要针对于CP的Raft算法。

Raft协议

Raft协议是一种强一致性、去中心化、高可用的分布式协议,它是用来解决分布式一致性问题的,相对于大名鼎鼎的Paxos协议,Raft协议更容易理解,并且在性能、可靠性、可用性方面是不输于Paxos协议的。许多中间件都是利用Raft协议来保证分布式一致性的,例如Redis的sentinel,CP模式的Nacos的leader选举都是通过Raft协议来实现的。因为Nacos的一致性协议是采用的Raft协议,所以建议在看相关代码前先了解Raft协议。
可以通过下面链接的动图了解Raft算法:
raft算法

Nacos之leader选举(version:1.3.2)

leader选举流程

Nacos源码分析之leader选举(一)_第1张图片

首先是init方法,init方法上存在@PostConstruct注解,所以在RaftCore初始化后就会执行init方法。

    @PostConstruct
    public void init() throws Exception {

        Loggers.RAFT.info("initializing Raft sub-system");
        //建立一个监听,监听Datums的修改和删除事件,(利用生产者消费者模式)
        executor.submit(notifier);

        final long start = System.currentTimeMillis();
        //加载本地磁盘的Datums和Term进行数据恢复
        raftStore.loadDatums(notifier, datums);
        
        setTerm(NumberUtils.toLong(raftStore.loadMeta().getProperty("term"), 0L));

        Loggers.RAFT.info("cache loaded, datum count: {}, current term: {}", datums.size(), peers.getTerm());

        while (true) {
            if (notifier.tasks.size() <= 0) {
                break;
            }
            Thread.sleep(1000L);
        }

        initialized = true;

        Loggers.RAFT.info("finish to load data from disk, cost: {} ms.", (System.currentTimeMillis() - start));
        //每隔500ms去检查一次leader状态
        GlobalExecutor.registerMasterElection(new MasterElection());
        GlobalExecutor.registerHeartbeat(new HeartBeat());

        Loggers.RAFT.info("timer started: leader timeout ms: {}, heart-beat timeout ms: {}",
                GlobalExecutor.LEADER_TIMEOUT_MS, GlobalExecutor.HEARTBEAT_INTERVAL_MS);
    }

在这个方法中主要做了一下几件事:
1.建立一个监听,监听Datums的修改和删除事件
2.加载本地磁盘的Datums和Term进行数据恢复
3.注册leader选举的任务
4.注册发送心跳的任务

leader选举相关代码

在init方法中注册了一个每500ms执行一次的leader选举的任务MasterElection,

public class MasterElection implements Runnable {

        @Override
        public void run() {
            try {

                if (!peers.isReady()) {
                    return;
                }
                //得到本机信息
                RaftPeer local = peers.local();
                //leaderDueMs 发起投票的倒计时初始化是0-15000ms之间的随机值
                // 任务每500ms执行一次,每次进来-500ms
                local.leaderDueMs -= GlobalExecutor.TICK_PERIOD_MS;

                if (local.leaderDueMs > 0) {
                    return;
                }
                //倒计时小于0时进行拉票
                // reset timeout
                //重置leader选举时间
                local.resetLeaderDue();
                //重置心跳发送时间
                local.resetHeartbeatDue();
                //拉票
                sendVote();
            } catch (Exception e) {
                Loggers.RAFT.warn("[RAFT] error while master election {}", e);
            }

        }

leaderDueMs(默认是0-15000ms之间的随机数)每次进入到这个方法中都会减少500ms,当leaderDueMs减少到小于等于0,那么就会发起投票进行选举。而选举拉票的逻辑,主要就是sendVote()方法。

private void sendVote() {

            RaftPeer local = peers.get(NetUtils.localServer());
            Loggers.RAFT.info("leader timeout, start voting,leader: {}, term: {}", JacksonUtils.toJson(getLeader()),
                    local.term);

            peers.reset();//重置leader和票据

            local.term.incrementAndGet();//增加term
            local.voteFor = local.ip; //当前投自己
            local.state = RaftPeer.State.CANDIDATE;//设置当前状态为候选人

            Map params = new HashMap<>(1);
            params.put("vote", JacksonUtils.toJson(local));
            //遍历除本机之外的机器
            for (final String server : peers.allServersWithoutMySelf()) {
                //构建http请求
                final String url = buildUrl(server, API_VOTE);
                try {
                    HttpClient.asyncHttpPost(url, null, params, new AsyncCompletionHandler() {
                        @Override
                        public Integer onCompleted(Response response) throws Exception {
                            if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
                                Loggers.RAFT
                                        .error("NACOS-RAFT vote failed: {}, url: {}", response.getResponseBody(), url);
                                return 1;
                            }
                            //获取其他节点的信息
                            RaftPeer peer = JacksonUtils.toObj(response.getResponseBody(), RaftPeer.class);

                            Loggers.RAFT.info("received approve from peer: {}", JacksonUtils.toJson(peer));
                            //决定谁是leader
                            peers.decideLeader(peer);

                            return 0;
                        }
                    });
                } catch (Exception e) {
                    Loggers.RAFT.warn("error while sending vote to server: {}", server);
                }
            }
        }
    }

而sendVote()方法主要就是做了以下几件事:
1.重置leader和选票

    public void reset() {
        leader = null;
        for (RaftPeer peer : peers.values()) {
            peer.voteFor = null;
        }
    }

2.把本机的term+1,把选票投给自己,并且设置自己为候选人的角色

local.term.incrementAndGet();//增加term
local.voteFor = local.ip; //当前投自己
local.state = RaftPeer.State.CANDIDATE;//设置当前状态为候选人

3.遍历本机以外的机器,发送异步请求到 /v1/ns/raft/vote接口,获取其他机器的选票信息

4.处理请求结果,根据请求的结果决定leader

 peers.decideLeader(peer);

首先看集群其他机器接收到请求后的处理逻辑,请求到RaftController的 /v1/ns/raft/vote接口后

@PostMapping("/vote")
public JsonNode vote(HttpServletRequest request, HttpServletResponse response) throws Exception {
        RaftPeer peer = raftCore.receivedVote(JacksonUtils.toObj(WebUtils.required(request, "vote"), RaftPeer.class));
        return JacksonUtils.transferToJsonNode(peer);
    }

最主要的逻辑就是在raftCore.receivedVote()

    public synchronized RaftPeer receivedVote(RaftPeer remote) {
        if (!peers.contains(remote)) {
            throw new IllegalStateException("can not find peer: " + remote.ip);
        }
        RaftPeer local = peers.get(NetUtils.localServer());
        if (remote.term.get() <= local.term.get()) {
            //如果请求的任期小于自己的任期并且还没有投出选票,那么将票投给自己
            String msg = "received illegitimate vote" + ", voter-term:" + remote.term + ", votee-term:" + local.term;

            Loggers.RAFT.info(msg);
            if (StringUtils.isEmpty(local.voteFor)) {
                local.voteFor = local.ip;
            }

            return local;
        }
        //选举时间重置
        local.resetLeaderDue();
        //状态标记为follower,并且为请求过来的机器投票
        local.state = RaftPeer.State.FOLLOWER;
        local.voteFor = remote.ip;
        local.term.set(remote.term.get());

        Loggers.RAFT.info("vote {} as leader, term: {}", remote.ip, remote.term);

        return local;
    }

这个方法主要就是处理自己的选票的,当收到其他机器拉票的请求的时候,会比较term,如果自身的term大于全程请求机器的term,并且自己的选票没有还没投出去的时候,就把选票投给自己,否则将选票投给远程请求的机器,并且把自己的状态设置为follower,并且把信息返回出去。

在收到其他机器的返回的信息后,会根据结果决定leader,而这块的主要逻辑就是在 peers.decideLeader(peer)中。

    public RaftPeer decideLeader(RaftPeer candidate) {
        peers.put(candidate.ip, candidate);

        SortedBag ips = new TreeBag();
        //选票最多的票数
        int maxApproveCount = 0;
        //选票最多的ip
        String maxApprovePeer = null;
        for (RaftPeer peer : peers.values()) {
            if (StringUtils.isEmpty(peer.voteFor)) {
                continue;
            }
            //收集选票
            ips.add(peer.voteFor);
            //获取选票的最大值,并且设置选票最多的ip
            if (ips.getCount(peer.voteFor) > maxApproveCount) {
                maxApproveCount = ips.getCount(peer.voteFor);
                maxApprovePeer = peer.voteFor;
            }
        }
        //如果票数大于半数,将最多票数的节点状态设置为leader状态
        if (maxApproveCount >= majorityCount()) {
            RaftPeer peer = peers.get(maxApprovePeer);
            peer.state = RaftPeer.State.LEADER;
            // 如果当前leader和选举出来的leader不是同一个,那么将选举的leader重置并且发布一个leader选举完成的事件
            if (!Objects.equals(leader, peer)) {
                leader = peer;
                ApplicationUtils.publishEvent(new LeaderElectFinishedEvent(this, leader, local()));
                Loggers.RAFT.info("{} has become the LEADER", leader.ip);
            }
        }

        return leader;
    }

其实这个方法就是看是否有机器已经获得一半以上的票数,如果有,那么设置它为leader,leader选举也就完成了。

至于发送心跳的部分,由于篇幅原因就在下一篇文章中讲解。

最后

个人觉得阿里巴巴体系的源码还是比较好懂的,无论是nacos还是dubbo,感觉看起来不是太难,只要花时间应该都可以看得懂。

你可能感兴趣的:(Nacos源码分析之leader选举(一))