Nacos集群的CP架构,CAP原则与BASE原则的应用


1. CAP原则

        CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)这三个要素最多只能同时实现两点,不可能三者兼顾。

  • C:一致性(Consistency)
    • 一致性要求各个节点查询的数据都一致,如果某个节点由于出现了分区,不能及时同步其他节点的数据,那么这个节点要么停止使用,要么整个系统停止使用。所以如果要保证强一致性,则会牺牲掉节点的可用性!比如zookeeper,当发生网络分区时,为了保证数据一致性,非Leader分区下的节点将变为不可用,并重新进入选举状态。
  • A:可用性(Availability)
    • 可用性要求所有节点尽量可用,就算出现了网络分区,不同节点之间的数据出现了不一致,但是仍然让该节点可用,所以会牺牲数据一致性。因为从不同节点读取到的数据可能不同!比如eurak、nacos都支持AP架构,当发生网络分区时,所有节点仍然可以读数据,但不保证读到的数据是最新的,不过也不用担心,最终会通过心跳保证数据的最终一致性!
  • P:分区容错性(Partition tolerance)
    • CAP协议的P是一定要保证的!不能说发生了网络分区,系统就不能提供服务了
    • 分区:分区是指网络分区。在多节点部署的系统中,由于网络原因,节点之间无法通信、无法同步数据,就出现了网络分区
    • 容错:容错是指当因为网络原因,系统节点出现了分区,对外仍然要提供服务。

各种分布式中间件使用的架构如下:

  • mysql单机:CA架构
  • eureka集群:AP架构
  • zookeeper集群:CP架构
  • nacos集群:AP或CP架构,可根据实例的(临时、持久)类型,来选择AP或CP
  • redis集群:主要是AP架构,也可通过配置min-slaves-to-write = x(大于1个节点)去模拟CP架构


2. BASE原则

  • BA:基本可用(Basically Available)
  • S:软状态(Soft State)
  • E:最终一致性(Eventual Consistency)

        CAP原则是三选二,BASE原则是CAP的折中,C,A,P三个都要,但不用100%的保证每一个原则。分布式系统肯定优先保证P,多数时候是在CA之间做权衡选择!

        满足AP的系统在一定程度上也可以说是符合 BASE原则的,比如eurka集群,三个节点挂了两个,系统还是基本可用的(BA)。此时如果有系统来注册了,因为挂了两个节点,这时整个系统各个节点的数据是不一致的,但是等挂掉的两个节点恢复了,数据会同步过去,保证最终一致性(E),对于中间数据暂时不一致的状态可以称为软状态(S)!


3. CP架构是如何防止脑裂问题的

AP架构

  • 向一个节点A写入数据成功后,立刻给客户端响应写成功的信号。
  • 如果此时集群节点之间网络断开了,由于其可用性,其他节点仍然提供服务,但是A节点的数据还未写入到其他节点,当访问除A之外的其他节点时,就会出现数据不一致的问题,当网络恢复后,才会通过心跳保证最终一致性!

CP架构

  • 在向一个节点A写入数据成功后,并不是马上给客户端响应写成功的信号,而是等待数据同步到其他节点后(个数取决于配置),才响应客户端,表示此次写数据成功了!这在一定程度上保证了数据一致性。为了防止数据混乱,写数据时只允许往Leader节点写,读数据时可以从所有节点读取!
  • CP架构下具有特殊的Leader - Flower机制,当发生网络分区时,非Leader分区下的节点会变成不可用,重新进入选举状态,

nacos和zookeeper是如何防止脑裂的?

  • 集群的脑裂通常是发生在集群之间通信不可达(分区)的情况下,一个大集群会分裂成不同的小集群,小集群中又各自选举出自己的master节点,导致原先的集群出现多个master节点对外提供服务的情况!
  • leader选举时,要求节点获取到的投票数量 > 总节点数量/2,有了这个选举原则,当发生网络分区时,无论如何最多只有一个小集群选出leader,避免集群发生脑裂。

集群节点个数为什么推荐是奇数个?

  • 在集群启动时,偶数个节点的集群一旦节点对半分区(比如4个节点分区成2个节点和2个节点的情况),整个集群无法选出leader,集群无法提供服务,无法满足CAP中的P
  • 容错能力相同的情况下,奇数节点比偶数节点更节省资源,比如5个节点最多挂掉2个节点还能选leader,6个节点最多也只能挂掉2个节点才能保证可以选leader


4. Nacos的CP架构源码解析

        Nacos的 CP 和 AP 架构的选择,取决于我们配置的服务实例是临时实例还是持久实例

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        group:  mall-order
        cluster-name: SH
        ephemeral: false   //持久化实例,使用 CP架构
        ephemeral: true	   //临时实例,使用 AP架构        

        AP架构的源码解析之前已经发表过一篇文章,可以点击查看:Nacos的AP架构下,服务的注册与发现!本节主要解释一下Nacos的CP架构的以下几点:其他功能与AP架构类似,不做赘述!

  • 持久化服务注册
  • Leader选举
  • 发送心跳、同步数据


4.1 注册持久化服务,同步其他节点!

        CP架构下的服务的注册与AP架构不同点在于:向nacos服务端注册实例时的consistencyService.put(key, instances)方法实现不同!

    public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
            throws NacosException {

        //获取实例的key。key分为临时实例 和 持久化实例
        //根据入参ephemeral去判断,ephemeral默认为true,默认是临时实例
        String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);

        Service service = getService(namespaceId, serviceName);

        synchronized (service) {
            //更新或者新增(临时、持久)实例
            List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);

            Instances instances = new Instances();
            instances.setInstanceList(instanceList);
            //把(临时、持久)实例放入队列
            //注意:此处会根据实例类型 选择AP架构或者CP架构 的存储方式!
            consistencyService.put(key, instances);
        }
    }

在执行consistencyService.put方法时,nacos会根据不同的实例类型选择不同的架构

  1. 临时实例,选择AP架构,使用Distro协议,分布式协议的一种,阿里内部的协议,服务是放在内存中!
  2. 持久实例,选择CP架构,使用Raft协议来实现,点击查看Raft协议详情!服务是放在磁盘中!

在这里插入图片描述
由于本节探讨的CP架构,使用的Raft协议,所以进入RaftConsistencyServiceImpl类中,查看真正的注册逻辑

  • 由于Raft协议规定,写操作只能由Leader节点去操作你,所以要先查看本节点是否是leader,如果当前节点不是leader,需要把这个写请求发给Leader,让Leader节点去操作,进行服务注册!
  • 如果本节点就是Leader,则开始服务注册
    • 服务注册时,先加同步锁,再向磁盘中写入文件,文件内容就是当前服务
    • 把服务写入磁盘中nacos/data/naming/datas/public/xxx服务文件 目录下,并发布服务变更事件,nacos监听到此事件并修改服务列表
    • Leader节点写完后,需要把数据同步到其他节点,使用CountDownLatch(集群节点数/2 + 1)保证集群半数节点以上同步成功

代码如下:

com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore#RaftCore 类中signalPublish方法如下:

    public void signalPublish(String key, Record value) throws Exception {
        if (stopWork) {
            throw new IllegalStateException("old raft protocol already stop work");
        }
        //如果当前server节点不是leader
        if (!isLeader()) {
            ObjectNode params = JacksonUtils.createEmptyJsonNode();
            params.put("key", key);
            params.replace("value", JacksonUtils.transferToJsonNode(value));
            Map<String, String> parameters = new HashMap<>(1);
            parameters.put("key", key);

            //获取leader节点
            final RaftPeer leader = getLeader();
            //把当前服务的写入请求,发给leader节点,让leader去做
            raftProxy.proxyPostLarge(leader.ip, API_PUB, params.toString(), parameters);
            return;
        }
        //如果当前节点是leader,先加锁,再向磁盘写入文件,文件内容就是当前服务
        OPERATE_LOCK.lock();
        try {
            final long start = System.currentTimeMillis();
            final Datum datum = new Datum();
            datum.key = key;
            datum.value = value;
            if (getDatum(key) == null) {
                datum.timestamp.set(1L);
            } else {
                datum.timestamp.set(getDatum(key).timestamp.incrementAndGet());
            }

            ObjectNode json = JacksonUtils.createEmptyJsonNode();
            json.replace("datum", JacksonUtils.transferToJsonNode(datum));
            json.replace("source", JacksonUtils.transferToJsonNode(peers.local()));
            //把服务写入磁盘,并发布服务变动事件,
            // 监听器监听到事件后,更新内存中服务列表(双层map结构)
            //注意:如果严格按照Raft的协议来做的话,应该是写入本地磁盘文件后,立马通知其他集群节点
            // 但在这里的顺序是: 写磁盘--更新内存服务列表--通知集群其他节点 
            onPublish(datum, peers.local());

            final String content = json.toString();
            //CP架构下,leader写完后需要同步给其他节点
            //使用CountDownLatch来做同步
            //peers.majorityCount() = 集群节点数/2 + 1,代表集群半数节点同步成功
            final CountDownLatch latch = new CountDownLatch(peers.majorityCount());
            //peers.allServersIncludeMyself():遍历包括当前节点在内的节点
            for (final String server : peers.allServersIncludeMyself()) {
                if (isLeader(server)) {
                    //如果是当前节点 CountDownLatch-1
                    latch.countDown();
                    //继续循环
                    continue;
                }
                //如果是其他节点,调API发送服务信息
                final String url = buildUrl(server, API_ON_PUB);
                //异步发送
                HttpClient.asyncHttpPostLarge(url, Arrays.asList("key", key), content, new Callback<String>() {
                    @Override
                    public void onReceive(RestResult<String> result) {
                        //发送回调
                        if (!result.ok()) {
                            Loggers.RAFT
                                    .warn("[RAFT] failed to publish data to peer, datumId={}, peer={}, http code={}",
                                            datum.key, server, result.getCode());
                            return;
                        }
                        //发送完 CountDownLatch-1
                        latch.countDown();
                    }

                    @Override
                    public void onError(Throwable throwable) {
                        Loggers.RAFT.error("[RAFT] failed to publish data to peer", throwable);
                    }

                    @Override
                    public void onCancel() {

                    }
                });

            }
            // await 等待CountDownLunch执行完毕!
            //如果超时 ,抛异常!
            //但是这里有个bug:往其他节点写数据出现问题时,这里跑了异常,但是主节点却保存服务成功了!!理论上主节点应该同时保存失败的!
            //新版本使用了jRaft协议来替换,使用两段式提交的方式避免了这个bug
            if (!latch.await(UtilsAndCommons.RAFT_PUBLISH_TIMEOUT, TimeUnit.MILLISECONDS)) {
                // only majority servers return success can we consider this update success
                Loggers.RAFT.error("data publish failed, caused failed to notify majority, key={}", key);
                throw new IllegalStateException("data publish failed, caused failed to notify majority, key=" + key);
            }

            long end = System.currentTimeMillis();
            Loggers.RAFT.info("signalPublish cost {} ms, key: {}", (end - start), key);
        } finally {
            OPERATE_LOCK.unlock();
        }
    }

        其中向磁盘写入服务,并发布服务变更时事件ValueChangeEventonPublish方法如下:该服务变更时事件ValueChangeEvent被监听到后会触发updateIps(),该方法与nacos的AP架构中的介绍一致!可自行前往查看!

    public void onPublish(Datum datum, RaftPeer source) throws Exception {
    
		。。。。。 //省略代码
	
        // if data should be persisted, usually this is true:
        if (KeyBuilder.matchPersistentKey(datum.key)) {
            //向磁盘写入服务文件,目录为:nacos/data/naming/datas/public/xxx服务文件
            raftStore.write(datum);
        }
        
		。。。。。 //省略代码
	
        //服务写完后,发布服务变动事件,nacos监听到此事件并修改服务列表
        NotifyCenter.publishEvent(ValueChangeEvent.builder().key(datum.key).action(DataOperation.CHANGE).build());
        Loggers.RAFT.info("data added/updated, key={}, term={}", datum.key, local.term);
    }

接下来总结一下使用CP架构时,nacos服务端的服务注册逻辑:

  1. Leader节点把服务写入磁盘中nacos/data/naming/datas/public/xxx服务文件 目录下
  2. 同时发布服务变动事件,由其他线程监听事件,更新nacos server端的服务列表(双层map结构)
  3. 通过异步的方式同步集群中其他节点,其他节点与Leader节点的操作一致!


4.2 Leader选举

        上面介绍了CP架构下的服务注册,接下来看一下nacos集群启动时是如何进行Leader选举的!由于nacos的CP架构使用的Raft协议,所以在Nacos集群启动时,也会经过半数选举机制为集群选择一个Leader节点,负责接收数据,同步数据!Raft协议中只是简单画出了Leader选举示意图,点击可查看!.,接下来看一下Ncaos底层是如何实践Raft协议的:

        Nacos的leader选举是发生在RaftCore类中的!但是这个类在源码中使用了@Deprecated标识,说明这个类可能在未来会过期,被新的实现替换掉。目前还是先研究一下这个类吧!

com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore

        RaftCore类中有一个init方法,使用了@PostConstruct标识,说明这个方法会在RaftCore类初始化完成时调用,进入init方法:

   //leader选举方法
    @PostConstruct
    public void init() throws Exception {
        Loggers.RAFT.info("initializing Raft sub-system");
        final long start = System.currentTimeMillis();
        //从服务存储目录中加载服务文件到内存中
        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());

        initialized = true;

        Loggers.RAFT.info("finish to load data from disk, cost: {} ms.", (System.currentTimeMillis() - start));

        //两个延时定时线程池 执行两个任务
        //1.leader选举任务
        masterTask = GlobalExecutor.registerMasterElection(new MasterElection());
        //2.心跳任务
        heartbeatTask = GlobalExecutor.registerHeartbeat(new HeartBeat());

        versionJudgement.registerObserver(isAllNewVersion -> {
            stopWork = isAllNewVersion;
            if (stopWork) {
                try {
                    shutdown();
                    raftListener.removeOldRaftMetadata();
                } catch (NacosException e) {
                    throw new NacosRuntimeException(NacosException.SERVER_ERROR, e);
                }
            }
        }, 100);

        NotifyCenter.registerSubscriber(notifier);

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

可以看到在init方法内部有两个延时定时线程池,分别执行了leader选举任务 和 心跳任务!

 	//Leader选举线程池 ,立即执行一次选择,然后每500ms执行一次
 	//TICK_PERIOD_MS : 500 毫秒
    public static ScheduledFuture registerMasterElection(Runnable runnable) {
        return NAMING_TIMER_EXECUTOR.scheduleAtFixedRate(runnable, 0, TICK_PERIOD_MS, TimeUnit.MILLISECONDS);
    }

	//心跳任务线程池: 与leader选举一致
    public static ScheduledFuture registerHeartbeat(Runnable runnable) {
        return NAMING_TIMER_EXECUTOR.scheduleWithFixedDelay(runnable, 0, TICK_PERIOD_MS, TimeUnit.MILLISECONDS);
    }

        我们先看Leader选举任务是如何执行的,进入new MasterElection()类中,由于MasterElection类实现了Runnable接口,所以直接进入其run()方法内部!

   public class MasterElection implements Runnable {
		
		//进入run方法!
        @Override
        public void run() {
            try {
                if (stopWork) {
                    return;
                }
                //选举完成后进入,直接return,不会一直选举
                if (!peers.isReady()) {
                    return;
                }

                RaftPeer local = peers.local();
                //随机休眠
                local.leaderDueMs -= GlobalExecutor.TICK_PERIOD_MS;

                if (local.leaderDueMs > 0) {
                    return;
                }

                // reset timeout
                local.resetLeaderDue();
                local.resetHeartbeatDue();
                //休眠结束 进行投票,进入下面的投票方法
                sendVote();
            } catch (Exception e) {
                Loggers.RAFT.warn("[RAFT] error while master election {}", e);
            }

        }
        
        ==========================================
        
		//投票方法
        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();
            //选举周期 +1
            local.term.incrementAndGet();
            //voteFor先投给自己
            local.voteFor = local.ip;
            //设置状态为:候选者CANDIDATE,候选者才能参与投票
            local.state = RaftPeer.State.CANDIDATE;

            Map<String, String> params = new HashMap<>(1);
            params.put("vote", JacksonUtils.toJson(local));
            //遍历除了自己之外的节点
            for (final String server : peers.allServersWithoutMySelf()) {
                //调用nacos提供的API给除自己之外的其他节点发送选票信息
                final String url = buildUrl(server, API_VOTE);
                try {
                    //异步发送
                    HttpClient.asyncHttpPost(url, null, params, new Callback<String>() {
                        @Override
                        //发送后回调,回调主要是获取其他节点给当前节点的投票结果
                        public void onReceive(RestResult<String> result) {
                            if (!result.ok()) {
                                Loggers.RAFT.error("NACOS-RAFT vote failed: {}, url: {}", result.getCode(), url);
                                return;
                            }
                            //从别处收到的投票结果
                            //如果当前节点随机时间最先走完,这里收到选票结果肯定其他节点都投的是自己(当前节点)!
                            RaftPeer peer = JacksonUtils.toObj(result.getData(), RaftPeer.class);

                            Loggers.RAFT.info("received approve from peer: {}", JacksonUtils.toJson(peer));
                            //根据收到的选票个数看是否大于集群半数,来决定当前节点是否被选为leader
                            peers.decideLeader(peer);

                        }

                        @Override
                        public void onError(Throwable throwable) {
                            Loggers.RAFT.error("error while sending vote to server: {}", server, throwable);
                        }

                        @Override
                        public void onCancel() {

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

其中sendVote方法内部的decideLeader方法中会比较选票个数是否大于集群节点个数的一半,进而决定当前节点是否当选leader

    public RaftPeer decideLeader(RaftPeer candidate) {
    
		。。。。。。//省略代码
		
        //如果选票大于集群半数,设置State状态为leader
        // majorityCount() = peers.size() / 2 + 1
        if (maxApproveCount >= majorityCount()) {
            RaftPeer peer = peers.get(maxApprovePeer);
            //设置State状态为leader
            peer.state = RaftPeer.State.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;
    }


4.3 发送心跳

        nacos的发送心跳也是由上文的定时线程池去触发的,每隔5秒发一次心跳,进入HeartBeatrun方法中

   public class HeartBeat implements Runnable {

        @Override
        public void run() {
            try {
                if (stopWork) {
                    return;
                }
                if (!peers.isReady()) {
                    return;
                }

                RaftPeer local = peers.local();
                local.heartbeatDueMs -= GlobalExecutor.TICK_PERIOD_MS;
                if (local.heartbeatDueMs > 0) {
                    return;
                }
                //心跳间隔5s
                local.resetHeartbeatDue();
                //发送心跳
                sendBeat();
            } catch (Exception e) {
                Loggers.RAFT.warn("[RAFT] error while sending beat {}", e);
            }
        }

发送心跳的主要逻辑就是sendBeat()方法,主要内容有

  • 检查当前节点是否是Leader,只有Leader才能发心跳!
  • 遍历所有服务,把所有服务的key组装起来,并压缩一下,然后发送给出自己以外所有节点
       private void sendBeat() throws IOException, InterruptedException {
            RaftPeer local = peers.local();
            //raft协议规定:如果不是leader,不能发心跳
            if (EnvUtil.getStandaloneMode() || local.state != RaftPeer.State.LEADER) {
                return;
            }
            if (Loggers.RAFT.isDebugEnabled()) {
                Loggers.RAFT.debug("[RAFT] send beat with {} keys.", datums.size());
            }

            local.resetLeaderDue();

            // build data 构建数据包packet
            ObjectNode packet = JacksonUtils.createEmptyJsonNode();
            packet.replace("peer", JacksonUtils.transferToJsonNode(local));
            //数据数组,这个数组会放在数据包packet中
            ArrayNode array = JacksonUtils.createEmptyArrayNode();

            if (switchDomain.isSendBeatOnly()) {
                Loggers.RAFT.info("[SEND-BEAT-ONLY] {}", switchDomain.isSendBeatOnly());
            }

            if (!switchDomain.isSendBeatOnly()) {
                //遍历所有的之前已经加载到内存中的服务
                //注意:一开始会把磁盘的服务加载到内存中去
                for (Datum datum : datums.values()) {
                    //要发送的数据元素
                    ObjectNode element = JacksonUtils.createEmptyJsonNode();

                    if (KeyBuilder.matchServiceMetaKey(datum.key)) {
                        //只获取服务的key,不发送整个服务,目的是为了更轻量
                        element.put("key", KeyBuilder.briefServiceMetaKey(datum.key));
                    } else if (KeyBuilder.matchInstanceListKey(datum.key)) {
                        element.put("key", KeyBuilder.briefInstanceListkey(datum.key));
                    }
                    //添加时间戳
                    element.put("timestamp", datum.timestamp.get());
                    //把每一个服务的key放入数据数组中
                    array.add(element);
                }
            }

            //把数据数组放进数据包packet中
            packet.replace("datums", array);
            // broadcast
            Map<String, String> params = new HashMap<String, String>(1);
            params.put("beat", JacksonUtils.toJson(packet));

            String content = JacksonUtils.toJson(params);

            //发送数据的输出流
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            //压缩输出流
            GZIPOutputStream gzip = new GZIPOutputStream(out);
            gzip.write(content.getBytes(StandardCharsets.UTF_8));
            gzip.close();

            byte[] compressedBytes = out.toByteArray();
            String compressedContent = new String(compressedBytes, StandardCharsets.UTF_8);

            if (Loggers.RAFT.isDebugEnabled()) {
                Loggers.RAFT.debug("raw beat data size: {}, size of compressed data: {}", content.length(),
                        compressedContent.length());
            }
            //遍历除了自己以外的节点,发送心跳,请求接口 /raft/beat
            for (final String server : peers.allServersWithoutMySelf()) {
                try {
                    final String url = buildUrl(server, API_BEAT);
                    if (Loggers.RAFT.isDebugEnabled()) {
                        Loggers.RAFT.debug("send beat to server " + server);
                    }
                    //异步发送
                    HttpClient.asyncHttpPostLarge(url, null, compressedBytes, new Callback<String>() {
                        @Override
                        public void onReceive(RestResult<String> result) {
                            if (!result.ok()) {
                                Loggers.RAFT.error("NACOS-RAFT beat failed: {}, peer: {}", result.getCode(), server);
                                MetricsMonitor.getLeaderSendBeatFailedException().increment();
                                return;
                            }

                            peers.update(JacksonUtils.toObj(result.getData(), RaftPeer.class));
                            if (Loggers.RAFT.isDebugEnabled()) {
                                Loggers.RAFT.debug("receive beat response from: {}", url);
                            }
                        }

		。。。。。。。 //省略代码!

Leader发送心跳会请求nacos服务端的/raft/beat接口, 其他节点在接收到心跳后会做什么呢,让我们看一下/raft/beat接口的逻辑,

    //接受心跳接口
    @PostMapping("/beat")
    public JsonNode beat(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (versionJudgement.allMemberIsNewVersion()) {
            throw new IllegalStateException("old raft protocol already stop");
        }
        //解码、解压缩
        String entity = new String(IoUtils.tryDecompress(request.getInputStream()), StandardCharsets.UTF_8);
        String value = URLDecoder.decode(entity, "UTF-8");
        value = URLDecoder.decode(value, "UTF-8");

        JsonNode json = JacksonUtils.toObj(value);

        //接收心跳
        RaftPeer peer = raftCore.receivedBeat(JacksonUtils.toObj(json.get("beat").asText()));

        return JacksonUtils.transferToJsonNode(peer);
    }

receivedBeat是真正的接受心跳方法,具体接受心跳逻辑如下:

  • 先把心跳内容解压缩出来,然后接受心跳
  • 判断发送者是否是Leader,以及接受者是否是Flower,如不是,抛异常
  • 遍历leader发过来的服务的key信息,并做批量处理
  • 因为发心跳时为了轻量,只发了服务的key信息,Flower还需要根据服务的key信息请求Leader节点的ip,并根据Key信息拉取完整的服务数据,保存在Flower本地!

具体源码如下:

   public RaftPeer receivedBeat(JsonNode beat) throws Exception {
        if (stopWork) {
            throw new IllegalStateException("old raft protocol already stop work");
        }
        //拿到心跳信息
        final RaftPeer local = peers.local();
        final RaftPeer remote = new RaftPeer();
        JsonNode peer = beat.get("peer");
        remote.ip = peer.get("ip").asText();
        remote.state = RaftPeer.State.valueOf(peer.get("state").asText());
        remote.term.set(peer.get("term").asLong());
        remote.heartbeatDueMs = peer.get("heartbeatDueMs").asLong();
        remote.leaderDueMs = peer.get("leaderDueMs").asLong();
        remote.voteFor = peer.get("voteFor").asText();

        //如果发送者不是leader,抛异常
        if (remote.state != RaftPeer.State.LEADER) {
            Loggers.RAFT.info("[RAFT] invalid state from master, state: {}, remote peer: {}", remote.state,
                    JacksonUtils.toJson(remote));
            throw new IllegalArgumentException("invalid state from master, state: " + remote.state);
        }
        //如果当前节点票数大于发送者的票数,抛异常
        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(), JacksonUtils.toJson(remote), local.leaderDueMs);
            throw new IllegalArgumentException(
                    "out of date beat, beat-from-term: " + remote.term.get() + ", beat-to-term: " + local.term.get());
        }
        //如果当前节点不是FOLLOWER节点,直接把自己变成FOLLOWER节点,因为FOLLOWER节点才能接收心跳
        if (local.state != RaftPeer.State.FOLLOWER) {

            Loggers.RAFT.info("[RAFT] make remote as leader, remote peer: {}", JacksonUtils.toJson(remote));
            // mk follower
            //设置自己为FOLLOWER
            local.state = RaftPeer.State.FOLLOWER;
            local.voteFor = remote.ip;
        }

        final JsonNode beatDatums = beat.get("datums");
        local.resetLeaderDue();
        local.resetHeartbeatDue();

        peers.makeLeader(remote);

        if (!switchDomain.isSendBeatOnly()) {

            //创建一个map,用于接受心跳中带来的服务的key信息
            Map<String, Integer> receivedKeysMap = new HashMap<>(datums.size());

            for (Map.Entry<String, Datum> entry : datums.entrySet()) {
                receivedKeysMap.put(entry.getKey(), 0);
            }

            // now check datums
            List<String> batch = new ArrayList<>();

            int processedCount = 0;
            if (Loggers.RAFT.isDebugEnabled()) {
                Loggers.RAFT
                        .debug("[RAFT] received beat with {} keys, RaftCore.datums' size is {}, remote server: {}, term: {}, local term: {}",
                                beatDatums.size(), datums.size(), remote.ip, remote.term, local.term);
            }
            //flower节点获取服务步骤如下:
            //遍历leader心跳发过来的数据
            for (Object object : beatDatums) {
                processedCount = processedCount + 1;

                JsonNode entry = (JsonNode) object;
                //发过来的心跳包括:服务的key信息,
                // 为什么心跳会发送服务的key呢?,因为key足够小,并且经过了压缩,理论上一次心跳可以发送几万个服务的key信息
                String key = entry.get("key").asText();
                final String datumKey;

                if (KeyBuilder.matchServiceMetaKey(key)) {
                    datumKey = KeyBuilder.detailServiceMetaKey(key);
                } else if (KeyBuilder.matchInstanceListKey(key)) {
                    datumKey = KeyBuilder.detailInstanceListkey(key);
                } else {
                    // ignore corrupted key:
                    continue;
                }

                long timestamp = entry.get("timestamp").asLong();

                receivedKeysMap.put(datumKey, 1);

                try {

                    if (datums.containsKey(datumKey) && datums.get(datumKey).timestamp.get() >= timestamp
                            && processedCount < beatDatums.size()) {
                        continue;
                    }
                    //批量处理服务的key信息,先把key分批
                    if (!(datums.containsKey(datumKey) && datums.get(datumKey).timestamp.get() >= timestamp)) {
                        batch.add(datumKey);
                    }
                    //一批key组成的数组可以有50个key,如果不够继续添加,够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());

                    // 找到leader的url,准备根据服务的key拉取服务的完整信息。
                    // 因为此时Flower节点只是通过心跳拿到了服务的key而已,并没有服务的完整信息,所以要回调leader
                    String url = buildUrl(remote.ip, API_GET);
                    Map<String, String> queryParam = new HashMap<>(1);
                    queryParam.put("keys", URLEncoder.encode(keys, "UTF-8"));

                    //最终flower节点会根据key信息 ,自己去leader的ip里拉取对应的服务实例,并保存在磁盘中
                    HttpClient.asyncHttpGet(url, null, queryParam, new Callback<String>() {
                        @Override
                        public void onReceive(RestResult<String> result) {
                            if (!result.ok()) {
                                return;
                            }
                            
			。。。。。。。 //省略代码


5. 集群、分布式与微服务的区别

  • 集群:同一个业务,部署在多个服务器上
  • 分布式:分为 分布式部署 和 分布式存储
    • 分布式部署:一个业务拆分成多个子业务,每个业务分别存储在不同的服务器上
    • 分布式存储:存储一台机器上的数据被拆分成多份,存储在不同的服务器上
  • 微服务:微服务就是一种分布式部署架构!
  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值