服务注册: Nacos Client 会通过发送 REST 请求的方式向 Nacos Server 注册自己的服务,提供自身的元数据,比如 IP 地址、端口等信息,Nacos Server 接收到注册请求以后,就会把这些元数据信息存储在一个双层的内存 Map 中
服务心跳: 在服务注册后,Nacos Client 会维护一个定时心跳来持续通知 Nacos Server,说明服务一直处于可用状态,防止被剔除,默认 5s 发送一次心跳
服务健康检查: Nacos Server 会开启一个定时任务来检查注册服务实例的健康情况,对于超过 15s 没有收到客户端心跳会将它的 healthy 属性设置为 false「客户端服务发现时不会发现」,如果某个实例超过 30s 没有收到心跳,直接剔除该实例「被剔除的实例如果恢复发送心跳则会重新注册」
服务发现: 服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个 REST 请求给 Nacos Server,获取上面注册的服务清单,并且缓存在 Nacos Client 本地,同时会在 Nacos Client 本地开启一个定时任务来定时拉取服务端最新的注册表信息更新到本地缓存中
服务同步: Nacos Server 集群之间会互相同步服务实例,来保证服务信息的一致性
如上图,核心模块集中在 nacos-console、name-naming、nacos-config 中
Nacos GitHub 地址
Nacos 源码,本文在 nacos-2.1.1 版本进行分析
从 nacos-client 模块中开始说起,说起客户端就必然涉及到服务注册,先了解一下 Nacos 客户端会传递什么信息给到服务端侧,我们直接从 nacos-client 项目的 NamingTest 类说起:
public class NamingTest {
@Test
public void testServiceList() throws Exception {
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);
NamingService namingService = NacosFactory.createNamingService(properties);
namingService.registerInstance("nacos.test.1", instance);
ThreadUtils.sleep(5000L);
List<Instance> list = namingService.getAllInstances("nacos.test.1");
System.out.println(list);
ThreadUtils.sleep(30000L);
// ExpressionSelector expressionSelector = new ExpressionSelector();
// expressionSelector.setExpression("INSTANCE.metadata.registerSource = 'dubbo'");
// ListView serviceList = namingService.getServicesOfServer(1, 10, expressionSelector);
}
}
其实这就是客户端注册的一个 Test 类,它模仿了一个真实的服务注册进了 Nacos 的过程,包括 NacosServer 连接、实例的创建、实例属性的赋值、注册实例,所以在这个其中包括了服务注册的核心代码;仅从此处的代码分析,可以看出,Nacos 注册服务实例时,包含了两大类信息:Nacos Server 连接信息和实例信息
Nacos Server 连接信息,存储在 Properties 当中,包含以下信息:
注册实例信息用 Instance 对象承载,注册的实例信息又分为两部分:实例基础信息、元数据
实例基础信息
Instance 类包含了实例的基础信息之外,还包含了用于存储元数据的 metadata「描述数据的数据」类型为 HashMap,从当前这个 Demo 中我们可以得知存放了两个数据:
除了 Demo 中这些 “自定义” 信息,在 Instance 类中还定义了一些默认信息,这些信息通过 get 方法提供:
// 心跳的间隔时间默认值 5s
public long getInstanceHeartBeatInterval() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL,
Constants.DEFAULT_HEART_BEAT_INTERVAL);
}
// 心跳超时时间默认值 15s
public long getInstanceHeartBeatTimeOut() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_TIMEOUT,
Constants.DEFAULT_HEART_BEAT_TIMEOUT);
}
// IP 删除超时时间默认值 30s
public long getIpDeleteTimeout() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.IP_DELETE_TIMEOUT,
Constants.DEFAULT_IP_DELETE_TIMEOUT);
}
// 实例 ID 生成器默认值:simple
public String getInstanceIdGenerator() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.INSTANCE_ID_GENERATOR,
Constants.DEFAULT_INSTANCE_ID_GENERATOR);
}
上面的 get 方法在需要元数据默认值时会被使用到:
这些都是 Nacos 提供的默认值,也就是当前实例注册时会告知 Nacos Server 说:我的心跳间隔、心跳超时等对应的值是多少,按照这个值来判断我这个实例是否健康
有了这些信息,基本上已经知道注册实例时需要传递什么参数、需要配置什么参数了
NamingService 接口是 Nacos 命名服务对外提供的一个统一接口,看对应的源码可以发现,它提供了大量实例相关的接口方法
注册服务实例,提供了多个不同参数的重载方法,可以指定组名、集群名
// 注册服务实例,指定 IP、Port
void registerInstance(String serviceName, String ip, int port) throws NacosException
注销服务实例
void deregisterInstance(String serviceName, String ip, int port) throws NacosException
获取全部的服务实例
List<Instance> getAllInstances(String serviceName) throws NacosException
获取健康的服务实例
List<Instance> selectInstances(String serviceName, boolean healthy) throws NacosException
获取集群中健康的服务实例
List<Instance> selectInstances(String serviceName, List<String> clusters, boolean healthy) throws NacosException
使用负载均衡策略选择一个健康的服务实例
Instance selectOneHealthyInstance(String serviceName) throws NacosException
订阅服务事件
void subscribe(String serviceName, EventListener listener) throws NacosException
取消订阅服务事件
void unsubscribe(String serviceName, EventListener listener) throws NacosException
获取所有(或指定)服务名称
ListView<String> getServicesOfServer(int pageNo, int pageSize, ...) throws NacosException
获取所有订阅的服务
List<ServiceInfo> getSubscribeServices() throws NacosException
获取 Nacos 服务状态
String getServerStatus()
主动关闭服务
void shutDown() throws NacosException
这些方法中提供了大量的重载方法,应用于不同场景、不同类型实例或服务的筛选,所以我们只需要在不同的情况下使用不同的方法即可
NamingService 实例化是通过 NamingFactory 类和上面的 Nacos 服务信息,从以下代码中可以看出这里采用了反射机制来实例化 NamingService,具体的实现类为 NacosNamingService:
public static NamingService createNamingService(Properties properties) throws NacosException {
try {
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#registerInstance 方法来进行服务实例的注册,该方法接收两个参数:服务名称和实例对象;这个方法的最大作用是设置了当前实例的分组信息;在 Nacos 中,通过 Namespace、Group、Service、Cluster 等一层层的将实例进行环境的隔离;在这里设置了默认的分组名:DEFAULT_GROUP
public void registerInstance(String serviceName, Instance instance) throws NacosException {
registerInstance(serviceName, Constants.DEFAULT_GROUP, instance);
}
紧接着调用的 registerInstance 方法如下,这个方法做了两件事情:
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
NamingUtils.checkInstanceIsLegal(instance);
clientProxy.registerService(serviceName, groupName, instance);
}
1、检查心跳时间设置的是否正确(心跳默认值是 5s)
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'.");
}
// 实例的集群名称不满足条件:只支持数字和字母
if (!StringUtils.isEmpty(instance.getClusterName()) && !CLUSTER_NAME_PATTERN.matcher(instance.getClusterName()).matches()) {
throw new NacosException(NacosException.INVALID_PARAM,String.format("Instance 'clusterName' should be characters with only 0-9a-zA-Z-. (current: %s)",
instance.getClusterName()));
}
}
2、通过 NamingClientProxy 代理类来执行服务注册操作
通过 clientProxy 属性可以发现 NamingClientProxy 这个代理接口的具体实现是由 NamingClientProxyDelegate 来完成的,这个可以直接从 NacosNamingService 构造方法看出,在 init 方法中进行初始化操作:
public NacosNamingService(Properties properties) throws NacosException {
init(properties);
}
private void init(Properties properties) throws NacosException {
ValidatorUtils.checkInitParam(properties);
this.namespace = InitUtils.initNamespaceForNaming(properties);
InitUtils.initSerialization();
InitUtils.initWebRootContext(properties);
initLogName(properties);
this.notifierEventScope = UUID.randomUUID().toString();
this.changeNotifier = new InstancesChangeNotifier(this.notifierEventScope);
NotifyCenter.registerToPublisher(InstancesChangeEvent.class, 16384);
NotifyCenter.registerSubscriber(changeNotifier);
this.serviceInfoHolder = new ServiceInfoHolder(namespace, this.notifierEventScope, properties);
this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, changeNotifier);
}
继续追踪 NamingClientProxyDelegate#registerService 方法的具体实现,代码如下:
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
}
真正调用注册服务方并不是代理实现类,而是通过当前实例是否为瞬时对象,来选择对应的客户端代理进行请求:
private NamingClientProxy getExecuteClientProxy(Instance instance) {
return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
}
如果当前实例为瞬时对象,则采用 gRPC 协议(NamingGrpcClientProxy)进行请求,否则采用(NamingHttpClientProxy)进行请求
private boolean ephemeral = true;
默认为瞬时对象,也就是说,2.0 版本默认采用了 gRPC 协议与 Nacos 服务进行交互
主要关注一下 registerService 方法的实现,主要作了以下两件事情:
1、缓存当前注册的实例信息用于恢复,缓存的数据结构为 ConcurrentMap
2、另外一件事情就是封装了具体的参数,基于 gRPC 进行服务的调用和结果的处理
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);
}
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);
}
实际上我们在真实的生产的环境中,若让某个服务注册到 Nacos 中,首先需要引入依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
在引入这个依赖之后,找到 spring-boot 自动装配文件:META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration,\
com.alibaba.cloud.nacos.ribbon.RibbonNacosAutoConfiguration,\
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.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 自动装配的功能,自动装配的源码实现可以看博主博客:SpringBoot 自动装配流程及源码剖析
首先找到加载 EnbaleAutoConfiguration 对应的类,然后在这里我们就能看见很多 Nacos 相关的内容,一般这种文件都会找 Auto
关键字的文件来进行查看,然后我们要现在要了解的是客户端注册,所以我们应该要找的是:NacosServiceRegistryAutoConfiguration
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnNacosDiscoveryEnabled
@ConditionalOnProperty(value = {"spring.cloud.service-registry.auto-registration.enabled"},matchIfMissing = true)
@AutoConfigureAfter({AutoServiceRegistrationConfiguration.class, AutoServiceRegistrationAutoConfiguration.class, NacosDiscoveryAutoConfiguration.class})
public class NacosServiceRegistryAutoConfiguration {
public NacosServiceRegistryAutoConfiguration() {
}
@Bean
public NacosServiceRegistry nacosServiceRegistry(NacosServiceManager nacosServiceManager, NacosDiscoveryProperties nacosDiscoveryProperties) {
return new NacosServiceRegistry(nacosServiceManager, nacosDiscoveryProperties);
}
@Bean
@ConditionalOnBean({AutoServiceRegistrationProperties.class})
public NacosRegistration nacosRegistration(ObjectProvider<List<NacosRegistrationCustomizer>> registrationCustomizers, NacosDiscoveryProperties nacosDiscoveryProperties, ApplicationContext context) {
return new NacosRegistration((List)registrationCustomizers.getIfAvailable(), nacosDiscoveryProperties, context);
}
@Bean
@ConditionalOnBean({AutoServiceRegistrationProperties.class})
public NacosAutoServiceRegistration nacosAutoServiceRegistration(NacosServiceRegistry registry, AutoServiceRegistrationProperties autoServiceRegistrationProperties, NacosRegistration registration) {
return new NacosAutoServiceRegistration(registry, autoServiceRegistrationProperties, registration);
}
}
在此类中有很多的 Bean 组件,这些都是 spring 容器启动时自动进行注入的,一般情况下我们可能会看每一个 Bean 组件初始化时具体作了什么,但是实际上这里最核心的是 NacosAutoServiceRegistration 类,注入它时还需要三个参数,这三个参数会提前被加载,尤其是 NacosServiceRegistry 这个参数后续中也会用到
通过以上的类图可以看出,NacosAutoServiceRegistration 继承自 AbstractAutoServiceRegistration,而这个类型又实现了 ApplicationListener 接口,一般只要实现了 ApplicationListener 接口的,只需要关注它的 onApplicationEvent
方法的实现逻辑,它会帮我们完成一些初始化加载的工作,该方法是在项目启动时-容器进行初始化时进行调用的
public void onApplicationEvent(WebServerInitializedEvent event) {
this.bind(event);
}
@Deprecated
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();
}
}
最终在 start 方法中调用 register 方法进行服务注册
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);
}
}
}
分析到这里,可以得知真实的服务注册的入口和具体调用那个方法来注册
protected void register() {
this.serviceRegistry.register(this.getRegistration());
}
但 serviceRegistry 实际上是一个接口,它的具体实现类是 NacosServiceRegistry,到这里就与之前分析到的源码连接在一起了,因为在这里调用了之前讲过的 registerInstance 实例注册方法,查看该类下的 register 方法
public void register(Registration registration) {
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
} else {
NamingService namingService = this.namingService();
String serviceId = registration.getServiceId();
String group = this.nacosDiscoveryProperties.getGroup();
// 构建 Instance 实例
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});
}
}
}
}
接下来就是从已经分析过的链路进行调用了,如下:
NamingClientProxyDelegate#registerService—>NamingGrpcClientProxy#registerService
此段源码在标题「Nacos 客户端服务注册源码入口分析」分析过,在这不作过多阐述
发送请求服务端进行注册实例,在此处会涉及到 rpcClient#request 方法调用
private <T extends Response> T requestToServer(AbstractNamingRequest request, Class<T> responseClass) throws NacosException {
try {
request.putAllHeader(this.getSecurityHeaders(request.getNamespace(), request.getGroupName(), request.getServiceName()));
Response response = this.requestTimeout < 0L ? this.rpcClient.request(request) : this.rpcClient.request(request, this.requestTimeout);
if (ResponseCode.SUCCESS.getCode() != response.getResultCode()) {
throw new NacosException(response.getErrorCode(), response.getMessage());
}
if (responseClass.isAssignableFrom(response.getClass())) {
return response;
}
LogUtils.NAMING_LOGGER.error("Server return unexpected response '{}', expected response should be '{}'", response.getClass().getName(), responseClass.getName());
} catch (Exception var4) {
throw new NacosException(500, "Request nacos server failed: ", var4);
}
throw new NacosException(500, "Server return invalid response");
}
此 RpcClient 对象是在 NamingGrpcClientProxy 构造函数中进行初始化的,随即会调用其下 start 方法启动,在里面会调用 connectToServer(serverInfo) 方法「主要操作:通过地址+端口来初始化 gRPC 连接信息后进行建立连接」
public NamingGrpcClientProxy(String namespaceId, SecurityProxy securityProxy, ServerListFactory serverListFactory, Properties properties, ServiceInfoHolder serviceInfoHolder) throws NacosException {
super(securityProxy);
this.namespaceId = namespaceId;
this.uuid = UUID.randomUUID().toString();
this.requestTimeout = Long.parseLong(properties.getProperty("namingRequestTimeout", "-1"));
Map<String, String> labels = new HashMap();
labels.put("source", "sdk");
labels.put("module", "naming");
this.rpcClient = RpcClientFactory.createClient(this.uuid, ConnectionType.GRPC, labels);
this.redoService = new NamingGrpcRedoService(this);
this.start(serverListFactory, serviceInfoHolder);
}
private void start(ServerListFactory serverListFactory, ServiceInfoHolder serviceInfoHolder) throws NacosException {
this.rpcClient.serverListFactory(serverListFactory);
this.rpcClient.registerConnectionListener(this.redoService);
this.rpcClient.registerServerRequestHandler(new NamingPushRequestHandler(serviceInfoHolder));
this.rpcClient.start();
NotifyCenter.registerSubscriber(this);
}
随即在 requestToServer 方法中调用 rpcClient#request 方法就可以将客户端实例注册进去了!!!
Nacos 官网提供了基于 RestFul API 方式的接口给我们去进行客户端实例的注册
官网地址:Nacos 注册实例-Open API
通过 postman 方式进行调用,来测试是否通过该请求可以实现客户端注册效果:
curl --location --request POST 'http://127.0.0.1:8848/nacos/v1/ns/instance' \
--form 'port="8848"' \
--form 'ip="127.0.0.1"' \
--form 'serviceName="nacos.test"'
或在项目中将 ephemeral 属性设置为 false,也可以实现通过 http 方法进行调用.
ephemeral=true 代表基于 gRPC,ephemeral=false 代表基于 Http
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
ephemeral: false
然后可以运行有引入该依赖的项目,通过 Debug 方式运行,增加如下断点,验证流程是否按正常的来走:
随后会调用 NamingClientProxy 的实现类 NamingHttpClientProxy#registerService 方法,源码如下:
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
LogUtils.NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", new Object[]{this.namespaceId, serviceName, instance});
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
if (instance.isEphemeral()) {
BeatInfo beatInfo = this.beatReactor.buildBeatInfo(groupedServiceName, instance);
this.beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
Map<String, String> params = new HashMap(32);
params.put("namespaceId", this.namespaceId);
params.put("serviceName", groupedServiceName);
params.put("groupName", groupName);
params.put("clusterName", instance.getClusterName());
params.put("ip", instance.getIp());
params.put("port", String.valueOf(instance.getPort()));
params.put("weight", String.valueOf(instance.getWeight()));
params.put("enable", String.valueOf(instance.isEnabled()));
params.put("healthy", String.valueOf(instance.isHealthy()));
params.put("ephemeral", String.valueOf(instance.isEphemeral()));
params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
this.reqApi(UtilAndComs.nacosUrlInstance, params, "POST");
// UtilAndComs.nacosUrlInstance == /nacos/v1/ns/instance
}
在这里会把实例信息先放到散列表中,然后调用 reqApi 方法来发送请求,接口:/nacos/v1/ns/instance
spring-cloud-alibaba-version:2.2.8 版本使用的是 Nacos 2.1.1,默认采用 gRPC 调用;2.2.5.RELEASE 版本使用的是 Nacos 1.4.1,采用的是 Http 调用
欢迎大家在评论框分享您的看法,喜欢该文章帮忙给个赞和收藏,感谢!!
分享个人学习源码的几部曲
更多技术文章可以查看:vnjohn 个人博客