nacos服务注册与发现原理解析
前言:nacos 玩过微服务的想必不会陌生,它是阿里对于springcloud孵化出来的产品,用来完成服务之间的注册发现和配置中心,其核心作用我就不废话了
大致流程:每个服务都会有一个nacos client,它用来和nacos server打交道 用来具体的服务注册 查询等操作,服务提供者在启动的时候会向nacos server注册自己,服务消费者在启动的时候订阅nacos server上的服务提供者
服务注册
首先需要引入spring-cloud-starter-alibaba-nacos-discovery包,本文的引入的版本是2.2.1
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
根据sprin.factories配置来完成相关类的自动注册

我们重点来看这几个类,看名称可猜到是用来服务注册的,NacosServiceRegistryAutoConfiguration用来注册管理这几个bean

NacosServiceRegistry:完成服务注册,实现ServiceRegistry
NacosRegistration:用来注册时存储nacos服务端的相关信息
NacosAutoServiceRegistration 继承spring中的AbstractAutoServiceRegistration,AbstractAutoServiceRegistration继承ApplicationListener<WebServerInitializedEvent>,通过事件监听来发起服务注册,到时候会调用NacosServiceRegistry.register(registration)
来看具体如何注册
/******************************************************NacosServiceRegistry******************************************************/
public void register(Registration registration) {
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
} else {
String serviceId = registration.getServiceId();
String group = this.nacosDiscoveryProperties.getGroup();
Instance instance = this.getNacosInstanceFromRegistration(registration);
try {
this.namingService.registerInstance(serviceId, group, instance);
}
}
} /******************************************************NacosNamingService******************************************************/
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException { if (instance.isEphemeral()) {
// 添加心跳检测
beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
}
// 完成服务注册
serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
} /******************************************************NacosNamingService******************************************************/
public void addBeatInfo(String serviceName, BeatInfo beatInfo) { String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
// 发起一个心跳检测任务
executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
} /******************************************************BeatTask******************************************************/
class BeatTask implements Runnable { @Override
public void run() {
if (beatInfo.isStopped()) {
return;
}
long nextTime = beatInfo.getPeriod();
try {
// 向nacos服务发起心跳检测
JSONObject result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
long interval = result.getIntValue("clientBeatInterval");
boolean lightBeatEnabled = false;
if (result.containsKey(CommonParams.LIGHT_BEAT_ENABLED)) {
lightBeatEnabled = result.getBooleanValue(CommonParams.LIGHT_BEAT_ENABLED);
}
BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
if (interval > 0) {
nextTime = interval;
}
int code = NamingResponseCode.OK;
if (result.containsKey(CommonParams.CODE)) {
code = result.getIntValue(CommonParams.CODE);
}
if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
// 未注册 先完成注册
try {
serverProxy.registerService(beatInfo.getServiceName(),
NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
}
}
}
// 发起下一次心跳检测
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
}
服务提供者向nacos server发起服务注册前,先向nacos server建立起心跳检测机制,nacos server那边也有一个心跳检测,服务提供者不停的向nacos server发起心跳检测 告知自己的健康状态,nacos serve发现该服务心跳检测时间超时会发布超时事件来告知服务消费者

服务发现
服务发现由NacosWatch完成,它实现了Spring的Lifecycle接口,容器启动和销毁时会调用对应的start()和stop()方法
来看对应源码
@Override
public void start() {
// cas设置运行状态为true
if (this.running.compareAndSet(false, true)) {
// 延时执行一个服务发现任务
this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(
this::nacosServicesWatch, this.properties.getWatchDelay());
}
} @Override
public void stop() {
// 设置运行状态为false 然后取消正在执行的任务
if (this.running.compareAndSet(true, false) && this.watchFuture != null) {
this.watchFuture.cancel(true);
}
} public void nacosServicesWatch() {
try { boolean changed = false;
NamingService namingService = properties.namingServiceInstance();
// 获取nacos server上最新的服务提供者们
ListView<String> listView = properties.namingServiceInstance()
.getServicesOfServer(1, Integer.MAX_VALUE); List<String> serviceList = listView.getData(); // 有新的订阅产生 订阅完后发布事件
Set<String> currentServices = new HashSet<>(serviceList);
currentServices.removeAll(cacheServices);
if (currentServices.size() > 0) {
changed = true;
} // 取消已经下线的服务订阅,发起取消订阅操作并删除订阅监听
if (cacheServices.removeAll(new HashSet<>(serviceList))
&& cacheServices.size() > 0) {
changed = true; for (String serviceName : cacheServices) {
namingService.unsubscribe(serviceName,
subscribeListeners.get(serviceName));
subscribeListeners.remove(serviceName);
}
} cacheServices = new HashSet<>(serviceList); // 订阅服务 并对每个服务都添加一个心跳检测监听
for (String serviceName : cacheServices) {
if (!subscribeListeners.containsKey(serviceName)) {
EventListener eventListener = event -> NacosWatch.this.publisher
.publishEvent(new HeartbeatEvent(NacosWatch.this,
nacosWatchIndex.getAndIncrement()));
subscribeListeners.put(serviceName, eventListener);
namingService.subscribe(serviceName, eventListener); }
}
// 有服务变化 发布事件
if (changed) {
this.publisher.publishEvent(
new HeartbeatEvent(this, nacosWatchIndex.getAndIncrement()));
} }
catch (Exception e) {
log.error("Error watching Nacos Service change", e);
}
}
大致流程:nacos client这边在spring容器启动后执行一个服务订阅操作的延时任务,这个任务执行时先拉取nacos server那边最新的服务列表,然后与本地缓存的服务列表进行比较,取消订阅下线的服务,然后向nacos server发起订阅操作,订阅所有服务
那么服务消费者如何实时感知服务提供者的状态信息呢

1、服务消费者订阅后会执行一个轮询任务(每10s执行一次)用来拉取最新的服务提供者信息并实时更新,实现在HostReactor中的UpdateTask完成,下面来看代码
public class UpdateTask implements Runnable {
long lastRefTime = Long.MAX_VALUE;
private String clusters;
private String serviceName;
public UpdateTask(String serviceName, String clusters) {
this.serviceName = serviceName;
this.clusters = clusters;
}
@Override
public void run() {
try {
// 拿到当前的服务信息
ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
// 为空 拉取最新的服务列表随后更新
if (serviceObj == null) {
updateServiceNow(serviceName, clusters);
// 继续轮询
executor.schedule(this, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
return;
}
if (serviceObj.getLastRefTime() <= lastRefTime) {
// 当前服务未及时更新 进行更新操作
updateServiceNow(serviceName, clusters);
serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
} else {
// if serviceName already updated by push, we should not override it
// since the push data may be different from pull through force push
refreshOnly(serviceName, clusters);
}
// 设置服务最新的更新时间
lastRefTime = serviceObj.getLastRefTime();
// 订阅被取消
if (!eventDispatcher.isSubscribed(serviceName, clusters) &&
!futureMap.containsKey(ServiceInfo.getKey(serviceName, clusters))) {
// abort the update task:
NAMING_LOGGER.info("update task is stopped, service:" + serviceName + ", clusters:" + clusters);
return;
}
// 继续下一次轮询
executor.schedule(this, serviceObj.getCacheMillis(), TimeUnit.MILLISECONDS);
} catch (Throwable e) {
NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
}
}
}
2、上面服务注册时我们说过,服务提供者注册时nacos服务端也有一个相应的心跳检测,当心跳检测超时也就是未及时收到服务提供者的心跳包,nacos server判定该服务状态异常 随后通过UDP推送服务信息用来告知对应服务消费者,服务消费者通过PushReceiver来处理udp协议,HostReactor.processServiceJson(String json)来更新本地服务列表
/********************************PushReceiver*****************************/
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); 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 = JSON.parseObject(json, PushPacket.class);
String ack;
if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
// 处理变更信息
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(JSON.toJSONString(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);
}
}
}
服务注册和订阅我只讲解了主要流程,nacos server那边处理源码太多就不一一贴出来了,根据对应的api接口进去一看便知,nacos源码比较好理解,没有什么特别难读懂的地方,这边只是提供给大家一个看源码的思路,具体详细流程还需要读者自己去细读
下面通过代码来模拟nacos服务注册和订阅
先启动一个nacos server,然后打开控制台,添加一个命名空间

服务注册:分别注册两个服务,其中一个服务有两个实例
public class ServerRegister {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.setProperty("serverAddr", "http://localhost:8848");
properties.setProperty("namespace", "c7981cfd-ccb8-4a9f-8e80-cd1f9633ecec");
NamingService namingService = NacosFactory.createNamingService(properties);
// 同一个服务注册两个实例
namingService.registerInstance("serverProvider_1", "127.0.0.1", 8080);
namingService.registerInstance("serverProvider_1", "127.0.0.1", 8081);
namingService.registerInstance("serverProvider_2", "127.0.0.1", 7070);
// 获取服务名为serverProvider_1的实例信息
List<Instance> serverProvider = namingService.getAllInstances("serverProvider_1");
System.out.println(JSONArray.toJSONString(serverProvider, SerializerFeature.PrettyFormat, SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteDateUseDateFormat));
System.in.read();
}
}
服务订阅:获取所有的服务提供者,然后进行订阅 并添加一个事件用来监听订阅成功后的实例
public class ServerCustomer {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.setProperty("serverAddr", "http://localhost:8848");
properties.setProperty("namespace", "c7981cfd-ccb8-4a9f-8e80-cd1f9633ecec");
NamingService namingService = NacosFactory.createNamingService(properties);
List<String> serverList = namingService.getServicesOfServer(1, Integer.MAX_VALUE).getData();
System.out.println("得到服务提供者列表:" + JSONArray.toJSONString(serverList));
for (String server : serverList) {
// 订阅serverProvider服务 并添加一个监听器用来监听服务状态
namingService.subscribe(server, event -> {
NamingEvent namingEvent = (NamingEvent) event;
System.out.println(JSONObject.toJSONString(namingEvent, SerializerFeature.PrettyFormat, SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteDateUseDateFormat));
});
}
System.in.read();
}
}
打印信息:


nacos控制台也能看到相应服务信息

随后把服务提供者下线 再来看服务消费者那边的输出,可以看到服务实例已全部下线

nacos控制台

nacos服务注册与发现原理解析的更多相关文章
- Spring Cloud Alibaba Nacos 服务注册与发现功能实现!
Nacos 是 Spring Cloud Alibaba 中一个重要的组成部分,它提供了两个重要的功能:服务注册与发现和统一的配置中心功能. 服务注册与发现功能解决了微服务集群中,调用者和服务提供者连 ...
- Spring Cloud Alibaba | Nacos服务注册与发现
目录 Spring Cloud Alibaba | Nacos服务注册与发现 1. 服务提供者 1.1 pom.xml项目依赖 1.2 配置文件application.yml 1.3 启动类Produ ...
- Spring Cloud Alibaba(一) 如何使用nacos服务注册和发现
Nacos介绍 Nacos 致力于帮助您发现.配置和管理微服务.Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现.服务配置.服务元数据及流量管理. Nacos 帮助您更敏捷和容易地构 ...
- Alibaba Nacos 学习(三):Spring Cloud Nacos Discovery - FeignClient,Nacos 服务注册与发现
Alibaba Nacos 学习(一):Nacos介绍与安装 Alibaba Nacos 学习(二):Spring Cloud Nacos Config Alibaba Nacos 学习(三):Spr ...
- 2021升级版微服务教程4—Nacos 服务注册和发现
2021升级版SpringCloud教程从入门到实战精通「H版&alibaba&链路追踪&日志&事务&锁」 默认文件1610014380163 教程全目录「含视 ...
- 微服务架构 | *3.5 Nacos 服务注册与发现的源码分析
目录 前言 1. 客户端注册进 Nacos 注册中心(客户端视角) 1.1 Spring Cloud 提供的规范标准 1.2 Nacos 的自动配置类 1.3 监听服务初始化事件 AbstractAu ...
- Zookeeper服务注册与发现原理浅析
了解Zookeeper的我们都知道,Zookeeper是一种分布式协调服务,在分布式应用中,主要用来实现分布式服务的注册与发现以及分布式锁,本文我们简单介绍一下Zookeeper是如何实现服务的注册与 ...
- Spring Cloud Alibaba 实战 之 Nacos 服务注册和发现
服务注册与发现,服务发现主要用于实现各个微服务实例的自动化注册与发现,是微服务治理的核心,学习 Spring Cloud Alibaba,首先要了解框架中的服务注册和发现组件——Nacos. 一.Sp ...
- Nacos服务注册与发现的2种实现方法!
Spring Cloud Alibaba 技术体系中的 Nacos,提供了两个重要的功能:注册中心(服务注册与发现)功能和配置中心功能. 其中注册中心解决了微服务调用中,服务提供者和服务调用者的解耦, ...
随机推荐
- C语言网络编程(Linux && Windows)(1)
和朋友一起做课程设计,同时学习C语言的网络编程,以前写的都是python网络编程,但python很多的库都是封装好的,大部分人在使用的时候不会去了解底层的实现,这样对长远的学习不太好,也改正自己这方面 ...
- Intellij IDEA新导入项目运行出现Error:(60, 47) java: -source 1.5 中不支持 diamond 运算符 (请使用 -source 7 或更高版本以启用 diamond 运算符)
后台窗口报错如下: 问题原因 项目jdk版本配置不正确. 解决方案 ①File ->Project Structure ② ③之后还要检查一下这里 Settings-->Build,Exe ...
- 【题解】「CF675A」Infinite Sequence
我用的是:分类讨论+暴力 其中分类讨论中,我用了一种namespace名命名空间.如果: \(c = 0\) : 当 \(a == b\) 时,输出 \(YES\) 否则 \(NO\) \(c < ...
- 【eJOI2020】考试(dp & 树状数组优化)
Description \(n\) 个正整数排成一列,每个位置 \(i\) 有一个初始值 \(A_i\) 以及目标值 \(B_i\). 一次操作可以选定一个区间 \([l, r]\),并将区间内所有数 ...
- AcWing 326. XOR和路径
大型补档计划 题目链接 如果整体来做,发现既有加法,也有整体异或,这样不容易搞. 考虑异或,各个位置互不干扰,按位考虑一下. 枚举每一位 \(k\) 发现如果设 \(f[u]\) 为这一位的期望结果还 ...
- 通过游戏学javascript系列第一节Canvas游戏开发基础
本节教程通过一个简单的游戏小例子,讲解Canvas的基础知识. 最终效果: 点击移动的方块,方块上的分数会增加,方块的行进方向会改变,并且方块的速度会增加. 在线演示 源码 HTML5引入了canva ...
- 2020传智博黑马python课
网上花钱买来的资源,免费分享给冷冷的兄弟们! 已经把相关网站广告后缀名全部替换修改,现在文件已经全部没有广告了, 不过就是课件的压缩包需要密码,已经在该文件夹说明,大家放心使用, 文件清爽,文件名 ...
- 6个JS特效教程,学完即精通
6个JS特效教程,学完即精通 JavaScript特效教程,学完你就能写任何特效.本课程将JavaScript.BOM.DOM.jQuery和Ajax课程中的各种网页特效提取出了再进行汇总.内容涵盖了 ...
- Jmeter(6)命令行执行
Jmeter执行方式有2种:GUI和非GUI模式 GUI:在Windows电脑上运行,图形化界面,可直接查看测试结果,但是消耗压力机资源较高 非GUI:通过命令行执行,无图形化界面,不方便查看测试结果 ...
- js--数组的map()方法的使用
javaScript中Array.map()的用法 前言 作为一个刚刚踏入前端世界的小白,工作中看到身边同事大佬写的代码就像古诗一样简介整齐,而我的代码如同一堆散沙,看上去毫无段落感,而且简单的功能需 ...