Nacos 服务注册与发现概述
Nacos 核心功能点
服务注册: 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 服务端及客户端模块图
如上图,核心模块集中在 nacos-console、name-naming、nacos-config 中
Nacos 客户端服务注册源码入口分析
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<String> serviceList = namingService.getServicesOfServer(1, 10, expressionSelector); } }
其实这就是客户端注册的一个 Test 类,它模仿了一个真实的服务注册进了 Nacos 的过程,包括 NacosServer 连接、实例的创建、实例属性的赋值、注册实例,所以在这个其中包括了服务注册的核心代码;仅从此处的代码分析,可以看出,Nacos 注册服务实例时,包含了两大类信息:Nacos Server 连接信息和实例信息
Nacos Server 连接信息
Nacos Server 连接信息,存储在 Properties 当中,包含以下信息:
- Server 地址:Nacos 服务器地址,属性 Key 为 serverAddr
- 用户名:连接 Nacos 服务用户名,属性 Key 为 username,默认值为 nacos
- 密码:连接 Nacos 服务密码,属性 Key 为 password,默认值为 nacos
实例信息
注册实例信息用 Instance 对象承载,注册的实例信息又分为两部分:实例基础信息、元数据
实例基础信息
- instanceId:实例的唯一 ID
- ip:实例 IP,提供给消费者进行通信的地址
- port:端口,提供给消费者访问的端口
- weight:权重,当前实例的权限,浮点类型(默认为 1.0D)
- healthy:健康状况,默认 true
- enabled:实例是否准备好接收请求,默认 true
- ephemeral:实例是否为瞬时的,默认为 true,缓存在内存中,还未持久化入库的
- clusterName:实例所属的集群名称
- serviceName:实例的服务信息
Instance 类包含了实例的基础信息之外,还包含了用于存储元数据的 metadata「描述数据的数据」类型为 HashMap,从当前这个 Demo 中我们可以得知存放了两个数据:
- netType:顾名思义,网络类型:值为 external,也就是外网的意思
- version:版本,Nacos 版本,这里是 2.0 版本
除了 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 方法在需要元数据默认值时会被使用到:
- preserved.heart.beat.interval:心跳间隔 Key,默认值为 5s,也就是默认 5s 进行一次心跳
- preserved.heart.beat.timeout:心跳超时 Key,默认值为 15s,也就是默认 15s 收不到心跳,实例将会标记为不健康
- preserved.ip.delete.timeout:实例 IP 被删除 Key,默认值为 30s,也就是 30s 收不到心跳,实例将会被移除
- preserved.instance.id.generator:实例 ID 生成器 Key,默认值为 simple
这些都是 Nacos 提供的默认值,也就是当前实例注册时会告知 Nacos Server 说:我的心跳间隔、心跳超时等对应的值是多少,按照这个值来判断我这个实例是否健康
有了这些信息,基本上已经知道注册实例时需要传递什么参数、需要配置什么参数了
NamingService 接口
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<I
- 使用负载均衡策略选择一个健康的服务实例
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); } }
NacosNamingService 实现
在示例代码中使用了 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); }