前面搭建了真实的微服务项目环境,体验了Nacos作为服务注册、服务发现以及配置中心的功能,这些功能里面包含了一下核心知识点:
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-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 map = new HashMap();
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");
这些信息包括:
服务实例封装
注册实例信息用 Instance 对象承载,注册的实例信息又分两部分:实例基础信息 和 元数据 。
基础信息字段说明:
元数据:
Map map = new HashMap();
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命名服务对外提供的一个统一接口,其提供的方法丰富:
主要包括如下方法:
这些方法均提供了重载方法,应用于不同场景和不同类型实例或服务的筛选。
回到服务注册测试类中的第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,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 implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener {
//...略
//实现监听类的方法
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
)。
如果对你有帮助,可以关注博主(不定期更新各种技术文档)
给博主一个免费的点赞以示鼓励,谢谢 !
欢迎各位点赞评论收藏⭐️