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可以很好的解决不同场景的业务需求。
协议介绍
1、阿里自研发
2、保证cp,保证最终一致性。
1、国外论文
2、实现简单,使用方便,jRaft包支持。
3、能够保证强一致性
4、nacos采用jRaft包实现raft强一致性协议
nacos使用了raft优秀的生产级别的jraft包实现Raft协议。
com.alipay.sofa
jraft-core
1.3.8
compile
bolt
com.alipay.sofa
log4j-api
org.apache.logging.log4j
log4j-core
org.apache.logging.log4j
log4j-slf4j-impl
org.apache.logging.log4j
log4j-jcl
org.apache.logging.log4j
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有几个特别重要的概念
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