一、注册中心主要作用
注册中心是存放(服务注册)和调度服务服务发现和调用,实现服务和注册中心,服务和服务之间的相互通信。在服务端和客户端之间起协调者的作用。
1.1 服务端
- 服务注册:各个微服务部署的服务详细信息(ip、端口等服务信息)存放到注册中心,方便服务调用时候给调用方提供。
- 服务剔除:对于无法提供服务的服务进行清单列表的去除。
- 心跳检测:一定间隔内对所有注册到注册中心的服务进行存活健康检测,没存活的进行剔除操作。
1.2 对于客户端
- 服务发现:客户端向注册中心动态实时获取所有可用服务的清单列表。
- 服务通知:服务节点放生变化,及时将服务变更数据推送给客户端
1.3 高级功能
服务治理 :注册中心除了实现服务注册与发现,还可以用来实现服务治理相关功能
- 服务扩容/缩容, 机器迁移,权重,灰度流量
1.4 常用的注册中心
- Zookeeper
- Eureka
- Consul
- Nacos
- ETCD
二. nacos注册中心
2.1 springCloud集成nacos
2.1.1 nacos服务
通过图可以知道,使用nacos需要启用一个nacos服务,下载地址:nacos版本2.3.0下载地址
#下载nacos服务
wget -c https://github.com/alibaba/nacos/releases/download/2.3.0/nacos-server-2.3.0.tar.gz
#解压nacos
tar -zxvf nacos-server-2.3.0-BETA.tar.gz -C /usr/local/src/
#启动nacos服务(学习示例-单机启动)
./bin/startup.sh -m standalone
nacos客户端也是一个使用java程序
2.1.2 集成nacos
- maven依赖
基于springBoot版本2.3.2.RELEASE、 alibaba-cloud版本2.2.3.RELEASE、nacos的client为1.3.3
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependency>
</dependencies>
-
配置文件
spring: application: name: order-server cloud: nacos: #nacos注册中心配置 discovery: #nacos服务地址 server-addr: 47.94.132.22:8848 #命名空间 namespace: public #启动鉴权模式 访问nacos服务端需要使用用户名和密码 username: nacos password: nacos123
-
开启服务注册发现功能
@EnableDiscoveryClient开启服务注册发现功能
@SpringBootApplication //开启nacos注册中心 @EnableDiscoveryClient public class OrderApp { public static void main(String[] args) { SpringApplication.run(OrderApp.class, args); } }
2.2 nacos原理
分析nacos如何整合到spring中两个途径
- 开启注解@EnableDiscoveryClient做了哪些事情
- 自动装配类引入的实例类做了哪些shying
2.2.1 @EnableDiscoveryClient原理
从@EnableDiscoveryClient 开启注解开始分析nacos如何整合到spring中。进入该注解其使用@Import注解将ImportSelector实现类EnableDiscoveryClientImportSelector中创建的bean实例放入spring容器。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//EnableDiscoveryClientImportSelector 引入一些bean实例
@Import({EnableDiscoveryClientImportSelector.class})
public @interface EnableDiscoveryClient {
//是否自动注册到spring环境
boolean autoRegister() default true;
}
//EnableDiscoveryClientImportSelector 的selectImports 返回所有的权限定类名
EnableDiscoveryClientImportSelector引入一个自动装配类org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration
最终引入AutoServiceRegistrationProperties属性对象(没有什么实际作用)
属性 | 描述 | 默认值 |
---|---|---|
enabled | nacos自动装配是否可用 | true |
registerManagement | 是否将服务注册为本地管理服务 本地调用不走nacos获取服务fegin调用 | true |
failFast | 没有进行自动装配是否快速失败 | false |
2.2.1 自动装配
spring-cloud-starter-alibaba-nacos-discovery的maven依赖下的META-INF/spring.factories下是关于nacos注册中心的相关配置累
自动装配类 | 实例化的bean对象 | 描述 |
---|---|---|
NacosDiscoveryAutoConfiguration | NacosDiscoveryProperties | nacos服务相关配置对象,包含nacos地址、用户名、密码等 |
NacosDiscoveryAutoConfiguration | NacosServiceDiscovery | nacos服务注册发现核心对象 |
NacosServiceAutoConfiguration | NacosServiceManager | nacos服务对象管理 |
RibbonNacosAutoConfiguration | 集成ribbon | |
NacosServiceRegistryAutoConfiguration | NacosServiceRegistry | 执行注册逻辑的类对象 |
NacosServiceRegistryAutoConfiguration | NacosRegistration | 配置一些注册相关的信息,例如心跳周期、服务删除时间等等 |
NacosServiceRegistryAutoConfiguration | NacosAutoServiceRegistration | 服务自动注册的方法入口 |
NacosDiscoveryClientConfiguration | NacosDiscoveryClient | nacos注册发现客户端对象 |
NacosDiscoveryClientConfiguration | NacosWatch | NacosWatch用于定时与Nacos服务端数据同步当前服务的metadata数据并定时发布心跳事件 |
NacosReactiveDiscoveryClientConfiguration | NacosReactiveDiscoveryClient | 新版服务注册客户端对象 |
NacosConfigServerAutoConfiguration | 为NacosDiscoveryProperties设置元数据configPath |
自动装配类是实例化UML类图
通过UML类图发现NacosAutoServiceRegistration应该是nacos服务注册的入口
2.2.2 服务注册
- NacosAutoServiceRegistration实现了ApplicationListener的WebServerInitializedEvent,而WebServerInitializedEvent在当 Web 服务器初始化完成时,会触发该事件,并执行对应的监听器方法
@Override
@SuppressWarnings("deprecation")
// Web 服务器初始化完成时,会触发该监听器方法
public void onApplicationEvent(WebServerInitializedEvent event) {
bind(event);
}
@Deprecated
public void bind(WebServerInitializedEvent event) {
ApplicationContext context = event.getApplicationContext();
if (context instanceof ConfigurableWebServerApplicationContext) {
if ("management".equals(((ConfigurableWebServerApplicationContext) context)
.getServerNamespace())) {
return;
}
}
this.port.compareAndSet(0, event.getWebServer().getPort());
//服务注册入口
this.start();
}
//服务注册
public void start() {
//省略部分代码.....
//初次服务注册false
if (!this.running.get()) {
//服务注册预发布事件InstancePreRegisteredEvent,对nacos的一些元数据属性做缓存
this.context.publishEvent(
new InstancePreRegisteredEvent(this, getRegistration()));
//服务注册核心方法
register();
if (shouldRegisterManagement()) {
//注册本地管理服务
registerManagement();
}
服务注册预发布事件InstancePreRegisteredEvent,对nacos的一些元数据属性做缓存
this.context.publishEvent(
new InstanceRegisteredEvent<>(this, getConfiguration()));
//设置服务注册成功标识
this.running.compareAndSet(false, true);
}
}
- 最终调用NacosServiceRegistry的register
//Registration(NacosRegistration)保存需要注册服务的相关信息,比如serviceId,servideName,ip,port等
public void register(Registration registration) {
//如果没有注册服务信息 则不进行服务注册
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
} else {
//NamingService最终与Nacos服务端进行请求交互的核心对象
//通过HTTP请求进行服务注册,获取服务列表,心跳发送等
NamingService namingService = this.namingService();
String serviceId = registration.getServiceId();
String group = this.nacosDiscoveryProperties.getGroup();
//将服务信息转换为nacos注册的实例instance 实体转换而已
Instance instance = this.getNacosInstanceFromRegistration(registration);
try {
//服务注册
namingService.registerInstance(serviceId, group, instance);
//日志
} catch (Exception var7) {
//异常处理
}
}
}
上述Nacos java SDK的注册方法没有返回值,这是因为Nacos SDK做了补偿机制,注册之前,会先往缓存中插入一条记录表示开始注册,注册成功之后再从缓存中标记这条记录为注册成功,当注册失败时,缓存中这条记录是未注册成功的状态,Nacos SDK开启了一个定时任务,定时查询异常的缓存数据,重新发起注册。
- NacosNameService 注册请求
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
//groupedServiceName :groupName@ServiceName ->DEFAULT@order-service
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
//如果是临时节点则添加心跳
if (instance.isEphemeral()) {
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
//注册服务 http请求
serverProxy.registerService(groupedServiceName, groupName, instance);
}
- 流程图
2.2.3 服务发现
服务发现(自身)
Nacos的服务发现功能有两种实现方式
-
主动拉取服务实例:定时任务每周期时间内从服务端获取一下服务列表
-
服务订阅后通知:主动拉取的同时,会进行订阅,以便服务变更通知
核心组件为NacosWatch对象。此处订阅的是服务自身
NacosWatch主要实现了SmartLifecycle和ApplicationEventPublisherAware,ApplicationEventPublisherAware就是发布事件,这里主要指的就是发布HeartbeatEvent
事件上报心跳。SmartLifecycle该接口主要是作用是所有的bean都创建完成之后,可以执行自己的初始化工作,或者在退出时执行资源销毁工作。NacosWatch的start方法,主要是完成以下四件事情:
- 加入NamingEvent监听;
- 获取自身服务实例列表信息;
- 订阅自身服务变更事件;
- 发布HeartbeatEvent事件;
public void start() {
if (this.running.compareAndSet(false, true)) {
//构造一个事件监听对象(CAS锁保证只会创建一个)
EventListener eventListener = listenerMap.computeIfAbsent(buildKey(),
event -> new EventListener() {
//服务实例发生变化时触发
@Override
public void onEvent(Event event) {
if (event instanceof NamingEvent) {
List<Instance> instances = ((NamingEvent) event)
.getInstances();
Optional<Instance> instanceOptional = selectCurrentInstance(
instances);
instanceOptional.ifPresent(currentInstance -> {
resetIfNeeded(currentInstance);
});
}
}
});
//与nacos服务端交互的核心对象
NamingService namingService = nacosServiceManager
.getNamingService(properties.getNacosProperties());
try {
//通过客户端对象去订阅一个服务,当这个服务发生变更的时候就会回调EventListener监听器的onEvent方法
namingService.subscribe(properties.getService(), properties.getGroup(),
Arrays.asList(properties.getClusterName()), eventListener);
}
catch (Exception e) {
log.error("namingService subscribe failed, properties:{}", properties, e);
}
//定时发送心跳事件(心跳检测)
this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(
this::nacosServicesWatch, this.properties.getWatchDelay());
}
}
- 服务订阅
//com.alibaba.nacos.client.naming.NacosNamingService#subscribe
//获取服务列表信息、添加服务变更事件监听
public void subscribe(String serviceName, String groupName, List<String> clusters, EventListener listener)
throws NacosException {
//添加服务变更事件监听
eventDispatcher.addListener(
//主动获取拉取服务列表
hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ",")),
StringUtils.join(clusters, ","), listener);
}
先看第一个次主动拉取服务实例列表方法 hostReactor.getServiceInfo 方法
public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
//key生成逻辑:groupName@@applicationNme@@clusterName
String key = ServiceInfo.getKey(serviceName, clusters);
//如果开启了容灾\容错(默认为false) 从本地磁盘获取
if (failoverReactor.isFailoverSwitch()) {
return failoverReactor.getService(key);
}
//从缓存serviceInfoMap(内存)中获取服务实例,包含一个服务和多个服务实例
ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
//首次获取为空
if (null == serviceObj) {
//创建一个‘空’对象,放入缓存
serviceObj = new ServiceInfo(serviceName, clusters);
serviceInfoMap.put(serviceObj.getKey(), serviceObj);
//先用空对象站位,防止多个线程同时操作
updatingMap.put(serviceName, new Object());
//真正获取服务实例列表
updateServiceNow(serviceName, clusters);
//获取服务成功 移除
updatingMap.remove(serviceName);
}else if (updatingMap.containsKey(serviceName)) {
//正常获取服务实例列表,其他线程进入也获取服务实例列表
//如果有线程正在获取则进行阻塞等待5s
if (UPDATE_HOLD_INTERVAL > 0) {
synchronized (serviceObj) {
try {
serviceObj.wait(UPDATE_HOLD_INTERVAL);
} catch (InterruptedException e) {
//日志
}
}
}
}
//定时获取服务列表
scheduleUpdateIfAbsent(serviceName, clusters);
return serviceInfoMap.get(serviceObj.getKey());
}
-
获取服务实例
上述方法主要是与nacos服务端进行http请求交互 请求地址get请求 https:ip:port/nacos/v1/ns/instance/list。
其有两个途径获取。
- updateServiceNow():直接获取
- scheduleUpdateIfAbsent():定时任务周期性获取默认上一个任务结束到下一个任务开始的时间间隔为固定的1秒执行。
public void updateService(String serviceName, String clusters) throws NacosException {
//先从内存中获取服务
ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
try {
//调用http请求获取服务实例信息
//这里需要注意pushReceiver.getUdpPort() udpPort端口
String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);
if (StringUtils.isNotEmpty(result)) {
//获取服务,放入两个地方
//放入本地文件${user.home}/nacos/naming/${namespace}
//内存中 ServiceInfoMap
processServiceJson(result);
}
} finally {
//服务不等于空 则释放锁,允许其他现场进行服务获取
if (oldService != null) {
synchronized (oldService) {
oldService.notifyAll();
}
}
}
}
serverProxy.queryList的第三个参数udpPort的用法:
- 如果udpPort大于0,则该nacos客户端发起请求返回数据后,nacos服务端会将该客户端作为一个可接收订阅的客户端。
- 如果udpPort小于等于0 则nacos服务端将其作为普通客户端返回数据即可。
服务发现(其他服务)
订阅其他服务和自身订阅相似,只是在具体是由负载均衡组件发起的,以OpenFeign里面的Ribbon负载均衡为例DynamicServerListLoadBalancer。订阅其他其他服务,会在首次rpc调用时候触发
DynamicServerListLoadBalancer#restOfInit
--->enableAndInitLearnNewServersFeature()开启定时任务获取其他服务实例
--->updateListOfServers()立即获取其他服务实例
最终调用com.alibaba.cloud.nacos.ribbon.NacosServerList#getUpdatedListOfServers方法获取服务信息
//NacosServerList部分方法
public List<NacosServer> getUpdatedListOfServers() {
return getServers();
}
private List<NacosServer> getServers() {
try {
//从nacos配置中心获取当前分组
String group = discoveryProperties.getGroup();
//discoveryProperties.namingServiceInstance 获取NamingService组件
//selectInstances获取服务实例列表
List<Instance> instances = discoveryProperties.namingServiceInstance()
.selectInstances(serviceId, group, true);
return instancesToServerList(instances);
}
catch (Exception e) {
//异常省略。。。
}
}
NamingService很多selectInstances的重载方法
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters,
boolean healthy, boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
if (subscribe) {
//如果是订阅模式,可订阅的客户端,获取服务实例响应数据后。
serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
} else {
//普通客户端,获取服务实例响应数据后
serviceInfo = hostReactor
.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
}
//进行服务实例的过滤,正常的、可用的,权重大与0的服务实例返回。
return selectInstances(serviceInfo, healthy);
}
最终和服务订阅/发现-自身的逻辑相关调用:com.alibaba.nacos.client.naming.core.HostReactor#getServiceInfo,订阅服务。
总结一下:Nacos的服务发现功能有两种实现方式
- 主动拉取服务实例:定时任务每周期时间内从服务端获取一下服务列表。
- 服务订阅后通知:首次获取服务,将订阅该服务,以便变更时候的通知。
2.2.5 服务变更
服务变更其实是在服务发现过程中创建的,还记得服务发现过程中最终会调用NameProxy的queryList()方法 且pushReceiver.getUdpPort()大于0则与nacos服务端交互后,服务端会将其设置为一个接收订阅的客户端(pushClients),一但订阅的服务有变更则会通过udp协议push请求到pushClient。
String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);
客户端接收的入口为PushReceiver,该类会在NacosNamingService实例化的时候创建,其包含一个线程任务,循环接受服务变更。
@Override
public void run() {
//closed 服务关闭才会变为true
//所以正常条件下死玄幻
while (!closed) {
try {
//接收变更通知请求
byte[] buffer = new byte[UDP_MSS];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
udpSocket.receive(packet);
//获取数据转换为json
String json = new String(IoUtils.tryDecompress(packet.getData()), UTF_8).trim();
NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);
String ack;
//type=dom或service获取到变更的实例信息存放到该客户端并发送ack确认
//type其他类型只发送ack确认
if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
hostReactor.processServiceJson(pushPacket.data);
// send ack to server
ack = "省略ack响应信息";
} else if ("dump".equals(pushPacket.type)) {
// dump data to server
ack = "省略ack响应信息"
} else {
// do nothing send ack only
ack = "省略ack响应信息";
}
//给nacos服务发送ack确认。
udpSocket.send(new DatagramPacket(ack.getBytes(UTF_8), ack.getBytes(UTF_8).length,
packet.getSocketAddress()));
} catch (Exception e) {
NAMING_LOGGER.error("[NA] error while receiving push data", e);
}
}
}
- processServiceJson
不管是服务发现和服务变更获取到的数据进行都会通过**hostReactor.processServiceJson()**处理转换为实例对象,这里统一对其进行讲解
public ServiceInfo processServiceJson(String json) {
//json数据转换为 ServiceInfo包含服务信息和多个服务实例信息
ServiceInfo serviceInfo = JacksonUtils.toObj(json, ServiceInfo.class);
ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
//非法服务直接忽略
if (serviceInfo.getHosts() == null || !serviceInfo.validate()) {
//empty or error push, just ignore
return oldService;
}
//是否变化标识
boolean changed = false;
//如果有存在的服务信息则需要变更
//变更涉及:modHosts变更的实例、newHosts新增的实例、remvHosts需要移除的实例
if (oldService != null) {
//缓存中的服务信息的最后更新时间大于push的服务信息的最后更新时间
//说明缓存中的服务信息是最新的- 只是警告
if (oldService.getLastRefTime() > serviceInfo.getLastRefTime()) {
NAMING_LOGGER.warn("out of date data received, old-t: " + oldService.getLastRefTime() + ", new-t: "
+ serviceInfo.getLastRefTime());
}
serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
Map<String, Instance> oldHostMap = new HashMap<String, Instance>(oldService.getHosts().size());
for (Instance host : oldService.getHosts()) {
oldHostMap.put(host.toInetAddr(), host);
}
Map<String, Instance> newHostMap = new HashMap<String, Instance>(serviceInfo.getHosts().size());
for (Instance host : serviceInfo.getHosts()) {
newHostMap.put(host.toInetAddr(), host);
}
Set<Instance> modHosts = new HashSet<Instance>();
Set<Instance> newHosts = new HashSet<Instance>();
Set<Instance> remvHosts = new HashSet<Instance>();
List<Map.Entry<String, Instance>> newServiceHosts = new ArrayList<Map.Entry<String, Instance>>(
newHostMap.entrySet());
for (Map.Entry<String, Instance> entry : newServiceHosts) {
Instance host = entry.getValue();
String key = entry.getKey();
if (oldHostMap.containsKey(key) && !StringUtils
.equals(host.toString(), oldHostMap.get(key).toString())) {
modHosts.add(host);
continue;
}
if (!oldHostMap.containsKey(key)) {
newHosts.add(host);
}
}
for (Map.Entry<String, Instance> entry : oldHostMap.entrySet()) {
Instance host = entry.getValue();
String key = entry.getKey();
if (newHostMap.containsKey(key)) {
continue;
}
if (!newHostMap.containsKey(key)) {
remvHosts.add(host);
}
}
if (newHosts.size() > 0) {
changed = true;
NAMING_LOGGER.info("new ips(" + newHosts.size() + ") service: " + serviceInfo.getKey() + " -> "
+ JacksonUtils.toJson(newHosts));
}
if (remvHosts.size() > 0) {
changed = true;
NAMING_LOGGER.info("removed ips(" + remvHosts.size() + ") service: " + serviceInfo.getKey() + " -> "
+ JacksonUtils.toJson(remvHosts));
}
if (modHosts.size() > 0) {
changed = true;
updateBeatInfo(modHosts);
NAMING_LOGGER.info("modified ips(" + modHosts.size() + ") service: " + serviceInfo.getKey() + " -> "
+ JacksonUtils.toJson(modHosts));
}
serviceInfo.setJsonFromServer(json);
if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {
//发布事件变更通知
eventDispatcher.serviceChanged(serviceInfo);
//写入磁盘
DiskCache.write(serviceInfo, cacheDir);
}
} else {
//没有旧数据,肯定有变化,且为新增
changed = true;
NAMING_LOGGER.info("init new ips(" + serviceInfo.ipCount() + ") service: " + serviceInfo.getKey() + " -> "
+ JacksonUtils.toJson(serviceInfo.getHosts()));
//服务信息放入内存
serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
//发送服务变更事件信息,监听
eventDispatcher.serviceChanged(serviceInfo);
//存放服务信息的json格式
serviceInfo.setJsonFromServer(json);
//服务信息写入磁盘
DiskCache.write(serviceInfo, cacheDir);
}
MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());
if (changed) {
NAMING_LOGGER.info("current ips:(" + serviceInfo.ipCount() + ") service: " + serviceInfo.getKey() + " -> "
+ JacksonUtils.toJson(serviceInfo.getHosts()));
}
return serviceInfo;
}
变更数据通知后,将数据转换为服务实例并存放缓存和磁盘中发布变更事件。
2.2.6 健康检查
- **Nacos中 的 2 种健康检查机制 **
方式 | 实例属性 | 健康检查机制 |
---|---|---|
客户端主动上报 | 临时节点 | 客户端通过心跳上报方式告知服务端(nacos注册中心)健康状态, 默认心跳间隔5秒 nacos会在超过15秒未收到心跳后将实例设置为不健康状态;超过30秒将实例删除。 |
服务器端反向探测 | 永久节点 | nacos主动探知客户端健康状态,默认间隔为20秒;健康检查失败后实例会被标记为不健康 |
- 为啥有两种机制
- 如果是临时实例,则不会在 Nacos 服务端持久化存储,需要通过上报心跳的方式进行包活,如果一段时间内没有上报心跳,则会被 Nacos 服务端摘除。在被摘除后如果又开始上报心跳,则会重新将这个实例注册。
- 持久化实例则会持久化被 Nacos 服务端,此时即使注册实例的客户端进程不在,这个实例也不会从服务端删除,只会将健康状态设为不健康。
临时节点健康检查
在服务注册的过程中
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
if (instance.isEphemeral()) {
//临时节点心跳检查
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
serverProxy.registerService(groupedServiceName, groupName, instance);
}
调用BeatReactor对象的addBeatInfo发送心跳检查,该对象包含一个与nacos服务端交互的NamingProxy对象和一个执行定时任务的线程池executorService。
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
//构建心跳对应的key serviceName#ip#port
String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
BeatInfo existBeat = null;
//存在心跳对象(非首次) 设置以前的心跳为stop不可用
if ((existBeat = dom2Beat.remove(key)) != null) {
existBeat.setStopped(true);
}
//设置新的心跳对象
dom2Beat.put(key, beatInfo);
//执行定时任务,延迟周期为preserved.heart.beat.interval设置的值,没有设置默认为5S
executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}
定时任务调用其BeatTask的run方法,执行心跳检查。
public void run() {
//停止心跳检查 直接返回
if (beatInfo.isStopped()) {
return;
}
//获取心跳检查间隔 默认为5S 可以通过preserved.heart.beat.interval设置
long nextTime = beatInfo.getPeriod();
try {
//调用http://ip:port/nacos/v1/ns/instance/beat。上报服务健康状态
JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
//响应结果处理 获取服务端返回的心跳间隔clientBeatInterval和lightBeatEnabled属性
long interval = result.get("clientBeatInterval").asLong();
boolean lightBeatEnabled = false;
if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
}
BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
if (interval > 0) {
nextTime = interval;
}
int code = NamingResponseCode.OK;
if (result.has(CommonParams.CODE)) {
code = result.get(CommonParams.CODE).asInt();
}
if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
//重新注册
Instance instance = new Instance();
//省略属性填充
try {
serverProxy.registerService(beatInfo.getServiceName(),
NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
} catch (Exception ignore) {
}
}
} catch (NacosException ex) {
异常日志
}
//每次都要使用服务端返回的next 设置为延迟执行
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
}
永久节点健康检查
Nacos 服务器反向探测目前内置了 3 种探测协议:HTTP 探测、TCP 探测和 MySQL 探测
⼀般而言 HTTP 和 TCP 探测已经可以涵盖绝大多数的健康检查场景,MySQL 主要用于特殊的业务场景,例如数据库的主备需要通过服务名对外提供访问,需要确定当前访问数据库是否为主库时,那么我们此时的健康检查接口,是⼀个检查数据库是否为主库的 MySQL 命令。
TCP 探测的大体逻辑是通过与注册实例建立 channel,不断 ping 注册实例的端口,来判断实例是否健康。
HTTP 探测需要在 Nacos 控制台手动配置,并在对应的nacos客户端编写相应的请求接口
2.2.7 NacosNamingServices
NacosNamingServices是我们学习nacos注册中心的核心组件,服务注册、发现和变更以及心跳的核心功能都是由该组件完成,所以此处梳理一下该类的作用
NacosNamingServices创建的时候会实例化三个核心组件
- NamingProxy:NamingProxy封装了与服务端的操作,与nacos服务端进行交互的核心组件,比如服务注册、服务下线(服务剔除)、服务发现。
- BeatReactor: BeatReactor负责将客户端的信息上报和下线,对于非持久化的实例节点采用周期上报内容。
- HostReactor: 用于客户端服务的订阅,以及从服务端更新服务信息。
NamingProxy类
NacosNamingService 底层的服务注册、下线服务及服务查询等功能都是通过NamingProxy以api请求的形式与Nacos Server 完成交互。NamingProxy 类核心功能是完成不同请求接口的参数拼装以及请求处理流程。
HostReactor类
HostReactor主要负责客户端获取服务端注册的信息的部分,主要分为三个部分:
-
客户端需要调用NacosNamingService获取服务信息方法的时候,HostReactor负责把服务信息维护本地缓存的serviceInfoMap中,并且通过UpdateTask定时更新已存在的服务;
-
HostReactor内部维护PushReceiver对象,负责接收服务端通过UDP协议推送过来的服务变更的信息,并更新到本地缓存serviceInfoMap当中;
-
HostReactor内部维护FailoverReactor对象,负责当服务端不可用的时候,切换到本地文件缓存模式,从本地文件的缓存中获取服务信息;
本地地址: u s e r . h o m e / n a c o s / n a m i n g / {user.home}/nacos/naming/ user.home/nacos/naming/{namespace}/failover
nacos参考资料及补充
配置项key(spring.cloud.nacos.discovery前缀) | 描述 | 默认值 |
---|---|---|
namespace | 命名空间(用于隔离不同环境) | public |
group | 分组 | DEFAULT_GROUP |
server-addr | nacos服务地址 | |
cluster-name | nacos服务集群名称 | DEFAULT |
username | 鉴权用户名 | |
password | 鉴权密码 | |
ephemeral | 服务实例是否为临时节点 | true |
heart-beat-interval | 服务定时发送心跳的间隔 | |
endpoint | 域名-通过访问endpoint获取对应的server-adds, | |
ip | 注册ip , 当前服务注册到nacos的IP,最高优先级 | |
port | 注册端口 , 当前服务注册到nacos的端口,最高优先级 | |
service | 当前服务名称 | ${spring.application.name} |
weight | 权重, 1到100,值越大,权重越大 | |
enable | 是否禁用nacos | |
register-enabled | 是否禁用nacos服务注册 | |
instance-enabled | 是否禁用nacos服务发现 | false |
metadata | 元数据 , 可以使用key-value格式定义一些元数据 | |
ribbon.nacos.enabled | 是否集成ribbon | true |
namingLoadCacheAtStart | 启动时是否优先读取本地缓存(服务列表) | false |
- Instance对象:服务实例
属性 | 描述 |
---|---|
host | 服务ip |
port | 服务端口 |
weight | 权重 |
clusterName | 集群名称 |
enable | 是否可用 |
metadata | 元数据信息 |
ephemeral | 是否临时节点 |