nacos-一致性协议(1)

Nacos支持CP+AP模式,即Nacos可以根据配置识别为CP模式或AP模式,默认是AP模式。如果注册Nacos的client节点注册时ephemeral=true,那么Nacos集群对这个client节点的效果就是AP,采用distro协议实现;而注册Nacos的client节点注册时ephemeral=false,那么Nacos集群对这个节点的效果就是CP的,采用raft协议实现。根据client注册时的属性,AP,CP同时混合存在,只是对不同的client节点效果不同。Nacos可以很好的解决不同场景的业务需求。

协议介绍

distro协议

1、阿里自研发

2、保证cp,保证最终一致性。

  • Nacos 每个节点是平等的都可以处理写请求,同时把新数据同步到其他节点。
  • 每个节点只负责部分数据,定时发送自己负责数据的校验值到其他节点来保持数据一致性。
  • 每个节点独立处理读请求,及时从本地发出响应。

Raft协议

1、国外论文

2、实现简单,使用方便,jRaft包支持。

3、能够保证强一致性

4、nacos采用jRaft包实现raft强一致性协议

nacos使用了raft优秀的生产级别的jraft包实现Raft协议。

  <dependency>
      <groupId>com.alipay.sofa</groupId>
      <artifactId>jraft-core</artifactId>
      <version>1.3.8</version>
      <scope>compile</scope>
      <exclusions>
        <exclusion>
          <artifactId>bolt</artifactId>
          <groupId>com.alipay.sofa</groupId>
        </exclusion>
        <exclusion>
          <artifactId>log4j-api</artifactId>
          <groupId>org.apache.logging.log4j</groupId>
        </exclusion>
        <exclusion>
          <artifactId>log4j-core</artifactId>
          <groupId>org.apache.logging.log4j</groupId>
        </exclusion>
        <exclusion>
          <artifactId>log4j-slf4j-impl</artifactId>
          <groupId>org.apache.logging.log4j</groupId>
        </exclusion>
        <exclusion>
          <artifactId>log4j-jcl</artifactId>
          <groupId>org.apache.logging.log4j</groupId>
        </exclusion>
      </exclusions>
    </dependency>

nacos强一致性协议初始化流程

一、通过@Configuration想spring注入一个协议bean:JRaftProtocol

@Bean(value = "strongAgreementProtocol")
    public CPProtocol strongAgreementProtocol(ServerMemberManager memberManager) throws Exception {
        final CPProtocol protocol = getProtocol(CPProtocol.class, () -> new JRaftProtocol(memberManager));
        return protocol;
    }

1、ServerMemberManager bean流程

1.1、感知本机服务的地址和端口号,将其维护到ServerMemberManager#serverList当中。

1.2、向通知中心注册事件MembersChangeEvent。由于MembersChangeEvent是Event的子类,所以他被注册到cNotifyCenter#publisherMap中。该事件的处理器类是ClusterRpcClientProxy,其在spring初始化调用@PostConstruct注解的时候将其注入到DefaultPublisher#subscribers列表当中。当调用registerToPublisher的时候会在subscribers列表当中找到执行该时间的监听器并返回。

1.3、初始化MemberLookup实例,并且读取本实例的conf文件,并写入本次集群节点信息到conf文件当中,并且启动job(WatchDirJob)监听该节点配置文件变化。

2、JRaftProtocol实例化

 public JRaftProtocol(ServerMemberManager memberManager) throws Exception {
        this.memberManager = memberManager;
        this.raftServer = new JRaftServer();
        this.jRaftMaintainService = new JRaftMaintainService(raftServer);
    }

JRaftProtocol实例化就是将memberManager注入进来,并实例化两个类JRaftServer和JRaftMaintainService。

二、JRaftProtocol初始化

1、spring注入PersistentConsistencyServiceDelegateImpl

@DependsOn("ProtocolManager")
@Component("persistentConsistencyServiceDelegate")
public class PersistentConsistencyServiceDelegateImpl implements PersistentConsistencyService {
    
    private final BasePersistentServiceProcessor persistentServiceProcessor;
    
    public PersistentConsistencyServiceDelegateImpl(ProtocolManager protocolManager) throws Exception {
        this.persistentServiceProcessor = createPersistentServiceProcessor(protocolManager);
    }

2、根据当前是单例还是集群,初始化BasePersistentServiceProcessor

   private BasePersistentServiceProcessor createPersistentServiceProcessor(ProtocolManager protocolManager)
            throws Exception {
        final BasePersistentServiceProcessor processor =
                EnvUtil.getStandaloneMode() ? new StandalonePersistentServiceProcessor()
                        : new PersistentServiceProcessor(protocolManager);
        processor.afterConstruct();
        return processor;
    }

3、PersistentServiceProcessor初始化会调用protocolManager的getCpProtocol方法

  public PersistentServiceProcessor(ProtocolManager protocolManager) throws Exception {
        this.protocol = protocolManager.getCpProtocol();
    }

4、protocolManager的getCpProtocol调用protocol.init方法

 private void initCPProtocol() {
        ApplicationUtils.getBeanIfExist(CPProtocol.class, protocol -> {
            Class configType = ClassUtils.resolveGenericType(protocol.getClass());
            Config config = (Config) ApplicationUtils.getBean(configType);
            injectMembers4CP(config);
            protocol.init(config);
            ProtocolManager.this.cpProtocol = protocol;
        });
    }

5、protocol.init方法流程

JRaft集成

jraft有几个特别重要的概念

1、地址 Endpoint

2、节点 PeerId,PeerId 表示一个 raft 协议的参与者

3、配置 Configuration,Configuration 表示一个 raft group 的配置,也就是参与者列表:

在JRaftServer初始化的时候会初始化以下信息:

1、raft协议的初始化配置

①、获取raft线程池核心线程

②、获取clientService核心线程数,

③、实例化核心线程池 ,名字开头为:com.alibaba.nacos.core.raft-core

④、实例化clientService线程池,名字开头为:com.alibaba.nacos.core.raft-cli-service

⑤、初始化普通线程池,线程数为8 名字开头为com.alibaba.nacos.core.protocol.raft-common

⑥、初始化快照线程池,名字开头为com.alibaba.nacos.core.raft-snapshot

2、将该节点地址信息包装成PeerId

localPeerId = PeerId.parsePeer(self);

3、设置节点参数nodeOptions

4、初始化类CliService

5、初始化类cliClientService

在JRaftServer启动的时候首先启动了rpcServer.rpcServer有两种方式,一种是GRpc,一种是Bolt rpc.

nacos使用的是Grpc的方式。

启动了rpc,接下来就是基于jraft做以下步骤

1、本地创建三个目录,并将三个目录设置到NodeOptions对象中。

2、初始化nacos的raft状态机NacosStateMachine。NacosStateMachine可以理解为raft协议在感知集群节点状态变化之后通知该节点。例如leader节点下线了,作为following节点加入raft协议。

所以这个状态机是实现jraft的核心。

3、启动组服务RaftGroupService。RaftGroupService主要是将本地包装节点node加入raft协议。

4、更新组信息到路由表groupConfTable

5、使用上文说的普通线程池(raftCommonExecutor)启动任务registerSelfToCluster。

该任务如下:

1、使用rpcExecutor线程池依次执行组内节点,取组节点的leaderId.如果找到就会跳出。如果找不到就抛异常。然后本节点跟leader节点连接,并发送获取组内节点信息。如果发现自己已经在组内节点信息内就返回,如果不在则调用CliService#addPeer,连接leader节点,并发送addPeer请求给leader节点。leader节点接到该请求会将节点加入到BaseCliRequestProcessor.CliRequestContext#node当中。客户端会根据leaderj节点返回的结果信息打印当前参与raft节点信息和节点变化信息。

registerSelfToCluster任务是1秒钟执行一次。

接下来我们通过几个问题来大概了解下nacos使用jraft实现raft的实现机理。

问题一、结点启动的时候是如何参与选举的?

PersistentConsistencyServiceDelegateImpl

spring注入该类的时候,调用了PersistentServiceProcessor的afterConstruct方法。

afterConstruct方法调用了JRaftProtocol的addRequestProcessors方法。

addRequestProcessors方法将PersistentServiceProcessor实例注入到了JRaftServer实例的processors字段中,并且调用了初始化raft节点,Node node = raftGroupService.start(false);

初始化raft节点的时候做了两件事儿

1、初始化节点

 final Node ret = createRaftNode(groupId, serverId);

2、调用节点的init方法。

ret.init(opts)

调用节点的init方法的时候做了很多事情,其中一个就是初始化了一个选举任务electionTimer。

 name = "JRaft-ElectionTimer-" + suffix;
        this.electionTimer = new RepeatedTimer(name, this.options.getElectionTimeoutMs(),
            TIMER_FACTORY.getElectionTimer(this.options.isSharedElectionTimer(), name)) {

            @Override
            protected void onTrigger() {
                handleElectionTimeout();
            }

            @Override
            protected int adjustTimeout(final int timeoutMs) {
                return randomTimeout(timeoutMs);
            }
        };

有了这个任务,在init快结束的时候发起了electSelf();操作。

问题二、结点下线的时候其它节点是如何感知的?

ProtocolManager实现了接口DisposableBean。当我们的服务由于某种原因下线会调用ProtocolManager的destroy方法。destroy方法会调用jraft协议的shutdown方法

 @PreDestroy
    @Override
    public void destroy() {
        if (Objects.nonNull(apProtocol)) {
            apProtocol.shutdown();
        }
        if (Objects.nonNull(cpProtocol)) {
            cpProtocol.shutdown();
        }
    }

备注kill -2 pid 能看到这个过程。

1、落数据到本地nacos-9100/data/protocol/raft/naming_persistent_service/meta-data。

问题三、节点很多的情况下,leader下线了如何选举?

leader节点正常要给每一个从节点发送心跳,如果心跳超时会重新发起选举

问题四、节点间心跳在哪里


 for (final PeerId peer : this.conf.listPeers()) {
            if (peer.equals(this.serverId)) {
                continue;
            }
            LOG.debug("Node {} add a replicator, term={}, peer={}.", getNodeId(), this.currTerm, peer);
            if (!this.replicatorGroup.addReplicator(peer)) {
                LOG.error("Fail to add a replicator, peer={}.", peer);
            }
        }

这段代码在electSelf()中,也就是选举自己作为leader的过程中,如果发现除了自己还有其他节点在集群当中会调用addReplicator将该节点以Follower的形式加入。

final ThreadId rid = Replicator.start(opts, this.raftOptions);

启动Replicator要启动一个复制线程并且设置了一个超时任务

 r.id = new ThreadId(r, r);
        r.id.lock();
        notifyReplicatorStatusListener(r, ReplicatorEvent.CREATED);
        LOG.info("Replicator={}@{} is started", r.id, r.options.getPeerId());
        r.catchUpClosure = null;
        r.lastRpcSendTimestamp = Utils.monotonicMs();
        r.startHeartbeatTimer(Utils.nowMs());

心跳延时任务使用的是延迟队列实现。心跳的发送

private void startHeartbeatTimer(final long startMs) {
        final long dueTime = startMs + this.options.getDynamicHeartBeatTimeoutMs();
        try {
            this.heartbeatTimer = this.timerManager.schedule(() -> onTimeout(this.id), dueTime - Utils.nowMs(),
                TimeUnit.MILLISECONDS);
        } catch (final Exception e) {
            LOG.error("Fail to schedule heartbeat timer", e);
            onTimeout(this.id);
        }
    }

触发探活的入口在读Leader这个方法NodeImpl#readLeader。

当心跳任务执行的时候,会根据远程结果动态再次触发下一次心跳任务。

问题五、选举超时

选举一旦超时就执行以下任务

this.voteTimer = new RepeatedTimer(name, this.options.getElectionTimeoutMs(), TIMER_FACTORY.getVoteTimer(
            this.options.isSharedVoteTimer(), name)) {

            @Override
            protected void onTrigger() {
                handleVoteTimeout();
            }

            @Override
            protected int adjustTimeout(final int timeoutMs) {
                return randomTimeout(timeoutMs);
            }
        };

问题六、节点之间数据同步是如何做的?

通过jRaft的Replicator来做。

小结

本文通过对nacos一致性协议之一的raft做一个简单的源码介绍,带大家粗略查看了下nacos是如何集成jraft通过raft协议实现CP模式的。集成jraft的关键类是JRaftServer和JRaftProtocol,通过仔细研读这两个类一定能体会一些集成jraft的方法,至于底层jraft实现大家可以先参考文档JRaft 用户指南 · SOFAStack,能够领略raft各个模块的隔离性在代码实现简洁性方面的巨大作用。

参考文献

Nacos 2.0原理解析(一):Distro协议_zyxzcr的博客-CSDN博客_distro协议

SOFAJRaft 源码分析二(日志复制、心跳)_大远哥的博客-CSDN博客_jraft grcp pipeline

 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值