前面搭建了真实的微服务项目环境,体验了Nacos作为服务注册、服务发现以及配置中心的功能,这些功能里面包含了一下核心知识点:
- 服务注册: Nacos Client 会通过发送REST请求的方式向 Nacos Server 注册自己的服务,提供自身的元数据,比如ip地址、端口等信息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个 双层的内存Map 中。
- 服务发现: 服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存 。
- 服务心跳: 在服务注册后,Nacos Client 会维护一个 定时心跳 来持续通知 Nacos Server ,说明服务一直处于可用状态,防止被剔除。 默认5s发送一次心跳。
- 服务健康检查: Nacos Server 会开启一个 定时任务 用来检查注册服务实例的健康情况,对于 超过15s没有收到客户端心跳的实例会将它的healthy属性置为false (客户端服务发现时不会发现),如果某个 实例超过30秒没有收到心跳,直接剔除该实例 (被剔除的实例如果恢复发送心跳则会重新注册)。
- 服务同步: Nacos Server集群 之间会互相同步服务实例,用来保证服务信息的 一致性 。
Nacos源码环境搭建
因为前面我们的Nacos版本选择的是 2.0.3,所以下载源码的时候去下载对应版本的源码:
如果直接拉取 github.com/alibaba/nac… ,下载的源码是最新版2.1.1。
下载下来导入到Idea中,项目结构为:
启动后台管理 nacos-console 模块的启动类 Nacos.java ,如果直接启动报如下错误:
原因是 Nacos 2.1 版本使用的是protocol buffer compiler编译,这里我们下载下来后使用Maven compile ,重新编译一下就行了。
启动的时候还需要加个参数,以单机模式启动:
-Dnacos.standalone=true
如果不加这个参数,默认以集群方式启动,这种方式启动需要修改 application.properties
中关于数据库MySQL部分的配置(保证集群数据一致性),否则启动会报错。
Unable to start embedded Tomca。
看源码,只需要单机模式启动就行了。在Idea中添加启动参数如下:
配置好之后就可以运行测试,和启动普通的Spring Boot聚合项目一样,启动之后直接访问:http://localhost:8848/nacos, 这个时候就能看到我们以前看到的对应客户端页面了,Nacos源码启动完成。
Nacos客户端服务注册源码分析
从源码级别看Nacos是如何注册实例的
Nacos源码模块中有一个 nacos-client ,直接看其中测试类 NamingTest :
@Ignore
public class NamingTest {
@Test
public void testServiceList() throws Exception {
// 连接nacos server信息
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848");
properties.put(PropertyKeyConst.USERNAME, "nacos");
properties.put(PropertyKeyConst.PASSWORD, "nacos");
//实例信息封装,包括基础信息和元数据信息
Instance instance = new Instance();
instance.setIp("1.1.1.1");
instance.setPort(800);
instance.setWeight(2);
Map<String, String> map = new HashMap<String, String>();
map.put("netType", "external");
map.put("version", "2.0");
instance.setMetadata(map);
//通过NacosFactory获取NamingService
NamingService namingService = NacosFactory.createNamingService(properties);
//通过namingService注册实例
namingService.registerInstance("nacos.test.1", instance);
}
}
这就是 客户端注册 的一个测试类,它模仿了一个真实的服务注册进Nacos的过程,包括 Nacos Server连接属性封装、实例的创建、实例属性的赋值、注册实例,所以一段测试代码包含了服务注册的核心代码。
设置Nacos Server连接属性
Nacos Server连接信息,存储在Properties当中:
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848");
properties.put(PropertyKeyConst.USERNAME, "nacos");
properties.put(PropertyKeyConst.PASSWORD, "nacos");
这些信息包括:
- SERVER_ADDR :Nacos服务器地址,属性的PropertyKeyConst key为serverAddr
- USERNAME :连接Nacos服务的用户名,PropertyKeyConst key为username,默认值为nacos
- PASSWORD :连接Nacos服务的密码,PropertyKeyConst key为passwod,默认值为nacos
服务实例封装
注册实例信息用 Instance 对象承载,注册的实例信息又分两部分:实例基础信息 和 元数据 。
基础信息字段说明:
- instanceId:实例的唯一ID;
- ip:实例IP,提供给消费者进行通信的地址;
- port: 端口,提供给消费者访问的端口;
- weight:权重,当前实例的权重,浮点类型(默认1.0D);
- healthy:健康状况,默认true;
- enabled:实例是否准备好接收请求,默认true;
- ephemeral:实例是否为瞬时的,默认为true;
- clusterName:实例所属的集群名称;
- serviceName:实例的服务信息。
元数据:
Map<String, String> map = new HashMap<String, String>();
map.put("netType", "external");
map.put("version", "2.0");
instance.setMetadata(map);
元数据 Metadata 封装在HashMap中,这里只设置了 netType 和 version 两个数据,未设置的元数据通过Instance设置的默认值可以get到。
Instance 获取元数据-心跳时间、心跳超时时间、实例IP被剔除的时间、实例ID生成器的方法:
/**
* 获取实例心跳间隙,默认为5s,也就是默认5秒进行一次心跳
* @return 实例心跳间隙
*/
public long getInstanceHeartBeatInterval() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL,
Constants.DEFAULT_HEART_BEAT_INTERVAL);
}
/**
* 获取心跳超时时间,默认为15s,也就是默认15秒收不到心跳,实例将会标记为不健康
* @return 实例心跳超时时间
*/
public long getInstanceHeartBeatTimeOut() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_TIMEOUT,
Constants.DEFAULT_HEART_BEAT_TIMEOUT);
}
/**
* 获取实例IP被删除的时间,默认为30s,也就是30秒收不到心跳,实例将会被移除
* @return 实例IP被删除的时间间隔
*/
public long getIpDeleteTimeout() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.IP_DELETE_TIMEOUT,
Constants.DEFAULT_IP_DELETE_TIMEOUT);
}
/**
* 实例ID生成器,默认为simple
* @return 实例ID生成器
*/
public String getInstanceIdGenerator() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.INSTANCE_ID_GENERATOR,
Constants.DEFAULT_INSTANCE_ID_GENERATOR);
}
Nacos提供的元数据key:
public class PreservedMetadataKeys {
//心跳超时的key
public static final String HEART_BEAT_TIMEOUT = "preserved.heart.beat.timeout";
//实例IP被删除的key
public static final String IP_DELETE_TIMEOUT = "preserved.ip.delete.timeout";
//心跳间隙的key
public static final String HEART_BEAT_INTERVAL = "preserved.heart.beat.interval";
//实例ID生成器key
public static final String INSTANCE_ID_GENERATOR = "preserved.instance.id.generator";
}
元数据key对应的默认值:
package com.alibaba.nacos.api.common;
import java.util.concurrent.TimeUnit;
/**
* Constants.
*
* @author Nacos
*/
public class Constants {
//...略
//心跳超时,默认15s
public static final long DEFAULT_HEART_BEAT_TIMEOUT = TimeUnit.SECONDS.toMillis(15);
//ip剔除时间,默认30s未收到心跳则剔除实例
public static final long DEFAULT_IP_DELETE_TIMEOUT = TimeUnit.SECONDS.toMillis(30);
//心跳间隔。默认5s
public static final long DEFAULT_HEART_BEAT_INTERVAL = TimeUnit.SECONDS.toMillis(5);
//实例ID生成器,默认为simple
public static final String DEFAULT_INSTANCE_ID_GENERATOR = "simple";
//...略
}
这些都是Nacos默认提供的值,也就是当前实例注册时会告诉Nacos Server说:我的心跳间隙、心跳超时等对应的值是多少,你按照这个值来判断我这个实例是否健康。
此时,注册实例的时候,该封装什么参数,我们心里应该有点数了。
通过NamingService接口进行实例注册
NamingService 接口是Nacos命名服务对外提供的一个统一接口,其提供的方法丰富:
主要包括如下方法:
- void registerInstance(...): 注册服务实例
- void deregisterInstance(...): 注销服务实例
- List getAllInstances(...): 获取服务实例列表
- List selectInstances(...): 查询健康服务实例
- List selectInstances(....List clusters....): 查询集群中健康的服务实例
- Instance selectOneHealthyInstance(...): 使用负载均衡策略选择一个健康的服务实例
- void subscribe(...): 服务订阅
- void unsubscribe(...): 取消服务订阅
- List getSubscribeServices(): 获取所有订阅的服务
- String getServerStatus(): 获取Nacos服务的状态
- void shutDown(): 关闭服务
这些方法均提供了重载方法,应用于不同场景和不同类型实例或服务的筛选。
回到服务注册测试类中的第3步,通过NamingService接口注册实例:
//通过NacosFactory获取NamingService
NamingService namingService = NacosFactory.createNamingService(properties);
//通过namingService注册实例
namingService.registerInstance("nacos.test.1", instance);
再来看一下 NacosFactory 创建namingService的具体实现方法:
/**
* 创建NamingService实例
* @param properties 连接nacos server的属性
*/
public static NamingService createNamingService(Properties properties) throws NacosException {
try {
//通过反射机制来实例化NamingService
Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService");
Constructor constructor = driverImplClass.getConstructor(Properties.class);
return (NamingService) constructor.newInstance(properties);
} catch (Throwable e) {
throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
}
}
通过反射机制来实例化一个NamingService,具体的实现类是 com.alibaba.nacos.client.naming.NacosNamingService 。
NacosNamingService实现注册服务实例
注册代码中:
namingService.registerInstance("nacos.test.1", instance);
前面已经分析到,通过反射调用的是 NacosNamingService 的 registerInstance 方法,传递了两个参数:服务名和实例对象。具体方法在 NacosNamingService 类中如下:
//服务注册,传递参数服务名称和实例对象
@Override
public void registerInstance(String serviceName, Instance instance) throws NacosException {
registerInstance(serviceName, Constants.DEFAULT_GROUP, instance);
}
该方法完成了对实例对象的分组,即将对象分配到默认分组中 DEFAULT_GROUP 。
紧接着调用的方法 registerInstance(serviceName, Constants.DEFAULT_GROUP, instance) :
//注册服务
//参数:服务名称,实例分组(默认DEFAULT_GROUP),实例对象
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
//检查实例是否合法:通过服务心跳,如果不合法直接抛出异常
NamingUtils.checkInstanceIsLegal(instance);
//通过NamingClientProxy代理来执行服务注册
clientProxy.registerService(serviceName, groupName, instance);
}
这个 registerInstance 方法干了两件事:
1: checkInstanceIsLegal(instance) 检查传入的实例是否合法,通过检查心跳时间设置的对不对来判断,其源码如下
//类NamingUtils工具类下
public static void checkInstanceIsLegal(Instance instance) throws NacosException {
//心跳超时时间必须小于心跳间隔时间
//IP剔除的检查时间必须小于心跳间隔时间
if (instance.getInstanceHeartBeatTimeOut() < instance.getInstanceHeartBeatInterval()
|| instance.getIpDeleteTimeout() < instance.getInstanceHeartBeatInterval()) {
throw new NacosException(NacosException.INVALID_PARAM,
"Instance 'heart beat interval' must less than 'heart beat timeout' and 'ip delete timeout'.");
}
}
2: 通过 NamingClientProxy 代理来执行服务注册。
进入 clientProxy.registerService(serviceName, groupName, instance) 方法,发现有多个实现类(如下图),那么这里对应的是哪个实现类呢?
我们继续阅读NacosNamingService源码,找到 clientProxy 属性,通过构造方法可以知道 NamingClientProxy 这个代理接口的具体实现类是 NamingClientProxyDelegate 。
NamingClientProxyDelegate中实现实例注册的方法
从上面分析得知,实例注册的方法最终由 NamingClientProxyDelegate 中的 registerService(String serviceName, String groupName, Instance instance) 来实现,其方法为:
/**
* 注册服务
* @param serviceName 服务名称
* @param groupName 服务所在组
* @param instance 注册的实例
*/
@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
//这一句话干了两件事:
//1.getExecuteClientProxy(instance) 判断当前实例是否为瞬时对象,如果是瞬时对象,则返回grpcClientProxy(NamingGrpcClientProxy),否则返回httpClientProxy(NamingHttpClientProxy)
//2.registerService(serviceName, groupName, instance) 根据第1步返回的代理类型,执行相应的注册请求
getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
}
//...
//返回代理类型
private NamingClientProxy getExecuteClientProxy(Instance instance) {
//如果是瞬时对象,返回grpc协议的代理,否则返回http协议的代理
return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
}
该方法的实现只有一句话:getExecuteClientProxy(instance).registerService(serviceName, groupName, instance); 这句话执行了2个动作:
1. getExecuteClientProxy(instance): 判断传入的实例对象是否为瞬时对象,如果是瞬时对象,则返回 grpcClientProxy(NamingGrpcClientProxy) grpc协议的请求代理,否则返回 httpClientProxy(NamingHttpClientProxy) http协议的请求代理;
2. registerService(serviceName, groupName, instance): 根据返回的clientProxy类型执行相应的注册实例请求。
**瞬时对象 ** 就是对象在实例化后还没有放到持久化储存中,还在内存中的对象。而这里要注册的实例默认就是瞬时对象,因此在 Nacos(2.0版本) 中默认就是采用gRPC(Google开发的高性能RPC框架)协议与Nacos服务进行交互。下面我们就看 NamingGrpcClientProxy 中注册服务的实现方法。
NamingGrpcClientProxy中服务注册的实现方法
在该类中,实现服务注册的方法源码:
/**
* 服务注册
* @param serviceName 服务名称
* @param groupName 服务所在组
* @param instance 注册的实例对象
*/
@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,
instance);
//缓存当前实例,用于将来恢复
redoService.cacheInstanceForRedo(serviceName, groupName, instance);
//基于gRPC进行服务的调用
doRegisterService(serviceName, groupName, instance);
}
该方法一是要将当前实例缓存起来用于恢复,二是执行基于gRPC协议的请求注册。
缓存当前实例的具体实现:
public void cacheInstanceForRedo(String serviceName, String groupName, Instance instance) {
//将Instance实例缓存到ConcurrentMap中
//缓存实例的key值,格式为 groupName@@serviceName
String key = NamingUtils.getGroupedName(serviceName, groupName);
//缓存实例的value值,就是封装的instance实例
InstanceRedoData redoData = InstanceRedoData.build(serviceName, groupName, instance);
synchronized (registeredInstances) {
//registeredInstances是一个 ConcurrentMap<String, InstanceRedoData>,key是NamingUtils.getGroupedName生成的key,value是封装的实例信息
registeredInstances.put(key, redoData);
}
}
基于gRPC协议的请求注册具体实现:
//NamingGrpcClientProxy.java
public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException {
InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName,
NamingRemoteConstants.REGISTER_INSTANCE, instance);
requestToServer(request, Response.class);
redoService.instanceRegistered(serviceName, groupName);
}
//NamingGrpcRedoService.java
public void instanceRegistered(String serviceName, String groupName) {
String key = NamingUtils.getGroupedName(serviceName, groupName);
synchronized (registeredInstances) {
InstanceRedoData redoData = registeredInstances.get(key);
if (null != redoData) {
redoData.setRegistered(true);
}
}
}
综上分析,Nacos的服务注册流程:
实际微服务项目中是如何进行服务注册的?
以前文创建的 cloud_nacos_provider 项目为例,引入了 spring-cloud-starter-alibaba-nacos-discovery 这个包,先来看一下这个jar的结构:
Spring Boot通过读取 META-INF/spring.factories
里面的监听器类来做相应的动作,看一下客户端的这个 spring.factories 文件的内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration,\
com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,\
com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration,\
com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientConfiguration,\
com.alibaba.cloud.nacos.discovery.reactive.NacosReactiveDiscoveryClientConfiguration,\
com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration,\
com.alibaba.cloud.nacos.loadbalancer.LoadBalancerNacosAutoConfiguration,\
com.alibaba.cloud.nacos.NacosServiceAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration
org.springframework.context.ApplicationListener=\
com.alibaba.cloud.nacos.discovery.logging.NacosLoggingListener
很显然,Spring Boot自动装配首先找到 EnableAutoConfiguration 对应的类来进行加载,这里我们要看服务时怎么注册的,自然就能想到注册服务对应的是 com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration 这个类。
该类自动注册服务的方法:
@Bean
@ConditionalOnBean({AutoServiceRegistrationProperties.class})
public NacosAutoServiceRegistration nacosAutoServiceRegistration(NacosServiceRegistry registry, AutoServiceRegistrationProperties autoServiceRegistrationProperties, NacosRegistration registration) {
//实例化一个NacosAutoServiceRegistration
return new NacosAutoServiceRegistration(registry, autoServiceRegistrationProperties, registration);
}
这里实例化了一个 NacosAutoServiceRegistration 类,它就是实例注册的核心:
protected void register() {
if (!this.registration.getNacosDiscoveryProperties().isRegisterEnabled()) {
log.debug("Registration disabled.");
} else {
if (this.registration.getPort() < 0) {
this.registration.setPort(this.getPort().get());
}
//调用父类的register
super.register();
}
}
那么NacosAutoServiceRegistration的父类是哪个呢?来看一下它的关系图:
也就是说,NacosAutoServiceRegistration 继承了 AbstractAutoServiceRegistration ,AbstractAutoServiceRegistration 实现了监听接口 ApplicationListener ,一般情况下,根据经验,该类型的监听类,都会实现 onApplicationEvent 这种方法,我们来看源码验证一下:
public abstract class AbstractAutoServiceRegistration<R extends Registration> implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener<WebServerInitializedEvent> {
//...略
//实现监听类的方法
public void onApplicationEvent(WebServerInitializedEvent event) {
this.bind(event);
}
//具体实现
public void bind(WebServerInitializedEvent event) {
ApplicationContext context = event.getApplicationContext();
if (!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace())) {
this.port.compareAndSet(0, event.getWebServer().getPort());
//启动
this.start();
}
}
public void start() {
if (!this.isEnabled()) {
if (logger.isDebugEnabled()) {
logger.debug("Discovery Lifecycle disabled. Not starting");
}
} else {
if (!this.running.get()) {
this.context.publishEvent(new InstancePreRegisteredEvent(this, this.getRegistration()));
//调用注册的方法
this.register();
if (this.shouldRegisterManagement()) {
this.registerManagement();
}
this.context.publishEvent(new InstanceRegisteredEvent(this, this.getConfiguration()));
this.running.compareAndSet(false, true);
}
}
}
//...略
}
也就是说,项目启动的时候就会触发该类,然后 bind() 调用 start() 然后调用 register() 方法。在 register() 方法处打个断点,debug一下:
可以看到,配置文件中的相关属性被放到实例信息中了。没有配置的,nacos会给默认值,比如分组的默认值就是 DEFAULT_GROUP 等。
那么Nacos客户端将什么信息传递给服务器,我们就明了了,比如nacos server的ip地址、用户名,密码等,还有实例信息比如实例的ip、端口、权重等,实例信息还包括元数据信息(metaData)。
接着往下看,调用的register方法:
protected void register() {
//调用NacosServiceRegistry的register方法
this.serviceRegistry.register(this.getRegistration());
}
在 NacosServiceRegistry 中:
public void register(Registration registration) {
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
} else {
//实例化NamingService
NamingService namingService = this.namingService();
//服务id、组信息
String serviceId = registration.getServiceId();
String group = this.nacosDiscoveryProperties.getGroup();
//实例信息封装
Instance instance = this.getNacosInstanceFromRegistration(registration);
try {
//注册实例
namingService.registerInstance(serviceId, group, instance);
log.info("nacos registry, {} {} {}:{} register finished", new Object[]{group, serviceId, instance.getIp(), instance.getPort()});
} catch (Exception var7) {
if (this.nacosDiscoveryProperties.isFailFast()) {
log.error("nacos registry, {} register failed...{},", new Object[]{serviceId, registration.toString(), var7});
ReflectionUtils.rethrowRuntimeException(var7);
} else {
log.warn("Failfast is false. {} register failed...{},", new Object[]{serviceId, registration.toString(), var7});
}
}
}
}
注册实例调用的是NamingService的实现类 NacosNamingService 中 registerInstance 方法:
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
//检查服务实例设置的心跳时间是否合法
NamingUtils.checkInstanceIsLegal(instance);
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
if (instance.isEphemeral()) {
BeatInfo beatInfo = this.beatReactor.buildBeatInfo(groupedServiceName, instance);
this.beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
//服务注册
this.serverProxy.registerService(groupedServiceName, groupName, instance);
}
这里就和前面直接从源码看服务的注册过程连接上了,先检查实例的心跳时间,然后调用gPRC协议的代理进行服务注册:
最终调用发送请求 /nacos/v1/ns/instance 实现注册。
总结
Nacos服务注册流程总结
注册步骤小结:
-
读取Spring Boot装载配置文件 spring.factories,找到启动类 NacosAutoServiceRegistration;
-
NacosAutoServiceRegistration 继承 AbstractAutoServiceRegistration,它实现 ApplicationListener 接口;
-
实现ApplicationListener接口的 onApplicationEvent 方法,该方法调用 bind() ,然后调用 start() 方法;
-
start()方法中调用register(),该方法调用 NacosServiceRegistry 的register方法;
-
NacosServiceRegistry的register方法内部调用 NacosNamingService 的 registerInstance 方法;
-
根据实例的瞬时状态选择不同的proxy执行注册,默认是 gRPC 协议的 NamingGrpcClientProxy 执行注册;
-
完成实例注册(POST请求
/nacos/v1/ns/instance
)。
👍如果对你有帮助,可以关注博主(不定期更新各种技术文档)
给博主一个免费的点赞以示鼓励,谢谢 !
欢迎各位🔎点赞👍评论收藏⭐️