Nacos作为SpringCloud Alibaba (SCA)中注册中心以及配置中心的组件。这里主要分析Nacos客户端与服务端之间注册,下线,心跳续约源码以及对Ribbon负载均衡的Nacos的具体实现。
核心对外提供API接口类:
对于客户端的使用其实很简单,只要引入spring-cloud-starter-alibaba-nacos-discovery包即可。这个包是一个springboot start项目,所以查看它的源码在
上述的每一个AutoConfiguration都有自己需要初始化的对象例如
NacosDiscoveryAutoConfiguration:用于初始化NacosDiscoveryProperties(存储配置属性类),NacosServiceDiscovery(提供对nacos中service以及Instance实例访问具体实现)
RibbonNacosAutoConfiguration:用于初始化Ribbon中核心ServerList类,以及自己实现NacosServerIntrospector,其中Ribbon依赖组件进行初始化(IRule,IPing等)还是在RibbonAutoConfiguration中被创建
NacosDiscoveryEndpointAutoConfiguration:对外提供端点信息(包含Health健康检查),需要依赖于actuator项目
NacosServiceRegistryAutoConfiguration:这里是对核心类一些类进行初始化,例如NacosServiceRegistry(包含创建后续与nacos服务端交互的核心类NamingService-实现NacosNamingService)负责客户端的注册,下线心跳续约检测,NacosAutoServiceRegistration负责触发上述动作
BeatReactor:客户端与服务端周期心跳检测类,内部定义ScheduledThreadPoolExecutor周期调度器,创建名为com.alibaba.nacos.naming.beat.sender线程周期的执行BeatTask任务(该类为BeatReactor中的一个内部类用于向服务端发送心跳信息,最终通过httpclient发送路径为/instance/beat的http请求),内部维护以serviceName+groupName+ip+host为key,BeatInfo为value的map,当client初始化向服务端注册实例时会创建一个BeatInfo对象,通过BeatReactor中addBeatInfo()函数写入,并在beatInfo属性period(默认5s)后调度一次BeatTask。关于心跳检测可以参考后续对心跳机制描述
HostReactor:客户端周期去拉取服务端代码,内部定义ScheduledThreadPoolExecutor周期调度器,创建名为com.alibaba.nacos.client.naming.updater线程周期的执行UpdateTask任务(该类为HostReactor中的一个内部类用于更新client中缓存的服务注册列表信息,在获取列表的同时,告诉服务度它的udp端口号信息,服务端生成对应的PushClient对象,一旦服务端中对应的Service信息发生来变更,服务端可以通过PushClient进行发送变更信息。UpdateTask以Service-cluster组合为单位来周期更新的,更新频率默认1s可设置。通过updateServiceNow()发送http请求-/instance/list)
EventDispatcher:事件分发器 用于管理EventListener。内部定义ScheduledThreadPoolExecutor周期调度器,创建名为com.alibaba.nacos.naming.client.listener线程周期的执行Notifier任务(通过Notifier向注册的EventListener中发生NamingEvent事件,可用于本地扩展(实现ApplicationListener接口监控NamingEvent事件))
PushReceiver:用于接受服务端发送来的ACK数据并进行与本地信息对比更新,最后返回服务端ack信息,该类初始化时创建一个udpSocket,用于与服务端数据通信,定义ScheduledThreadPoolExecutor创建前缀名为com.alibaba.nacos.naming.push.receiver的调度器,用于执行PushReceiver(该类本身实现了Runnable接口)。
//执行run源码
@Override
public void run() {
while (true) {
try {
// byte[] is initialized with 0 full filled by default
//定义
byte[] buffer = new byte[UDP_MSS];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
//接受服务端发送的udp请求
udpSocket.receive(packet);
//解析数据
String json = new String(IoUtils.tryDecompress(packet.getData()), "UTF-8").trim();
NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
//转化PushPacket
PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);
String ack;
//感觉不同数据类型 返回不同ack信息
if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
//处理请求 更新本地本地Service信息
hostReactor.processServiceJSON(pushPacket.data);
// send ack to server
ack = "{\"type\": \"push-ack\""
+ ", \"lastRefTime\":\"" + pushPacket.lastRefTime
+ "\", \"data\":" + "\"\"}";
} else if ("dump".equals(pushPacket.type)) {
// dump data to server
ack = "{\"type\": \"dump-ack\""
+ ", \"lastRefTime\": \"" + pushPacket.lastRefTime
+ "\", \"data\":" + "\""
+ StringUtils.escapeJavaScript(JacksonUtils.toJson(hostReactor.getServiceInfoMap()))
+ "\"}";
} else {
// do nothing send ack only
ack = "{\"type\": \"unknown-ack\""
+ ", \"lastRefTime\":\"" + pushPacket.lastRefTime
+ "\", \"data\":" + "\"\"}";
}
//重新发送给原服务
udpSocket.send(new DatagramPacket(ack.getBytes(Charset.forName("UTF-8")),
ack.getBytes(Charset.forName("UTF-8")).length, packet.getSocketAddress()));
} catch (Exception e) {
NAMING_LOGGER.error("[NA] error while receiving push data", e);
}
}
}
服务端通过InstanceController接受到/instance/list路径请求,做了什么事情呢?接受client请求数据,获取存在的Service信息(可能存在访问的服务器上还未同步到Service信息未null,若出现直接返回),如果携带中参数包含udp port并且服务端开启了push机制(默认开启但是可以通过配置关闭),通过PushService中addClient()函数创建该请求与Service连接对象(PushClient信息,),并存储在key为namespaceId, serviceName构建,value为PushClient的缓存中(后续通过心跳保持该PuchClient连接),用于后续通知。组装最新Service信息返回(中间需要对Service进行校验)。
数据模型有关
Instance:nacos数据模型中最小存储单位,主要由ip+port来确定唯一性,一个client对应一个instance
Cluster:集群由相同的配置的Instance构成,内部存储了该集群下persistentInstances与ephemeralInstances。Cluster实例完成之后通过HealthCheckTask来检测所属Instance活性
Service:代表一个服务实例,由多个Cluster构,实现了RecordListener
namespace:命名空间,可以使用它来管理一个注册中心管理多个环境(开发,测试(不同环境),生产)等
数据一致性有关
Nacos中提供的两种一致性算法的实现:CP的Raft与AP的distro,这两种算法分别针对于临时节点与持久性节点的存储,nacos中默认节点类型为临时节点。
nacos中通过DelegateConsistencyServiceImpl类(静态代理设计模式,包含两种具体策略实现),代理执行具体的数据(按照节点的类型)写入
具体算法实现:
DistroConsistencyServiceImpl:AP模型体现,所有节点都是对等的(nacos这里是参考了Eureka服务的原理),负责管理ephemeral实例信息(数据存储在内存中,由DataStore存储,初始化时会执行一次LoadDataTask任务来同步其它服务上的已存储的ephemeral实例信息,通过定期执行同步任务LoadDataTask,与其它节点做到最终数据统一,一旦实例数据发生变化触发通知机制,不仅通知其它节点也会push发送客户端,也通过TaskDispatcher来添加任务最终与其它节点进行同步) .内部包含一个TaskDispatcher来管理一批TaskScheduler的执行,而TaskScheduler通过BlockingQueue来存储数据变更同步到其它服务节点任务。最终使得集群中临时节点实例信息达到最终一致。
RaftConsistencyServiceImpl:CP模型体现,这里nacos对Raft算法的实现。除了leader写入后,follower过半数统一后再写入,follower也会通过leader心跳定期同步数据。负责保存persistent实例信息(数据存储在本地文件磁盘中,由RaftStore存储,实际上在RaftCore中会存在一份缓存数据用于读,避免频繁读写磁盘,写的动作在写入磁盘时刷新缓存)
数据管理相关:
ServiceStatusSynchronizer:同步器,本质都是通过发送其它节点http请求来获取或发送数据。该同步器就2个方法-send发送(/service/status),get获取(获取目标服务器最新Service信息/instance/statuses)
ServiceManage:nacos服务端用于管理Service信息,实现 RecordListener
PushService:用于向各订阅的客户端(管理所有注册过的PushClient信息)通过udp通信协议发送Service变更信息,内部包含多个ScheduledExecutorService执行器:名为com.alibaba.nacos.naming.push.retransmitter处理两种任务:并开启周期为20s,延时事件为0s的定期清除无效pushClient任务;名为com.alibaba.nacos.naming.push.udpSender:udp通信协议发送Service变更信息。名为com.alibaba.nacos.naming.push.receiver线程类Receiver用于接受接受client的ACK结果。通过监听ServiceChangeEvent事件触发onApplicationEvent函数最终通过udpSender异步进行状态同步。该类与Client中的PushReceiver相对应.
ServiceReporter:来向其它对等服务报告各服务的信息状态,这个属于服务端之间的心跳机制,通过ServiceStatusSynchronizer来发送,初始化延时60s执行一次后续通过周期(可通过serviceStatusSynchronizationPeriodMillis配置默认5s)来,报告的目标是除本身外的其它对等服务器节点。
UpdatedServiceProcessor:该任务用于从其他服务器异步获取更改的服务-每收到一次ServiceReporter任务都会通过ServiceStatusSynchronizer同步器来获取其发送服务节点下该Service下相同的实例,判断是否发生来health变更,若发生变更则将最新状态通知订阅的client
EmptyServiceAutoClean:该任务用于清除失效的Service数据
nacos对客户端提供的API接口:
nacos client拉取Server服务信息由NamingProxy来实现,用它来代理client通过httpclient访问nacos服务。nacos提供了两种方式获取,第一种:通过配置文件中nacos.discovery.server-addr=xxx,xxx,xxx属性来设置,这种方式后续客户端不会再通过定时任务获取来 第二种:通过配置文件nacos.discovery.enpoint设置,服务的域名,通过它可以动态获取服务器地址。增强endpoint的功能。在endpoint端配置网段和环境的映射关系,endpoint在接收到客户端的请求(实际上就是http请求)之后,根据客户端的来源IP所属网段,计算出该客户端所属环境,然后找到对应环境的IP列表返回给客户端。对于endpoint描述可以参考https://nacos.io/zh-cn/blog/address-server.html 官网对于这一块描述, 其中若设置endpoint则server-addr无效。我们说的拉取Nacos集群的任务就是这针对于enpoint。
如下所示:
NacosAutoServiceRegistration该类继承于AbstractAutoServiceRegistration(实现了ApplicationListener接口对WebServerInitializedEvent事件进行监控),当服务初始化完成后,因为NacosAutoServiceRegistration重写register函数先进行系统检测。最终还是通过NacosServiceRegistry(携带当前实例的注册表下线NacosRegistration)进行服务注册。如下所示
注: 由ephemeral决定,该实例类型(默认为临时节点)。
这里为何是随机的原因在于,对数据的分片存储,即任意的Service下的instance随机分片落到集群中(这里该server以service角色对这个instance进行管理,数据信息通过离线同步方式进行集群类同步),这里构建节点的类型有不同的措施,不同的一致性算法有不同的持久化实现,后续代码详细说明。到这里客户端发起注册请求结束。
2.2 服务端:
服务端的对外提供API接口类为InstanceController类
通过InstanceController找到ServiceManager。这里可以参考Nacos的模型https://nacos.io/zh-cn/docs/architecture.html
通用调用createEmptService用于创建一个空service 这时候并不添加instance。这里创建成功后会开启Service下的ClientBeatCheckTask监听任务,监听该Service临时节点(这里临时节点会分片保存)的变更。
这里的监听很重要 一旦Service中Instance数据集发送变化,会触发Service下对应的onChange事件,修改服务列表数据
因为是持久化节点所以这里通过Raft算法来写入:
/**
* 若节点为leader数据写入磁盘 否则转发到leader中由leader写入
* @param key
* @param value
* @throws Exception
*/
public void signalPublish(String key, Record value) throws Exception {
if (!isLeader()) {
//不为leader
ObjectNode params = JacksonUtils.createEmptyJsonNode();
params.put("key", key);
params.replace("value", JacksonUtils.transferToJsonNode(value));
Map parameters = new HashMap<>(1);
parameters.put("key", key);
final RaftPeer leader = getLeader();
//转发到leader节点 由leader节点统一落盘处理 /raft/datum
raftProxy.proxyPostLarge(leader.ip, API_PUB, params.toString(), parameters);
return;
}
try {
//自己是leader
//加锁 保证写入流正常
OPERATE_LOCK.lock();
long start = System.currentTimeMillis();
//封装新的datum
final Datum datum = new Datum();
datum.key = key;
datum.value = value;
//是否已存在旧Datum
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()));
//将datum写入磁盘中
onPublish(datum, peers.local());
final String content = json.toString();
//计算发送给各follower时间
final CountDownLatch latch = new CountDownLatch(peers.majorityCount());
//包含
for (final String server : peers.allServersIncludeMyself()) {
if (isLeader(server)) {
//不发送自己 因为本身已经保存
latch.countDown();
continue;
}
//构建请求url 这里调用/raft/datum/commit 各服务落地数据
final String url = buildURL(server, API_ON_PUB);
//异步发送数据 最终还是调用RaftCore.onPublish()方法
HttpClient.asyncHttpPostLarge(url, Arrays.asList("key=" + key), content, new AsyncCompletionHandler() {
@Override
public Integer onCompleted(Response response) throws Exception {
if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
Loggers.RAFT.warn("[RAFT] failed to publish data to peer, datumId={}, peer={}, http code={}",
datum.key, server, response.getStatusCode());
return 1;
}
latch.countDown();
return 0;
}
@Override
public STATE onContentWriteCompleted() {
return STATE.CONTINUE;
}
});
}
//定义发送各服务器超时时间
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();
}
}
非leader节点会将数据变更请求转发到RaftController(该接口用于leader与follower之间关于Raft的具体实现)对外API中处理
/**
* 处理非leader转发的数据put请求
* @param request
* @param response
* @return
* @throws Exception
*/
@PostMapping("/datum")
public String publish(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setHeader("Content-Type", "application/json; charset=" + getAcceptEncoding(request));
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Content-Encode", "gzip");
String entity = IoUtils.toString(request.getInputStream(), "UTF-8");
String value = URLDecoder.decode(entity, "UTF-8");
JsonNode json = JacksonUtils.toObj(value);
String key = json.get("key").asText();
//根据请求类型 落地不同数据 本质还是调用raftCore.signalPublish
if (KeyBuilder.matchInstanceListKey(key)) {
raftConsistencyService.put(key, JacksonUtils.toObj(json.get("value").toString(), Instances.class));
return "ok";
}
if (KeyBuilder.matchSwitchKey(key)) {
raftConsistencyService.put(key, JacksonUtils.toObj(json.get("value").toString(), SwitchDomain.class));
return "ok";
}
if (KeyBuilder.matchServiceMetaKey(key)) {
raftConsistencyService.put(key, JacksonUtils.toObj(json.get("value").toString(), Service.class));
return "ok";
}
throw new NacosException(NacosException.INVALID_PARAM, "unknown type publish key: " + key);
}
转发leader成功之后就由leader通过调用/raft/datum/commit接口,使各服务节点同步数据。
@PostMapping("/datum/commit")
public String onPublish(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setHeader("Content-Type", "application/json; charset=" + getAcceptEncoding(request));
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Content-Encode", "gzip");
String entity = IoUtils.toString(request.getInputStream(), "UTF-8");
String value = URLDecoder.decode(entity, "UTF-8");
JsonNode jsonObject = JacksonUtils.toObj(value);
String key = "key";
RaftPeer source = JacksonUtils.toObj(jsonObject.get("source").toString(), RaftPeer.class);
JsonNode datumJson = jsonObject.get("datum");
Datum datum = null;
if (KeyBuilder.matchInstanceListKey(datumJson.get(key).asText())) {
datum = JacksonUtils.toObj(jsonObject.get("datum").toString(), new TypeReference>() {
});
} else if (KeyBuilder.matchSwitchKey(datumJson.get(key).asText())) {
datum = JacksonUtils.toObj(jsonObject.get("datum").toString(), new TypeReference>() {
});
} else if (KeyBuilder.matchServiceMetaKey(datumJson.get(key).asText())) {
datum = JacksonUtils.toObj(jsonObject.get("datum").toString(), new TypeReference>() {
});
}
//上面都是处理请求数据 转化为datum
raftConsistencyService.onPut(datum, source);
return "ok";
}
最终核心还是调用Raft中onPublish写入。创建Service成功后会将instace写入到Service中。Instanc根据节点类型决定对应的一致性算法来定。
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips) throws NacosException {
//构建instance对应的key
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
//获取service
Service service = getService(namespaceId, serviceName);
//同步加锁 朝service中添加instace
synchronized (service) {
//获取添加instace之后的 service列表(以二级缓存为主)
List instanceList = addIpAddresses(service, ephemeral, ips);
Instances instances = new Instances();
instances.setInstanceList(instanceList);
//根据一致性算法的不同最新的service列表写入到集群中
consistencyService.put(key, instances);
}
}
因为Instance交由Service管理,所以Instance的变化通过Service的onChange来管理包含(change与del),nacos服务端通过Service类添加listener来监听instance数据变更,一旦数据变更触发Service下onChange方法来推送各client节点列表更新(后续的节点订阅详细叙说)。
3.1:客户端
与上线类似,在AbstractAutoServiceRegistration(NacosAutoServiceRegistration继承之)中
若NacosAutoServiceRegistration对象被spring容器销毁之后,调用destroy()方法
3.2:服务端
通过InstaceController来接受信息。
4.1:客户端
如果是临时实例,则不会在 Nacos 服务端持久化存储,需要通过上报心跳的方式进行包活,如果一段时间内没有上报心跳,则会被 Nacos 服务端摘除。在被摘除后如果又开始上报心跳,则会重新将这个实例注册。持久化实例则会持久化被 Nacos 服务端,此时即使注册实例的客户端进程不在,这个实例也不会从服务端删除,只会将健康状态设为不健康。之前服务的健康检查模式有三种:client、server 和none, 分别代表客户端上报、服务端探测和取消健康检查。在1.0之后由ephemeral指定,若ephemeral为true代表通过client来通过心跳检测,若为false代表由nacos服务进行探测。通过BeatReactor类中的定时任务定时执行。
当一个instance进行注册的时候,通过BeatReactor添加一个心跳检测任务
beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
BeatReactor该类从命名上可以看出采用了反应核设计模式,由他进行BeatTask的分配调度。
添加一个BeatInfo
客户端会携带这个实例的Meat数据通过服务端中InstanceController提供API接口进行通信,服务端中解析的核心代码:
上述的异步线程为ClientBeatProcessor,如下所示为其核心代码:
客户端收到服务端对心跳影响对请求之后,解析result结果,判断是否需要重新注册后,通过executorService进行延时调度(这个延时的时间以客户端设定为主HEART_BEAT_INTERVAL配置默认5s,若此次请求有问题 以默认的switchDomain中时间为主默认5s)
4.2:服务端
服务端健康检测由多个地方触发:
1:Service中通过ClientBeatCheckTask(每5s执行一次)检测该Service下所有非持久化实例信息的活性,剔除那些僵尸节点(这里只会处理非持久化节点,对于非持久化节点对应的Service是分片处理的,每一个非持久化实例信息在由注册的时候选定的nacos节点进行心跳检测,也存储在这个nacos Service中,但实例数据会通过leader转发到RaftCore中缓存中存储,不会落盘),若当前时间距离上一次心跳检测时间超过15s(默认)标为非健康并触发消息通知机制,告知各服务端节点列表变更(这属于后续的节点订阅内容),这里在nacos1.3版本为何在发送为使用nio来发送,这里可以进行优化。检测是否超过30s来自动剔除该实例(由全局设置nacos.naming.expireInstance 默认为true,这里的剔除通过http调用本地的InstanceController中删除接口(先整理本地service对应新实例列表,交由具体的一致性算法来重新写入)
源码:
/**
* Check and update statues of ephemeral instances, remove them if they have been expired.
* 检查并更新临时实例的状态,如果它们已过期则将其删除。
* @author nkorange
*/
public class ClientBeatCheckTask implements Runnable {
private Service service;
public ClientBeatCheckTask(Service service) {
this.service = service;
}
@JsonIgnore
public PushService getPushService() {
return ApplicationUtils.getBean(PushService.class);
}
@JsonIgnore
public DistroMapper getDistroMapper() {
return ApplicationUtils.getBean(DistroMapper.class);
}
//全局配置 在application设置
public GlobalConfig getGlobalConfig() {
return ApplicationUtils.getBean(GlobalConfig.class);
}
public SwitchDomain getSwitchDomain() {
return ApplicationUtils.getBean(SwitchDomain.class);
}
public String taskKey() {
return KeyBuilder.buildServiceMetaKey(service.getNamespaceId(), service.getName());
}
@Override
public void run() {
try {
if (!getDistroMapper().responsible(service.getName())) {
return;
}
//是否开启对service下实例下心跳检测
if (!getSwitchDomain().isHealthCheckEnabled()) {
return;
}
//获取所有非持久化节点
List instances = service.allIPs(true);
// first set health status of instances:
//第一次判断实例运行情况
for (Instance instance : instances) {
//判断实例有没有进行心跳检测默认15s
if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {
//没被marked 并且 上一次实例正常 修改healthy
if (!instance.isMarked()) {
if (instance.isHealthy()) {
instance.setHealthy(false);
//日志记录
Loggers.EVT_LOG.info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client timeout after {}, last beat: {}",
instance.getIp(), instance.getPort(), instance.getClusterName(), service.getName(),
UtilsAndCommons.LOCALHOST_SITE, instance.getInstanceHeartBeatTimeOut(), instance.getLastBeat());
//自身监听该事件(这里通过事件驱动设计模式进行解耦) 可能发生实例变化 推送service下实例信息给各订阅者
getPushService().serviceChanged(service);
//发布InstanceHeartbeatTimeoutEvent事件
ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
}
}
}
}
//是否自动摘除临时实例 默认为true
if (!getGlobalConfig().isExpireInstance()) {
return;
}
//自动摘除过时的 临时实例
// then remove obsolete instances:
for (Instance instance : instances) {
//被标记了 不做处理
if (instance.isMarked()) {
continue;
}
//判断超时事件是否到了超时时间 默认30s 这里可能存在网络波动导致的第一次检测失败 所以这里剔除时间是healthchek的2倍
if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {
// delete instance
Loggers.SRV_LOG.info("[AUTO-DELETE-IP] service: {}, ip: {}", service.getName(), JacksonUtils.toJson(instance));
//删除实例
deleteIP(instance);
}
}
} catch (Exception e) {
Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);
}
}
/**
* 删除实例
* @param instance
*/
private void deleteIP(Instance instance) {
try {
//构建请求参数
NamingProxy.Request request = NamingProxy.Request.newRequest();
request.appendParam("ip", instance.getIp())
.appendParam("port", String.valueOf(instance.getPort()))
.appendParam("ephemeral", "true")
.appendParam("clusterName", instance.getClusterName())
.appendParam("serviceName", service.getName())
.appendParam("namespaceId", service.getNamespaceId());
//集群删除instance实例
String url = "http://127.0.0.1:" + ApplicationUtils.getPort() + ApplicationUtils.getContextPath()
+ UtilsAndCommons.NACOS_NAMING_CONTEXT + "/instance?" + request.toUrl();
// delete instance asynchronously:
//异步删除实例
HttpClient.asyncHttpDelete(url, null, null, new AsyncCompletionHandler() {
@Override
public Object onCompleted(Response response) throws Exception {
if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
Loggers.SRV_LOG.error("[IP-DEAD] failed to delete ip automatically, ip: {}, caused {}, resp code: {}",
instance.toJSON(), response.getResponseBody(), response.getStatusCode());
}
return null;
}
});
} catch (Exception e) {
Loggers.SRV_LOG.error("[IP-DEAD] failed to delete ip automatically, ip: {}, error: {}", instance.toJSON(), e);
}
}
}
其它集群通过该类去接受这些ACK信息并做出响应的呢,PushReceiver做到了,
2:cluster(cluster是逻辑上的存储方式,可能一个service集群中存在多种不同的应用环境-例如不同的机架这里就会产生不同的配置实例cluster,例如A机架上3台service服务,B机架上有2台service服务,那么这里一个service中就存在5个实例 2个cluster)中。cluster中通过HealthCheckTask任务来进行心跳检测,此对象只会在Cluster对象初始化后执行并通过线程pool延时调度一次
对于注册中心中节点的订阅一般分为两种:主动轮训与服务端推送,不同的注册中心的有着不同的实现
主要通过client中的HostReactor定时pull服务端数据
一旦服务端中Service发送变更,通过PushService中的udpSender来push给client数据
根据实例节点的不同集群中数据同步的方式也不同
通过上述三个步骤,使得集群中数据达到最终一致性
通过Raft算法的实现来保证,第一数据的变更通过leader来带领follower写入,第二leader与follower之间通过心跳机制来保证数据一致
从上述描述中,我们可以发现Nacos中内部为了保证集群中数据能快速实现一致性,大量的通过短周期(很多都是5s)的同步任务(包含服务端与服务端,服务端与客户端),服务端的主动push(这也对Eureka中最大的问题的解决,Eureka服务端发现节点被剔除,不会主动的通知client,需要client自己主动pull发现,这样在默认情况下其它服务消费者client会保存30s的脏数据),导致nacos集群会产生大量的网络通信。