nacos原理三-注册中心原理&源码启动.md
1. nacos 服务端源码启动
资源信息:
操作系统:mac
JDK: 8
nacos: 1.1.4 (2.2.1 版本需要protobuf, 插件比较麻烦就放弃了)
- 下载项目
选择 1.1.4 版本
- mvn 编译所有项目
- 创建数据库:nacos/distribution/conf/nacos-mysql.sql
- 修改数据库连接信息:console/src/main/resources/application.properties
# mysql datasource
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos
db.user=root
db.password=
- 修改主类设置单机启动:
console/src/main/java/com/alibaba/nacos/Nacos.java
System.setProperty("nacos.standalone", "true");
2. 关于nacos 简单理解
1. 数据存储
nacos 简单分为注册中心、配置中心。
配置中心相关数据会落库(内置derby,支持切换为mysql, 策略模式切换), 注册中心的数据不会落库。
2. 注册中心模块集群间数据同步
内存存储,当前内存增加之后,同步给集群中的其他兄弟节点。
// 大概两种实现:distro(异步发送同步)、raft(同步发送,需要半数节点进行ACK)
- 平等节点:一个名为Distro的一致性协议算法, 当前注册完之后同步给其他兄弟节点
com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl#put
数据分区:
Distro算法将数据分割成多个块。
每个Nacos服务器节点负责存储和管理一个特定的数据块。
责任分配:
每个数据块的生成、删除和同步操作都由负责该块的服务器执行。
这意味着每个Nacos服务器仅处理总服务数据子集的写操作,降低了单个节点的压力。
数据同步:
所有的Nacos服务器同时接收其他服务器的数据同步。
通过这种方式,随着时间的推移,每个服务器都将最终拥有完整的数据集。
这确保了即使在分布式环境中,数据的一致性和可用性。
这种设计有助于提高系统的可扩展性和容错性,因为任何单个节点的故障都不会导致整个数据集的丢失,而且可以并行处理数据操作以提高性能。
- 叫做Raft 协议的一致性: 从集群中选一个leader 节点,节点就分为主节点和follower节点
com.alibaba.nacos.naming.consistency.persistent.raft.RaftConsistencyServiceImpl#put
在Raft算法中,只有Leader才拥有数据处理和信息分发的权利。因此当服务启动时,如果注册中心指定为Follower节点,则步骤如下:
1、Follower 会自动将注册心跳包转给 Leader 节点;
2、Leader 节点完成实质的注册登记工作;
3、完成注册后向其他 Follower 节点发起“同步注册日志”的指令;
4、所有可用的 Follower 在收到指令后进行“ack应答”,通知 Leader 消息已收到;
5、当 Leader 接收过半数 Follower 节点的 “ack 应答”后,返回给微服务“注册成功”的响应信息。
此外,如果某个Follower节点无ack反馈,Leader也会不断重复发送,直到所有Follower节点的状态与Leader同步为止。
3. 鉴权
使用过滤器进行前置校验,包括如果UA包含Nacos-Server 直接放行,都是 com.alibaba.nacos.core.auth.AuthFilter#doFilter 处理的。
鉴权方式: 简单的基于登录状态+权限的校验
转交给鉴权模块com.alibaba.nacos.plugin.auth.impl.NacosAuthPluginService
- 校验账号密码合法性:判断传的账号密码是否合法, jwt 生成token, 校验token 合法性
- 校验权限: 判断用户是否有对应的权限码
- 如果是python、sdk,请求的参数会携带账号密码。 如果是java sdk 会调用自己的登录接口获取到token,之后每次访问请求参数携带token。
3. Nacos-sdk-python服务注册相关API
服务注册相关的API 主要涉及三个,服务注册、心跳、获取服务。
如果自己实现注册中心也是重点实现这三个接口即可。 对实例的负载、缓存等是在SDK 做的。(如果需要鉴权,还需要实现一个登录界面,登录之后生成token 返回给sdk, sdk 每次请求会在参数携带token)
- 注册
uri: http://localhost:8848/nacos/v1/ns/instance?username=XXX&password=lbg-nacosxxx'
method: POST
data:b'ip=127.0.0.1&port=8848&serviceName=qlq_cus&weight=1.0&enable=True&healthy=True&clusterName=None&ephemeral=True&groupName=DEFAULT_GROUP&metadata=%7B%22token%22%3A+%2220e05776c4b810ac9557c7ce081dfe32%22%7D'
对应java 接口:
com.alibaba.nacos.naming.controllers.InstanceController#register
- 心跳:
uri: http://127.0.0.1:8848/nacos/v1/ns/instance/beat?serviceName=qlq_cus&beat=%7B%22serviceName%22%3A+%22qlq_cus%22%2C+%22ip%22%3A+%22127.0.0.1%22%2C+%22port%22%3A+%228848%22%2C+%22weight%22%3A+1.0%2C+%22ephemeral%22%3A+true%7D&groupName=DEFAULT_GROUP&username=XXX&password=lbg-nacosxxx
解码后:
http://127.0.0.1:8848/nacos/v1/ns/instance/beat?serviceName=qlq_cus&beat={"serviceName": "qlq_cus", "ip": "127.0.0.1", "port": "8848", "weight": 1.0, "ephemeral": true}&groupName=DEFAULT_GROUP&username=XXX&password=lbg-nacosxxx
method: PUT
对应java 接口:
com.alibaba.nacos.naming.controllers.InstanceController#beat
- 获取服务:
uri: 'http://localhost:8848/nacos/v1/ns/instance/list?serviceName=qlq_cus&healthyOnly=False&namespaceId=public&groupName=DEFAULT_GROUP'
method: GET
对应java 接口:
com.alibaba.nacos.naming.controllers.InstanceController#list
4. nacos 数据同步原理-CP|AP
nacos 有AP\CP, 默认是AP,也就是默认走distro 协议,可以通过配置设置为走Raft 协议来满足CP 原则。
1. distro 协议 -AP (默认)
核心参考: com.alibaba.nacos.core.distributed.distro.DistroProtocol
自研的一致性协议,主要用于在集群节点之间实现数据的快速同步和一致性保证。 思想是实现AP,节点分数据处理,也就是一个节点处理部分数据。 处理完之后通过异步任务,同步到其他节点,趋向于最终一致性。
思路:读请求所有节点都可以处理;写请求是根据service_name 或者 实例信息(ip:port) 进行分片,分到对应的节点进行处理,节点处理完异步通知其他节点进行同步。
1. com.alibaba.nacos.naming.web.DistroFilter#doFilter
(1). 判断是否能处理响应: 根据服务名称或者ip加端口获取服务下标
int index = distroHash(responsibleTag) % servers.size();
return servers.get(index);
private int distroHash(String responsibleTag) {
return Math.abs(responsibleTag.hashCode() % Integer.MAX_VALUE);
}
(2). 如果是当前服务自己,放行到后面的处理过程
(3). 否则拿到请求参数和请求头,http 调用到指定的机器进行处理
2. com.alibaba.nacos.naming.core.InstanceOperatorServiceImpl#registerInstance 处理注册逻辑
(1). 内部处理相关的注册逻辑
(2). 异步调用其他兄弟节点进行同步数据:com.alibaba.nacos.core.distributed.distro.DistroProtocol#sync
public void sync(DistroKey distroKey, DataOperation action, long delay) {
for (Member each : memberManager.allMembersWithoutSelf()) {
syncToTarget(distroKey, action, each.getAddress(), delay);
}
}
大致过程:
- 初始化: 节点启动时初始化distro 相关组件,包含数据同步处理器以及服务监听器。
- 全量同步: 新加入集群的节点会请求全量数据,其他节点发送全量数据; 新节点收到全量数据后,进行校验和处理
- 增量同步:全量同步完成后,后续的同步是基于增量同步,只同步新产生的数据。 每个节点定期向其他节点发送增量请求,同步到本地进行处理
- 心跳与确认:节点之前定时发送心跳,以检测对方的存活状态
2. Raft 协议 - CP
核心参考: com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore
思想: 有一个主节点,所有的写请求打到主节点。主节点然后同步阻塞将数据同步给其他节点,如果半数以上成功,就代表写入成功; 否则写入失败会抛出非法状态异常,可能就需要排查nacos 集群是否正常。
核心思想:
- 领导选举:
(1). Raft 系统中的节点在任何时候都处于三种状态之一:领导者(Leader)、跟随者(Follower)或候选者(Candidate)。
(2). 当系统启动或领导者失效时,节点会转换为候选者状态并发起选举,通过请求投票(RequestVote) RPC 向其他节点寻求支持。
(3). 基于任期(Term)的概念,每个选举都有一个独一无二的任期号,确保过时的投票无效,防止选票分裂。
(4). 获得大多数节点支持的候选者将成为新的领导者,负责处理客户端请求和管理日志复制
简单理解:
- 每个节点有一个随机等等时间,当集群暂未选出主节点或者没收到主节点心跳,且自己的随机等待时间到期之后,把自己的角色设为CANDIDATE、然后给自己投一票,然后向兄弟节点收集投票结果,假设B先到期 (com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore.MasterElection#sendVote);
- 兄弟节点A收到后和自己的票数term 比较,如果小于A当前的票数,那A投给A自己; 否则,A把自己的角色改为FOLLOWER 从节点,然后把自己的票数terms 设为和B节点的一样、重置自己的等待时间(com.alibaba.nacos.naming.controllers.RaftController#vote)
- B每次收集完结果之后,判断票数最多且超过半数,那么就可以设为leader 主节点, 这是B已经知道自己是主节点
- 主节点每次心跳会将自己的信息同步给兄弟节点(只有主节点会发心跳给其他节点,其他节点记录主节点等信息-com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore.HeartBeat#sendBeat)。
- 数据同步
(1). 所有的写请求会到达leader 节点
(2). leader 同步将数据发给其他兄弟节点,超过半数回复正常才会认为写入成功; 否则会抛出非法状态异常,这时候可能需要排查nacos 集群状态
参考: com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore#signalPublish
public void signalPublish(String key, Record value) throws Exception {
if (stopWork) {
throw new IllegalStateException("old raft protocol already stop work");
}
// 不是主节点,就转发到主节点
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);
final RaftPeer leader = getLeader();
raftProxy.proxyPostLarge(leader.ip, API_PUB, params.toString(), parameters);
return;
}
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()));
onPublish(datum, peers.local());
final String content = json.toString();
// 使用闭锁,等待一般以上节点回复成功
final CountDownLatch latch = new CountDownLatch(peers.majorityCount());
for (final String server : peers.allServersIncludeMyself()) {
if (isLeader(server)) {
latch.countDown();
continue;
}
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;
}
latch.countDown();
}
@Override
public void onError(Throwable throwable) {
Loggers.RAFT.error("[RAFT] failed to publish data to peer", throwable);
}
@Override
public void onCancel() {
}
});
}
// 超时就抛出非法状态异常
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();
}
}
com.alibaba.nacos.naming.consistency.persistent.raft.RaftPeerSet#majorityCount
public int majorityCount() {
return peers.size() / 2 + 1;
}
5. 实现自己的注册中心需要实现的接口
1. 关于存储
1、内存存储:可以选择数据存储到内存中,节点数据存储到内存,数据同步可以参考nacos 的distro 协议。
2、持久化:存储到redis,比较简单。 可以选择redis 的hash 结构,key是服务名称、hash内部的key 是instanceId 实例ID,value 是实例信息(包括上次心跳时间等)。
2. 关于接口
参考nacos,如果实现一个自己的注册中心核心的接口有三个:
1、服务注册:生成服务信息和实例信息,存储到db
2、心跳: 续期
3、获取服务: 根据服务获取所有实例信息
如果有权限相关,还需要一个登录接口
参考:
nacos github: https://github.com/alibaba/nacos
nacos-sdk-python: https://github.com/nacos-group/nacos-sdk-python