本文基于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方法如下:
服务端
1.注册表
先了解一下服务端保存实例信息的结构(下面我们简称叫注册表):
难理解的就是为什么一个服务会有多个集群,不应该一个服务就一个集群吗?
这里可以理解为按机房划分集群,不管有多少个集群都属于你该服务的。比如上海机房有一个SH 集群,深圳机房有一个SZ集群,请求的时候你可以按地区请求最近的集群实例,如果整个地区集群都不可用那么可以请求其他地区的集群实例。
网上找的示例图:
2.注册接口信息
注册接口:/nacos/v1/ns/instance
请求参数:
3.注册方法
InstanceController.register
4.注册流程(以临时实例为例)
ServiceManager.registerInstance
1.创建一个空的service放入注册表,为其 开启一个心跳检测,并将这个service加入监听列表
2.拿到创建好的service
3.完成实例的注册表更新,并完成nacos集群同步
5.创建空的service加入注册表
让我们看看是怎么创建空的service的
ServiceManager.createEmptyService:
ServiceManager.putServiceAndInit:
6.添加实例
ServiceManager.addInstance:
里面最重要的就是consistencyService.put(key, instances) 方法
consistencyService有很多种实现,根据实例的类型来判断具体走哪种实现方式,这里我们以临时实例为例,主要看看DistroConsistencyServiceImpl
DistroConsistencyServiceImpl.put 临时实例的注册方法
7.临时实例的添加
onPut方法:
1.会将任务放入Notifier内部的阻塞队列中,Notifier是个Runnable(异步执行任务)
2.最后会回到Service.onChange方法更新实例,内部调用updateIPs方法,这里面需要注意更新后会触发一个服务变更事件(后面有用)
Service.updateIPs:
public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
// 准备一个Map,key是cluster,值是集群下的Instance集合
Map<String, List<Instance>> 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<Instance> 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<String, List<Instance>> entry : ipMap.entrySet()) {
//make every ip mine
List<Instance> 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());
}
8.临时实例的集群同步
distroProtocol.sync()临时实例集群同步:
- 遍历集群中其他节点
- 定义一个DistroDelayTask异步任务放入一个ConcurrentHashMap中,会有一个ScheduledExecutorService线程池定时从这个map中取任务执行
线程池的定义在NacosDelayTaskExecuteEngine中:
上诉线程池执行的任务就是NacosDelayTaskExecuteEngine.processTasks()如下:
protected void processTasks() {
// 获取任务map中所有的key
Collection<Object> keys = getAllTaskKeys();
//遍历key 并执行任务
for (Object taskKey : keys) {
// 取一个任务便从map中移除一个任务
AbstractDelayTask task = removeTask(taskKey);
if (null == task) {
continue;
}
NacosTaskProcessor processor = getProcessor(taskKey);
if (null == processor) {
getEngineLog().error("processor not found for task, so discarded. " + task);
continue;
}
try {
// ReAdd task if process failed
// 尝试执行同步任务,如果失败会重试
if (!processor.process(task)) {
retryFailedTask(taskKey, task);
}
} catch (Throwable e) {
getEngineLog().error("Nacos task execute error : " + e.toString(), e);
// 如果失败会重试
retryFailedTask(taskKey, task);
}
}
}
DistroDelayTaskProcessor.process:
任务的执行被放入到process方法中,并被封装成DistroSyncChangeTask异步任务,又被塞到一个不知名封装好的地方(是一个阻塞队列,同样有地方取出来执行,我们直接看这个任务的执行)
DistroSyncChangeTask.run
1.syncData方法最终会到NamingProxy.syncData方法,执行HTTP请求,同步数据
2.如果失败了,则又会调用NacosDelayTaskExecuteEngine.addTask()方法重新将DistroDelayTask任务放进ConcurrentHashMap中,重复上述的processTasks方法
3.总结
- 客户端:启动则获取自身配置信息,发起http请求注册,临时实例同时会开启心跳机制(下面会说),服务端是单机的情况下请求失败会重试三次,服务端是单机的集群的情况下请求失败会轮询请求
- 服务端:
- 本地通过一个Map保存所有服务信息,注册的实质就是往map里面添加信息
- 会先创建空的服务,后更新服务中的实例信息
- 服务创建后会初始化服务,启动心跳检测
- 往服务中添加实例的时候会判断实例是永久实例还是临时实例,不同类型的实例有不同的处理方式
- 注册后同时会发布服务变更事件(后面说,先记着这个事件)
问题一:为什么客户端注册会先开启心跳后发起注册请求?
因为心跳是异步定时执行,就算后续的注册发生某意外注册失败,心跳机制还可以弥补注册(因为心跳也可以注册),如果是先发起注册后开启心跳,有可能注册发生某意外就直接终止了,心跳还没开启
问题二:服务端注册怎么保证线程安全?
服务器注册会先创建一个空的服务,后对该服务填充信息初始化,保存服务的map用ConcurrentHashMap修饰的,所以此过程是线程安全的,后续再对服务内实例更新的时候,采用synchronized对该服务做了加锁操作
问题三:服务端注册怎么保证性能?(临时实例)
前置操作时采用ConcurrentHashMap和synchronized锁服务,前者是最优的线程安全map,后者锁的是“服务”颗粒度一定程度的保证了性能,后续均采用了异步更新,如本地注册表更新采用了阻塞队列异步执行,临时实例集群同步过程中同样采用了阻塞队列异步执行机制,因为为阻塞的异步执行,所以保值性能的同时也保证了资源不会占用异常
二、心跳机制
临时实例:实例发起心跳请求,服务端处理请求,并需要进行心跳检测
永久实例:服务端主动发起健康检测
临时实例
客户端
临时实例在注册的时候会开启心跳包,这个在前面有说(默认5s心跳)
1.开启入口
NacosNamingService.registerInstance():
buildBeatInfo就是心跳信息的封装,我们主要看addBeatInfo方法
2.心跳开启
BeatReactor.addBeatInfo():
把心跳任务BeatTask丢到了延迟线程池里面执行,所以主要执行逻辑在BeatTask中
3.心跳执行逻辑
BeatTask.run()
1.发送HTTP心跳请求 URL地址为:/nacos/v1/ns/instance/beat
2.如果当前实例在注册中心未找到就重新注册
3.不管结果如何添加心跳任务,继续定时发起心跳(继续将任务丢到线程池里面执行)
心跳请求如下,URL地址为:/nacos/v1/ns/instance/beat
服务端
1.心跳请求的处理
从上面请求URL:/nacos/v1/ns/instance/beat ,我们很容易能找到处理请求逻辑:
InstanceControllerregister.beat()
我这里省略了前面的校验逻辑,直接看主体逻辑:
1.从注册表中获取实例信息,若无则重新注册
2.从注册表中获取服务,若无则直接异常(实例都注册完了,服务还找不到是不合理的)
3.开启异步任务将临时实例状态 置为健康状态,然后返回
2.心跳检测处理
实例如果宕机或者其他什么请求无法发送心跳,那么服务端自然也要对这个实例进行处理,就在服务初始化的时候,会开启会实例的心跳检测任务,上面也有提到过
ServiceManager.putServiceAndInit()
Service.init()
这个内部呢,就会有个ClientBeatCheckTask任务被放入了线程池,5s执行一次
ClientBeatCheckTask.run()
而ClientBeatCheckTask任务主要做了两件事:
- 找到心跳超时的实例,改变其健康状态,并发布serviceChange事件(后面说),还有实例心跳超时事件
- 找到满足删除条件的实例,从注册表中删除该实例信息(HTTP请求调用API,异步删除)
- 默认15s超时,30s剔除
永久实例
1.入口
入口和上面临时实例入口差不多,但永久实例是在集群初始化的时候,而临时实例是在服务初始化的时候
Cluster.init如下:
1.检测任务就是HealthCheckTask,这是个异步任务
2.延迟任务第一次(2000ms+5000ms以内随机数)执行,后续在1000ms-5000ms内浮动
HealthCheckTask任务如下:
- process方法有多种实现意味着有多种检测方案(这里以TCP为例)
- 不管结果如何继续延迟执行,约等于是个定时器(因为每次延迟时间不同)
- HealthCheckTask实例化的时候同时初始化了TcpSuperSenseProcessor,该方法是一个Runnable,会执行TcpSuperSenseProcessor.run方法(以TCP为例)
可以看到主动检测有多种方法:
2.获取所有永久实例加入阻塞队列
TcpSuperSenseProcessor .process()
这里会遍历所有永久实例并将实例封装成Beat加入到阻塞队列中
3.从队列中获取实例并封装
上面把永久实例信息放到了阻塞队列中,那么就肯定有方法去取,那是哪里呢?还记得前面说过TcpSuperSenseProcessor本身也是个异步任务吗?
TcpSuperSenseProcessor.run方法如下:
- 会先从阻塞队列中取出实例信息并封装,然后尝试与实例建立socket连接
- 最后判断连接状态,连接上了就进去实例健康处理并断开连接
- 同时会有一个异步的延迟任务,去检测这段时间内是否连接上过,这段时间内没连接上过说明连接超时了
- 可以看到这个run方法是个死循环
TcpSuperSenseProcessor.processTask方法如下:
- 从阻塞队列中取实例信息,并封装成TaskProcessor异步任务
- 批量提交任务,就是执行TaskProcessor异步任务
4.与实例尝试建立连接
TaskProcessor.run()
- 主要就是尝试建立socket连接
- 开启一个超时检测的延迟任务TimeOutTask(500ms)
5.超时判断
TimeOutTask.run()
因为已经延迟执行了,就判断这段时间内是否连接上过,没有就代表超时,超时会进入finishCheck方法
6.正常处理
前面都执行完了,就到TcpSuperSenseProcessor.run里面最后的PostProcessor异步任务了,连接成功会进入finishCheck方法
PostProcessor.run()
7.最终判断
Beat.finishCheck
连接成功或超时连接都会进到这里处理,不管如何都会发布服务变更事件,只会改变实例状态不会剔除实例
总结
- 临时实例:
- 采用客户端心跳检测模式,心跳检测周期5秒
- 心跳间隔超过15秒(默认)则标记为不健康
- 心跳间隔超过30秒(默认)则从服务列表删除
- 永久实例:
- 采用服务端主动健康检测方式
- 周期为2000 + 5000毫秒内的随机数
- 检测异常只会标记为不健康,不会删除
三、服务发现
实例是如何得知其他实例的信息呢?毕竟需要远程调用嘛
两种方式:1.客户端主动获取(定时更新)、2.服务端主动推送(长连接推送变更信息)
客户端主动获取
客户端
1.入口
NacosNamingService.getAllInstances
该方法就是获取所需的服务信息
2.第一次获取
HostReactor.getServiceInfo
1.先是故障转移机制判断是否去本地文件中读取信息,读到则返回
2.再去本地服务列表读取信息(本地缓存),没读到则创建一个空的服务,然后立刻去nacos中读取更新
3.读到了就返回,同时开启定时更新,定时向服务端同步信息 (正常1s,异常最多60s一次)
HostReactor.updateServiceNow
HostReactor.updateService
属性的serverProxy,这里面就是接口调用请求了
3.定时延迟任务
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
中间一大段省略了哈,最重要的就是那几步:
- 更新本地缓存
- 发布实例变更事件
- 写入磁盘(故障转移机制)
总结
服务的发现有两种方式
客户端主动获取:
- 会先读取缓存,缓存内读取不到则会去服务端获取,同时开启一个定时任务定时更新
- 定时任务1s一次,异常时会延长时间最长60s
- 拉取URL:/nacos/v1/ns/instance/list
服务端主动推送
- 服务端和客户端在启动后会建立一个长连接
- 服务端服务变更后会发布服务变更事件ServiceChangeEvent,会通过长连接将变更后的信息发送给客户端
- 客户端更新的方式是hostReactor.processServiceJson方法,会写入缓存、发布实例变更事件、写入磁盘