本文基于1.4.3版本
GitHub地址:https://github.com/alibaba/nacos/releases
从NamingExample来看
反射初始化NacosNamingService
NacosNamingService.registerInstance
1.心跳参数校验
2.最终的服务名格式:serviceName@@groupName
3.如果是临时实例则会开启心跳包
4.服务注册 (参数组装调用API请求注册)
调用API请求注册 NamingProxy.reqApi
1.单机注册中心,失败重试(默认3次,前提是nacos异常)
2.集群注册中心,随机挑选一个注册,失败则轮询其他注册中心
3.最终调用callServer方法 (API_URL: IP:PORT/nacos/v1/ns/instance)
callServer方法如下:
先了解一下服务端保存实例信息的结构(下面我们简称叫注册表):
难理解的就是为什么一个服务会有多个集群,不应该一个服务就一个集群吗?
这里可以理解为按机房划分集群,不管有多少个集群都属于你该服务的。比如上海机房有一个SH 集群,深圳机房有一个SZ集群,请求的时候你可以按地区请求最近的集群实例,如果整个地区集群都不可用那么可以请求其他地区的集群实例。
注册接口:/nacos/v1/ns/instance
请求参数:
InstanceController.register
ServiceManager.registerInstance
1.创建一个空的service放入注册表,为其 开启一个心跳检测,并将这个service加入监听列表
2.拿到创建好的service
3.完成实例的注册表更新,并完成nacos集群同步
让我们看看是怎么创建空的service的
ServiceManager.createEmptyService:
ServiceManager.putServiceAndInit:
ServiceManager.addInstance:
里面最重要的就是consistencyService.put(key, instances) 方法
consistencyService有很多种实现,根据实例的类型来判断具体走哪种实现方式,这里我们以临时实例为例,主要看看DistroConsistencyServiceImpl
DistroConsistencyServiceImpl.put 临时实例的注册方法
onPut方法:
1.会将任务放入Notifier内部的阻塞队列中,Notifier是个Runnable(异步执行任务)
2.最后会回到Service.onChange方法更新实例,内部调用updateIPs方法,这里面需要注意更新后会触发一个服务变更事件(后面有用)
Service.updateIPs:
public void updateIPs(Collection instances, boolean ephemeral) {
// 准备一个Map,key是cluster,值是集群下的Instance集合
Map> ipMap = new HashMap<>(clusterMap.size());
// 获取服务的所有cluster名称
for (String clusterName : clusterMap.keySet()) {
ipMap.put(clusterName, new ArrayList<>());
}
for (Instance instance : instances) {
try {
if (instance == null) {
Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");
continue;
}
// 判断实例是否包含clusterName,没有的话用默认cluster
if (StringUtils.isEmpty(instance.getClusterName())) {
instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
}
// 判断cluster是否存在,不存在则创建新的cluster
if (!clusterMap.containsKey(instance.getClusterName())) {
Loggers.SRV_LOG
.warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
instance.getClusterName(), instance.toJson());
Cluster cluster = new Cluster(instance.getClusterName(), this);
cluster.init();
getClusterMap().put(instance.getClusterName(), cluster);
}
// 获取当前cluster实例的集合,不存在则创建新的
List clusterIPs = ipMap.get(instance.getClusterName());
if (clusterIPs == null) {
clusterIPs = new LinkedList<>();
ipMap.put(instance.getClusterName(), clusterIPs);
}
// 添加新的实例到 Instance 集合
clusterIPs.add(instance);
} catch (Exception e) {
Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);
}
}
for (Map.Entry> entry : ipMap.entrySet()) {
//make every ip mine
List entryIPs = entry.getValue();
// 将实例集合更新到 clusterMap(注册表)
clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
}
setLastModifiedMillis(System.currentTimeMillis());
//触发服务变更事件
getPushService().serviceChanged(this);
StringBuilder stringBuilder = new StringBuilder();
for (Instance instance : allIPs()) {
stringBuilder.append(instance.toIpAddr()).append("_").append(instance.isHealthy()).append(",");
}
Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}", getNamespaceId(), getName(),
stringBuilder.toString());
}
distroProtocol.sync()临时实例集群同步:
线程池的定义在NacosDelayTaskExecuteEngine中:
上诉线程池执行的任务就是NacosDelayTaskExecuteEngine.processTasks()如下:
protected void processTasks() {
// 获取任务map中所有的key
Collection
DistroDelayTaskProcessor.process:
任务的执行被放入到process方法中,并被封装成DistroSyncChangeTask异步任务,又被塞到一个不知名封装好的地方(是一个阻塞队列,同样有地方取出来执行,我们直接看这个任务的执行)
DistroSyncChangeTask.run
1.syncData方法最终会到NamingProxy.syncData方法,执行HTTP请求,同步数据
2.如果失败了,则又会调用NacosDelayTaskExecuteEngine.addTask()方法重新将DistroDelayTask任务放进ConcurrentHashMap中,重复上述的processTasks方法
因为心跳是异步定时执行,就算后续的注册发生某意外注册失败,心跳机制还可以弥补注册(因为心跳也可以注册),如果是先发起注册后开启心跳,有可能注册发生某意外就直接终止了,心跳还没开启
服务器注册会先创建一个空的服务,后对该服务填充信息初始化,保存服务的map用ConcurrentHashMap修饰的,所以此过程是线程安全的,后续再对服务内实例更新的时候,采用synchronized对该服务做了加锁操作
前置操作时采用ConcurrentHashMap和synchronized锁服务,前者是最优的线程安全map,后者锁的是“服务”颗粒度一定程度的保证了性能,后续均采用了异步更新,如本地注册表更新采用了阻塞队列异步执行,临时实例集群同步过程中同样采用了阻塞队列异步执行机制,因为为阻塞的异步执行,所以保值性能的同时也保证了资源不会占用异常
临时实例:实例发起心跳请求,服务端处理请求,并需要进行心跳检测
永久实例:服务端主动发起健康检测
临时实例在注册的时候会开启心跳包,这个在前面有说(默认5s心跳)
NacosNamingService.registerInstance():
buildBeatInfo就是心跳信息的封装,我们主要看addBeatInfo方法
BeatReactor.addBeatInfo():
把心跳任务BeatTask丢到了延迟线程池里面执行,所以主要执行逻辑在BeatTask中
BeatTask.run()
1.发送HTTP心跳请求 URL地址为:/nacos/v1/ns/instance/beat
2.如果当前实例在注册中心未找到就重新注册
3.不管结果如何添加心跳任务,继续定时发起心跳(继续将任务丢到线程池里面执行)
心跳请求如下,URL地址为:/nacos/v1/ns/instance/beat
从上面请求URL:/nacos/v1/ns/instance/beat ,我们很容易能找到处理请求逻辑:
InstanceControllerregister.beat()
我这里省略了前面的校验逻辑,直接看主体逻辑:
1.从注册表中获取实例信息,若无则重新注册
2.从注册表中获取服务,若无则直接异常(实例都注册完了,服务还找不到是不合理的)
3.开启异步任务将临时实例状态 置为健康状态,然后返回
实例如果宕机或者其他什么请求无法发送心跳,那么服务端自然也要对这个实例进行处理,就在服务初始化的时候,会开启会实例的心跳检测任务,上面也有提到过
ServiceManager.putServiceAndInit()
Service.init()
这个内部呢,就会有个ClientBeatCheckTask任务被放入了线程池,5s执行一次
ClientBeatCheckTask.run()
而ClientBeatCheckTask任务主要做了两件事:
入口和上面临时实例入口差不多,但永久实例是在集群初始化的时候,而临时实例是在服务初始化的时候
Cluster.init如下:
1.检测任务就是HealthCheckTask,这是个异步任务
2.延迟任务第一次(2000ms+5000ms以内随机数)执行,后续在1000ms-5000ms内浮动
HealthCheckTask任务如下:
TcpSuperSenseProcessor .process()
这里会遍历所有永久实例并将实例封装成Beat加入到阻塞队列中
上面把永久实例信息放到了阻塞队列中,那么就肯定有方法去取,那是哪里呢?还记得前面说过TcpSuperSenseProcessor本身也是个异步任务吗?
TcpSuperSenseProcessor.run方法如下:
TcpSuperSenseProcessor.processTask方法如下:
TaskProcessor.run()
TimeOutTask.run()
因为已经延迟执行了,就判断这段时间内是否连接上过,没有就代表超时,超时会进入finishCheck方法
前面都执行完了,就到TcpSuperSenseProcessor.run里面最后的PostProcessor异步任务了,连接成功会进入finishCheck方法
PostProcessor.run()
Beat.finishCheck
连接成功或超时连接都会进到这里处理,不管如何都会发布服务变更事件,只会改变实例状态不会剔除实例
实例是如何得知其他实例的信息呢?毕竟需要远程调用嘛
两种方式:1.客户端主动获取(定时更新)、2.服务端主动推送(长连接推送变更信息)
NacosNamingService.getAllInstances
该方法就是获取所需的服务信息
HostReactor.getServiceInfo
1.先是故障转移机制判断是否去本地文件中读取信息,读到则返回
2.再去本地服务列表读取信息(本地缓存),没读到则创建一个空的服务,然后立刻去nacos中读取更新
3.读到了就返回,同时开启定时更新,定时向服务端同步信息 (正常1s,异常最多60s一次)
HostReactor.updateServiceNow
HostReactor.updateService
属性的serverProxy,这里面就是接口调用请求了
HostReactor.scheduleUpdateIfAbsent
这里全先判断定时任务是否已经在异步任务列表中了,不在才会添加一个UpdateTask任务延迟执行
UpdateTask.run
UpdateTask类就是一个异步执行类,里面会调用updateService方法更新服务信息,同时结束又会开启延迟,延迟的时间跟请求失败的次数有关,最多60s,正常是1s一次
无论是updateService方法、refreshOnly方法,还是刚开始的直接去nacos拉取信息的方法都会调用serverProxy.queryList方法,这个方法就是HTTP请求获取信息:
获取服务信息列表URL:/nacos/v1/ns/instance/list
NamingProxy.queryList
InstanceController.list()
服务端这边处理请求就比较简单了,除去参数获取以及相关校验就剩服务列表的获取了
InstanceController.doSrvIpxt如下:
这里记住有个PushService
既然是主动推送那么就需要两个条件:1.建立长连接2.触发推送的事件
上面InstanceController.doSrvIpxt中的pushService.addClient就是把客户端UDP、IP等信息封装成PushClient对象存储在PushService类中,方便以后服务变更后推送消息
PushService类实现ApplicationListener接口,监听ServiceChangeEvent(服务变更事件)
ServiceChangeEvent事件处理就在当前类下:
事件触发则是PushService.serviceChanged方法,这个方法之前我们就见过,在服务注册里面,心跳里面也有,服务变更就会调用这个方法,触发事件让服务端主动推送服务变更信息
客户端是在PushReceiver类里面,这个类是个Runnable会在HostReactor中被实例化
PushReceiver.run()
收到服务端的信息就会交给HostReactor.processServiceJson处理
HostReactor.processServiceJson就会更新本地缓存的信息,上述客户端主动拉取的时候也会调用这个方法更新
HostReactor.processServiceJson
中间一大段省略了哈,最重要的就是那几步:
服务的发现有两种方式
客户端主动获取:
服务端主动推送