Nacos源码分析

1.下载Nacos源码并运行

要研究Nacos源码自然不能用打包好的Nacos服务端jar包来运行,需要下载源码自己编译来运行。

1.1.下载Nacos源码

Nacos的GitHub地址:https://github.com/alibaba/nacos

课前资料中已经提供了下载好的1.4.2版本的Nacos源码:

Nacos源码分析 - 图1

如果需要研究其他版本的同学,也可以自行下载:

大家找到其release页面:https://github.com/alibaba/nacos/tags,找到其中的1.4.2.版本:

Nacos源码分析 - 图2

点击进入后,下载Source code(zip):

Nacos源码分析 - 图3

1.2.导入Demo工程

我们的课前资料提供了一个微服务Demo,包含了服务注册、发现等业务。

Nacos源码分析 - 图4

导入该项目后,查看其项目结构:

Nacos源码分析 - 图5

结构说明:

  • cloud-source-demo:项目父目录

    • cloud-demo:微服务的父工程,管理微服务依赖

      • order-service:订单微服务,业务中需要访问user-service,是一个服务消费者
      • user-service:用户微服务,对外暴露根据id查询用户的接口,是一个服务提供者

1.3.导入Nacos源码

将之前下载好的Nacos源码解压到cloud-source-demo项目目录中:

Nacos源码分析 - 图6

然后,使用IDEA将其作为一个module来导入:

1)选择项目结构选项:

Nacos源码分析 - 图7

然后点击导入module:

Nacos源码分析 - 图8

在弹出窗口中,选择nacos源码目录:

Nacos源码分析 - 图9

然后选择maven模块,finish:

Nacos源码分析 - 图10

最后,点击OK即可:

Nacos源码分析 - 图11

导入后的项目结构:

Nacos源码分析 - 图12

1.4.proto编译

Nacos底层的数据通信会基于protobuf对数据做序列化和反序列化。并将对应的proto文件定义在了consistency这个子模块中:

Nacos源码分析 - 图13

我们需要先将proto文件编译为对应的Java代码。

1.4.1.什么是protobuf

protobuf的全称是Protocol Buffer,是Google提供的一种数据序列化协议,这是Google官方的定义:

Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

可以简单理解为,是一种跨语言、跨平台的数据传输格式。与json的功能类似,但是无论是性能,还是数据大小都比json要好很多。

protobuf的之所以可以跨语言,就是因为数据定义的格式为.proto格式,需要基于protoc编译为对应的语言。

1.4.2.安装protoc

Protobuf的GitHub地址:https://github.com/protocolbuffers/protobuf/releases

我们可以下载windows版本的来使用:

Nacos源码分析 - 图14

另外,课前资料也提供了下载好的安装包:

Nacos源码分析 - 图15

解压到任意非中文目录下,其中的bin目录中的protoc.exe可以帮助我们编译:

Nacos源码分析 - 图16

然后将这个bin目录配置到你的环境变量path中,可以参考JDK的配置方式:

Nacos源码分析 - 图17

1.4.3.编译proto

进入nacos-1.4.2的consistency模块下的src/main目录下:

Nacos源码分析 - 图18

然后打开cmd窗口,运行下面的两个命令:

  1. protoc --java_out=./java ./proto/consistency.proto
  2. protoc --java_out=./java ./proto/Data.proto

如图:

Nacos源码分析 - 图19

会在nacos的consistency模块中编译出这些java代码:

Nacos源码分析 - 图20

1.5.运行

nacos服务端的入口是在console模块中的Nacos类:

Nacos源码分析 - 图21

我们需要让它单机启动:

Nacos源码分析 - 图22

然后新建一个SpringBootApplication:

Nacos源码分析 - 图23

然后填写应用信息:

Nacos源码分析 - 图24

然后运行Nacos这个main函数:

Nacos源码分析 - 图25

将order-service和user-service服务启动后,可以查看nacos控制台:

Nacos源码分析 - 图26

2.服务注册

服务注册到Nacos以后,会保存在一个本地注册表中,其结构如下:

Nacos源码分析 - 图27

首先最外层是一个Map,结构为:Map<String, Map<String, Service>>:

  • key:是namespace_id,起到环境隔离的作用。namespace下可以有多个group
  • value:又是一个Map<String, Service>,代表分组及组内的服务。一个组内可以有多个服务

    • key:代表group分组,不过作为key时格式是group_name:service_name
    • value:分组下的某一个服务,例如userservice,用户服务。类型为Service,内部也包含一个Map<String,Cluster>,一个服务下可以有多个集群

      • key:集群名称
      • value:Cluster类型,包含集群的具体信息。一个集群中可能包含多个实例,也就是具体的节点信息,其中包含一个Set<Instance>,就是该集群下的实例的集合

        • Instance:实例信息,包含实例的IP、Port、健康状态、权重等等信息

每一个服务去注册到Nacos时,就会把信息组织并存入这个Map中。

2.1.服务注册接口

Nacos提供了服务注册的API接口,客户端只需要向该接口发送请求,即可实现服务注册。

接口说明:注册一个实例到Nacos服务。

请求类型:POST

请求路径:/nacos/v1/ns/instance

请求参数:

名称 类型 是否必选 描述
ip 字符串 是 服务实例IP
port int 是 服务实例port
namespaceId 字符串 否 命名空间ID
weight double 否 权重
enabled boolean 否 是否上线
healthy boolean 否 是否健康
metadata 字符串 否 扩展信息
clusterName 字符串 否 集群名
serviceName 字符串 是 服务名
groupName 字符串 否 分组名
ephemeral boolean 否 是否临时实例

错误编码:

错误代码 描述 语义
400 Bad Request 客户端请求中的语法错误
403 Forbidden 没有权限
404 Not Found 无法找到资源
500 Internal Server Error 服务器内部错误
200 OK 正常

2.2.客户端

首先,我们需要找到服务注册的入口。

2.2.1.NacosServiceRegistryAutoConfiguration

因为Nacos的客户端是基于SpringBoot的自动装配实现的,我们可以在nacos-discovery依赖:

spring-cloud-starter-alibaba-nacos-discovery-2.2.6.RELEASE.jar

这个包中找到Nacos自动装配信息:

Nacos源码分析 - 图28

可以看到,有很多个自动配置类被加载了,其中跟服务注册有关的就是NacosServiceRegistryAutoConfiguration这个类,我们跟入其中。

可以看到,在NacosServiceRegistryAutoConfiguration这个类中,包含一个跟自动注册有关的Bean:

Nacos源码分析 - 图29

2.2.2.NacosAutoServiceRegistration

NacosAutoServiceRegistration源码如图:

Nacos源码分析 - 图30

可以看到在初始化时,其父类AbstractAutoServiceRegistration也被初始化了。

AbstractAutoServiceRegistration如图:

Nacos源码分析 - 图31

可以看到它实现了ApplicationListener接口,监听Spring容器启动过程中的事件。

在监听到WebServerInitializedEvent(web服务初始化完成)的事件后,执行了bind 方法。

Nacos源码分析 - 图32

其中的bind方法如下:

  1. public void bind(WebServerInitializedEvent event) {
  2. // 获取 ApplicationContext
  3. ApplicationContext context = event.getApplicationContext();
  4. // 判断服务的 namespace,一般都是null
  5. if (context instanceof ConfigurableWebServerApplicationContext) {
  6. if ("management".equals(((ConfigurableWebServerApplicationContext) context)
  7. .getServerNamespace())) {
  8. return;
  9. }
  10. }
  11. // 记录当前 web 服务的端口
  12. this.port.compareAndSet(0, event.getWebServer().getPort());
  13. // 启动当前服务注册流程
  14. this.start();
  15. }

其中的start方法流程:

  1. public void start() {
  2. if (!isEnabled()) {
  3. if (logger.isDebugEnabled()) {
  4. logger.debug("Discovery Lifecycle disabled. Not starting");
  5. }
  6. return;
  7. }
  8. // 当前服务处于未运行状态时,才进行初始化
  9. if (!this.running.get()) {
  10. // 发布服务开始注册的事件
  11. this.context.publishEvent(
  12. new InstancePreRegisteredEvent(this, getRegistration()));
  13. // ☆☆☆☆开始注册☆☆☆☆
  14. register();
  15. if (shouldRegisterManagement()) {
  16. registerManagement();
  17. }
  18. // 发布注册完成事件
  19. this.context.publishEvent(
  20. new InstanceRegisteredEvent<>(this, getConfiguration()));
  21. // 服务状态设置为运行状态,基于AtomicBoolean
  22. this.running.compareAndSet(false, true);
  23. }
  24. }

其中最关键的register()方法就是完成服务注册的关键,代码如下:

  1. protected void register() {
  2. this.serviceRegistry.register(getRegistration());
  3. }

此处的this.serviceRegistry就是NacosServiceRegistry:

Nacos源码分析 - 图33

2.2.3.NacosServiceRegistry

NacosServiceRegistry是Spring的ServiceRegistry接口的实现类,而ServiceRegistry接口是服务注册、发现的规约接口,定义了register、deregister等方法的声明。

而NacosServiceRegistry对register的实现如下:

  1. @Override
  2. public void register(Registration registration) {
  3. // 判断serviceId是否为空,也就是spring.application.name不能为空
  4. if (StringUtils.isEmpty(registration.getServiceId())) {
  5. log.warn("No service to register for nacos client...");
  6. return;
  7. }
  8. // 获取Nacos的命名服务,其实就是注册中心服务
  9. NamingService namingService = namingService();
  10. // 获取 serviceId 和 Group
  11. String serviceId = registration.getServiceId();
  12. String group = nacosDiscoveryProperties.getGroup();
  13. // 封装服务实例的基本信息,如 cluster-name、是否为临时实例、权重、IP、端口等
  14. Instance instance = getNacosInstanceFromRegistration(registration);
  15. try {
  16. // 开始注册服务
  17. namingService.registerInstance(serviceId, group, instance);
  18. log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
  19. instance.getIp(), instance.getPort());
  20. }
  21. catch (Exception e) {
  22. if (nacosDiscoveryProperties.isFailFast()) {
  23. log.error("nacos registry, {} register failed...{},", serviceId,
  24. registration.toString(), e);
  25. rethrowRuntimeException(e);
  26. }
  27. else {
  28. log.warn("Failfast is false. {} register failed...{},", serviceId,
  29. registration.toString(), e);
  30. }
  31. }
  32. }

可以看到方法中最终是调用NamingService的registerInstance方法实现注册的。

而NamingService接口的默认实现就是NacosNamingService。

2.2.4.NacosNamingService

NacosNamingService提供了服务注册、订阅等功能。

其中registerInstance就是注册服务实例,源码如下:

  1. @Override
  2. public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
  3. // 检查超时参数是否异常。心跳超时时间(默认15秒)必须大于心跳周期(默认5秒)
  4. NamingUtils.checkInstanceIsLegal(instance);
  5. // 拼接得到新的服务名,格式为:groupName@@serviceId
  6. String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
  7. // 判断是否为临时实例,默认为 true。
  8. if (instance.isEphemeral()) {
  9. // 如果是临时实例,需要定时向 Nacos 服务发送心跳
  10. BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
  11. beatReactor.addBeatInfo(groupedServiceName, beatInfo);
  12. }
  13. // 发送注册服务实例的请求
  14. serverProxy.registerService(groupedServiceName, groupName, instance);
  15. }

最终,由NacosProxy的registerService方法,完成服务注册。

代码如下:

  1. public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
  2. NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
  3. instance);
  4. // 组织请求参数
  5. final Map<String, String> params = new HashMap<String, String>(16);
  6. params.put(CommonParams.NAMESPACE_ID, namespaceId);
  7. params.put(CommonParams.SERVICE_NAME, serviceName);
  8. params.put(CommonParams.GROUP_NAME, groupName);
  9. params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
  10. params.put("ip", instance.getIp());
  11. params.put("port", String.valueOf(instance.getPort()));
  12. params.put("weight", String.valueOf(instance.getWeight()));
  13. params.put("enable", String.valueOf(instance.isEnabled()));
  14. params.put("healthy", String.valueOf(instance.isHealthy()));
  15. params.put("ephemeral", String.valueOf(instance.isEphemeral()));
  16. params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
  17. // 通过POST请求将上述参数,发送到 /nacos/v1/ns/instance
  18. reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
  19. }

这里提交的信息就是Nacos服务注册接口需要的完整参数,核心参数有:

  • namespace_id:环境
  • service_name:服务名称
  • group_name:组名称
  • cluster_name:集群名称
  • ip: 当前实例的ip地址
  • port: 当前实例的端口

而在NacosNamingService的registerInstance方法中,有一段是与服务心跳有关的代码,我们在后续会继续学习。

Nacos源码分析 - 图34

2.2.5.客户端注册的流程图

如图:

Nacos源码分析 - 图35

2.3.服务端

在nacos-console的模块中,会引入nacos-naming这个模块:

Nacos源码分析 - 图36

模块结构如下:

Nacos源码分析 - 图37

其中的com.alibaba.nacos.naming.controllers包下就有服务注册、发现等相关的各种接口,其中的服务注册是在InstanceController类中:

Nacos源码分析 - 图38

2.3.1.InstanceController

进入InstanceController类,可以看到一个register方法,就是服务注册的方法了:

  1. @CanDistro
  2. @PostMapping
  3. @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
  4. public String register(HttpServletRequest request) throws Exception {
  5. // 尝试获取namespaceId
  6. final String namespaceId = WebUtils
  7. .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
  8. // 尝试获取serviceName,其格式为 group_name@@service_name
  9. final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
  10. NamingUtils.checkServiceNameFormat(serviceName);
  11. // 解析出实例信息,封装为Instance对象
  12. final Instance instance = parseInstance(request);
  13. // 注册实例
  14. serviceManager.registerInstance(namespaceId, serviceName, instance);
  15. return "ok";
  16. }

这里,进入到了serviceManager.registerInstance()方法中。

2.3.2.ServiceManager

ServiceManager就是Nacos中管理服务、实例信息的核心API,其中就包含Nacos的服务注册表:

Nacos源码分析 - 图39

而其中的registerInstance方法就是注册服务实例的方法:

  1. /**
  2. * Register an instance to a service in AP mode.
  3. *
  4. * <p>This method creates service or cluster silently if they don't exist.
  5. *
  6. * @param namespaceId id of namespace
  7. * @param serviceName service name
  8. * @param instance instance to register
  9. * @throws Exception any error occurred in the process
  10. */
  11. public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
  12. // 创建一个空的service(如果是第一次来注册实例,要先创建一个空service出来,放入注册表)
  13. // 此时不包含实例信息
  14. createEmptyService(namespaceId, serviceName, instance.isEphemeral());
  15. // 拿到创建好的service
  16. Service service = getService(namespaceId, serviceName);
  17. // 拿不到则抛异常
  18. if (service == null) {
  19. throw new NacosException(NacosException.INVALID_PARAM,
  20. "service not found, namespace: " + namespaceId + ", service: " + serviceName);
  21. }
  22. // 添加要注册的实例到service中
  23. addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
  24. }

创建好了服务,接下来就要添加实例到服务中:

  1. /**
  2. * Add instance to service.
  3. *
  4. * @param namespaceId namespace
  5. * @param serviceName service name
  6. * @param ephemeral whether instance is ephemeral
  7. * @param ips instances
  8. * @throws NacosException nacos exception
  9. */
  10. public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
  11. throws NacosException {
  12. // 监听服务列表用到的key,服务唯一标识,例如:com.alibaba.nacos.naming.iplist.ephemeral.public##DEFAULT_GROUP@@order-service
  13. String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
  14. // 获取服务
  15. Service service = getService(namespaceId, serviceName);
  16. // 同步锁,避免并发修改的安全问题
  17. synchronized (service) {
  18. // 1)获取要更新的实例列表
  19. List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
  20. // 2)封装实例列表到Instances对象
  21. Instances instances = new Instances();
  22. instances.setInstanceList(instanceList);
  23. // 3)完成 注册表更新 以及 Nacos集群的数据同步
  24. consistencyService.put(key, instances);
  25. }
  26. }

该方法中对修改服务列表的动作加锁处理,确保线程安全。而在同步代码块中,包含下面几步:

  • 1)先获取要更新的实例列表,addIpAddresses(service, ephemeral, ips);
  • 2)然后将更新后的数据封装到Instances对象中,后面更新注册表时使用
  • 3)最后,调用consistencyService.put()方法完成Nacos集群的数据同步,保证集群一致性。

注意:在第1步的addIPAddress中,会拷贝旧的实例列表,添加新实例到列表中。在第3步中,完成对实例状态更新后,则会用新列表直接覆盖旧实例列表。而在更新过程中,旧实例列表不受影响,用户依然可以读取。

这样在更新列表状态过程中,无需阻塞用户的读操作,也不会导致用户读取到脏数据,性能比较好。这种方案称为CopyOnWrite方案。

1)更服务列表

我们来看看实例列表的更新,对应的方法是addIpAddresses(service, ephemeral, ips);:

  1. private List<Instance> addIpAddresses(Service service, boolean ephemeral, Instance... ips) throws NacosException {
  2. return updateIpAddresses(service, UtilsAndCommons.UPDATE_INSTANCE_ACTION_ADD, ephemeral, ips);
  3. }

继续进入updateIpAddresses方法:

  1. public List<Instance> updateIpAddresses(Service service, String action, boolean ephemeral, Instance... ips)
  2. throws NacosException {
  3. // 根据namespaceId、serviceName获取当前服务的实例列表,返回值是Datum
  4. // 第一次来,肯定是null
  5. Datum datum = consistencyService
  6. .get(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), ephemeral));
  7. // 得到服务中现有的实例列表
  8. List<Instance> currentIPs = service.allIPs(ephemeral);
  9. // 创建map,保存实例列表,key为ip地址,value是Instance对象
  10. Map<String, Instance> currentInstances = new HashMap<>(currentIPs.size());
  11. // 创建Set集合,保存实例的instanceId
  12. Set<String> currentInstanceIds = Sets.newHashSet();
  13. // 遍历要现有的实例列表
  14. for (Instance instance : currentIPs) {
  15. // 添加到map中
  16. currentInstances.put(instance.toIpAddr(), instance);
  17. // 添加instanceId到set中
  18. currentInstanceIds.add(instance.getInstanceId());
  19. }
  20. // 创建map,用来保存更新后的实例列表
  21. Map<String, Instance> instanceMap;
  22. if (datum != null && null != datum.value) {
  23. // 如果服务中已经有旧的数据,则先保存旧的实例列表
  24. instanceMap = setValid(((Instances) datum.value).getInstanceList(), currentInstances);
  25. } else {
  26. // 如果没有旧数据,则直接创建新的map
  27. instanceMap = new HashMap<>(ips.length);
  28. }
  29. // 遍历实例列表
  30. for (Instance instance : ips) {
  31. // 判断服务中是否包含要注册的实例的cluster信息
  32. if (!service.getClusterMap().containsKey(instance.getClusterName())) {
  33. // 如果不包含,创建新的cluster
  34. Cluster cluster = new Cluster(instance.getClusterName(), service);
  35. cluster.init();
  36. // 将集群放入service的注册表
  37. service.getClusterMap().put(instance.getClusterName(), cluster);
  38. Loggers.SRV_LOG
  39. .warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
  40. instance.getClusterName(), instance.toJson());
  41. }
  42. // 删除实例 or 新增实例 ?
  43. if (UtilsAndCommons.UPDATE_INSTANCE_ACTION_REMOVE.equals(action)) {
  44. instanceMap.remove(instance.getDatumKey());
  45. } else {
  46. // 新增实例,instance生成全新的instanceId
  47. Instance oldInstance = instanceMap.get(instance.getDatumKey());
  48. if (oldInstance != null) {
  49. instance.setInstanceId(oldInstance.getInstanceId());
  50. } else {
  51. instance.setInstanceId(instance.generateInstanceId(currentInstanceIds));
  52. }
  53. // 放入instance列表
  54. instanceMap.put(instance.getDatumKey(), instance);
  55. }
  56. }
  57. if (instanceMap.size() <= 0 && UtilsAndCommons.UPDATE_INSTANCE_ACTION_ADD.equals(action)) {
  58. throw new IllegalArgumentException(
  59. "ip list can not be empty, service: " + service.getName() + ", ip list: " + JacksonUtils
  60. .toJson(instanceMap.values()));
  61. }
  62. // 将instanceMap中的所有实例转为List返回
  63. return new ArrayList<>(instanceMap.values());
  64. }

简单来讲,就是先获取旧的实例列表,然后把新的实例信息与旧的做对比,新的实例就添加,老的实例同步ID。然后返回最新的实例列表。

2)Nacos集群一致性

在完成本地服务列表更新后,Nacos又实现了集群一致性更新,调用的是:

consistencyService.put(key, instances);

这里的ConsistencyService接口,代表集群一致性的接口,有很多中不同实现:

Nacos源码分析 - 图40

我们进入DelegateConsistencyServiceImpl来看:

  1. @Override
  2. public void put(String key, Record value) throws NacosException {
  3. // 根据实例是否是临时实例,判断委托对象
  4. mapConsistencyService(key).put(key, value);
  5. }

其中的mapConsistencyService(key)方法就是选择委托方式的:

  1. private ConsistencyService mapConsistencyService(String key) {
  2. // 判断是否是临时实例:
  3. // 是,选择 ephemeralConsistencyService,也就是 DistroConsistencyServiceImpl类
  4. // 否,选择 persistentConsistencyService,也就是PersistentConsistencyServiceDelegateImpl
  5. return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService : persistentConsistencyService;
  6. }

默认情况下,所有实例都是临时实例,我们关注DistroConsistencyServiceImpl即可。

2.3.4.DistroConsistencyServiceImpl

我们来看临时实例的一致性实现:DistroConsistencyServiceImpl类的put方法:

  1. public void put(String key, Record value) throws NacosException {
  2. // 先将要更新的实例信息写入本地实例列表
  3. onPut(key, value);
  4. // 开始集群同步
  5. distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
  6. globalConfig.getTaskDispatchPeriod() / 2);
  7. }

这里方法只有两行:

  • onPut(key, value):其中value就是Instances,要更新的服务信息。这行主要是基于线程池方式,异步的将Service信息写入注册表中(就是那个多重Map)
  • distroProtocol.sync():就是通过Distro协议将数据同步给集群中的其它Nacos节点

我们先看onPut方法

2.3.4.1.更新本地实例列表

1)放入阻塞队列

onPut方法如下:

  1. public void onPut(String key, Record value) {
  2. // 判断是否是临时实例
  3. if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
  4. // 封装 Instances 信息到 数据集:Datum
  5. Datum<Instances> datum = new Datum<>();
  6. datum.value = (Instances) value;
  7. datum.key = key;
  8. datum.timestamp.incrementAndGet();
  9. // 放入DataStore
  10. dataStore.put(key, datum);
  11. }
  12. if (!listeners.containsKey(key)) {
  13. return;
  14. }
  15. // 放入阻塞队列,这里的 notifier维护了一个阻塞队列,并且基于线程池异步执行队列中的任务
  16. notifier.addTask(key, DataOperation.CHANGE);
  17. }

notifier的类型就是DistroConsistencyServiceImpl.Notifier,内部维护了一个阻塞队列,存放服务列表变更的事件:

Nacos源码分析 - 图41

addTask时,将任务加入该阻塞队列:

  1. // DistroConsistencyServiceImpl.Notifier类的 addTask 方法:
  2. public void addTask(String datumKey, DataOperation action) {
  3. if (services.containsKey(datumKey) && action == DataOperation.CHANGE) {
  4. return;
  5. }
  6. if (action == DataOperation.CHANGE) {
  7. services.put(datumKey, StringUtils.EMPTY);
  8. }
  9. // 任务放入阻塞队列
  10. tasks.offer(Pair.with(datumKey, action));
  11. }

2)Notifier异步更新

同时,notifier还是一个Runnable,通过一个单线程的线程池来不断从阻塞队列中获取任务,执行服务列表的更新。来看下其中的run方法:

  1. // DistroConsistencyServiceImpl.Notifier类的run方法:
  2. @Override
  3. public void run() {
  4. Loggers.DISTRO.info("distro notifier started");
  5. // 死循环,不断执行任务。因为是阻塞队列,不会导致CPU负载过高
  6. for (; ; ) {
  7. try {
  8. // 从阻塞队列中获取任务
  9. Pair<String, DataOperation> pair = tasks.take();
  10. // 处理任务,更新服务列表
  11. handle(pair);
  12. } catch (Throwable e) {
  13. Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
  14. }
  15. }
  16. }

来看看handle方法:

  1. // DistroConsistencyServiceImpl.Notifier类的 handle 方法:
  2. private void handle(Pair<String, DataOperation> pair) {
  3. try {
  4. String datumKey = pair.getValue0();
  5. DataOperation action = pair.getValue1();
  6. services.remove(datumKey);
  7. int count = 0;
  8. if (!listeners.containsKey(datumKey)) {
  9. return;
  10. }
  11. // 遍历,找到变化的service,这里的 RecordListener就是 Service
  12. for (RecordListener listener : listeners.get(datumKey)) {
  13. count++;
  14. try {
  15. // 服务的实例列表CHANGE事件
  16. if (action == DataOperation.CHANGE) {
  17. // 更新服务列表
  18. listener.onChange(datumKey, dataStore.get(datumKey).value);
  19. continue;
  20. }
  21. // 服务的实例列表 DELETE 事件
  22. if (action == DataOperation.DELETE) {
  23. listener.onDelete(datumKey);
  24. continue;
  25. }
  26. } catch (Throwable e) {
  27. Loggers.DISTRO.error("[NACOS-DISTRO] error while notifying listener of key: {}", datumKey, e);
  28. }
  29. }
  30. if (Loggers.DISTRO.isDebugEnabled()) {
  31. Loggers.DISTRO
  32. .debug("[NACOS-DISTRO] datum change notified, key: {}, listener count: {}, action: {}",
  33. datumKey, count, action.name());
  34. }
  35. } catch (Throwable e) {
  36. Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
  37. }
  38. }

3)覆盖实例列表

而在Service的onChange方法中,就可以看到更新实例列表的逻辑了:

  1. @Override
  2. public void onChange(String key, Instances value) throws Exception {
  3. Loggers.SRV_LOG.info("[NACOS-RAFT] datum is changed, key: {}, value: {}", key, value);
  4. // 更新实例列表
  5. updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key));
  6. recalculateChecksum();
  7. }

updateIPs方法:

  1. public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
  2. // 准备一个Map,key是cluster,值是集群下的Instance集合
  3. Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
  4. // 获取服务的所有cluster名称
  5. for (String clusterName : clusterMap.keySet()) {
  6. ipMap.put(clusterName, new ArrayList<>());
  7. }
  8. // 遍历要更新的实例
  9. for (Instance instance : instances) {
  10. try {
  11. if (instance == null) {
  12. Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");
  13. continue;
  14. }
  15. // 判断实例是否包含clusterName,没有的话用默认cluster
  16. if (StringUtils.isEmpty(instance.getClusterName())) {
  17. instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
  18. }
  19. // 判断cluster是否存在,不存在则创建新的cluster
  20. if (!clusterMap.containsKey(instance.getClusterName())) {
  21. Loggers.SRV_LOG
  22. .warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
  23. instance.getClusterName(), instance.toJson());
  24. Cluster cluster = new Cluster(instance.getClusterName(), this);
  25. cluster.init();
  26. getClusterMap().put(instance.getClusterName(), cluster);
  27. }
  28. // 获取当前cluster实例的集合,不存在则创建新的
  29. List<Instance> clusterIPs = ipMap.get(instance.getClusterName());
  30. if (clusterIPs == null) {
  31. clusterIPs = new LinkedList<>();
  32. ipMap.put(instance.getClusterName(), clusterIPs);
  33. }
  34. // 添加新的实例到 Instance 集合
  35. clusterIPs.add(instance);
  36. } catch (Exception e) {
  37. Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);
  38. }
  39. }
  40. for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
  41. //make every ip mine
  42. List<Instance> entryIPs = entry.getValue();
  43. // 将实例集合更新到 clusterMap(注册表)
  44. clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
  45. }
  46. setLastModifiedMillis(System.currentTimeMillis());
  47. // 发布服务变更的通知消息
  48. getPushService().serviceChanged(this);
  49. StringBuilder stringBuilder = new StringBuilder();
  50. for (Instance instance : allIPs()) {
  51. stringBuilder.append(instance.toIpAddr()).append("_").append(instance.isHealthy()).append(",");
  52. }
  53. Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}", getNamespaceId(), getName(),
  54. stringBuilder.toString());
  55. }

在第45行的代码中:clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);

就是在更新注册表:

  1. public void updateIps(List<Instance> ips, boolean ephemeral) {
  2. // 获取旧实例列表
  3. Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;
  4. HashMap<String, Instance> oldIpMap = new HashMap<>(toUpdateInstances.size());
  5. for (Instance ip : toUpdateInstances) {
  6. oldIpMap.put(ip.getDatumKey(), ip);
  7. }
  8. // 检查新加入实例的状态
  9. List<Instance> newIPs = subtract(ips, oldIpMap.values());
  10. if (newIPs.size() > 0) {
  11. Loggers.EVT_LOG
  12. .info("{} {SYNC} {IP-NEW} cluster: {}, new ips size: {}, content: {}", getService().getName(),
  13. getName(), newIPs.size(), newIPs.toString());
  14. for (Instance ip : newIPs) {
  15. HealthCheckStatus.reset(ip);
  16. }
  17. }
  18. // 移除要删除的实例
  19. List<Instance> deadIPs = subtract(oldIpMap.values(), ips);
  20. if (deadIPs.size() > 0) {
  21. Loggers.EVT_LOG
  22. .info("{} {SYNC} {IP-DEAD} cluster: {}, dead ips size: {}, content: {}", getService().getName(),
  23. getName(), deadIPs.size(), deadIPs.toString());
  24. for (Instance ip : deadIPs) {
  25. HealthCheckStatus.remv(ip);
  26. }
  27. }
  28. toUpdateInstances = new HashSet<>(ips);
  29. // 直接覆盖旧实例列表
  30. if (ephemeral) {
  31. ephemeralInstances = toUpdateInstances;
  32. } else {
  33. persistentInstances = toUpdateInstances;
  34. }
  35. }

2.3.4.2.集群数据同步

在DistroConsistencyServiceImpl的put方法中分为两步:

Nacos源码分析 - 图42

其中的onPut方法已经分析过了。

下面的distroProtocol.sync()就是集群同步的逻辑了。

DistroProtocol类的sync方法如下:

  1. public void sync(DistroKey distroKey, DataOperation action, long delay) {
  2. // 遍历 Nacos 集群中除自己以外的其它节点
  3. for (Member each : memberManager.allMembersWithoutSelf()) {
  4. DistroKey distroKeyWithTarget = new DistroKey(distroKey.getResourceKey(), distroKey.getResourceType(),
  5. each.getAddress());
  6. // 定义一个Distro的同步任务
  7. DistroDelayTask distroDelayTask = new DistroDelayTask(distroKeyWithTarget, action, delay);
  8. // 交给线程池去执行
  9. distroTaskEngineHolder.getDelayTaskExecuteEngine().addTask(distroKeyWithTarget, distroDelayTask);
  10. if (Loggers.DISTRO.isDebugEnabled()) {
  11. Loggers.DISTRO.debug("[DISTRO-SCHEDULE] {} to {}", distroKey, each.getAddress());
  12. }
  13. }
  14. }

其中同步的任务封装为一个DistroDelayTask对象。

交给了distroTaskEngineHolder.getDelayTaskExecuteEngine()执行,这行代码的返回值是:

NacosDelayTaskExecuteEngine,这个类维护了一个线程池,并且接收任务,执行任务。

执行任务的方法为processTasks()方法:

  1. protected void processTasks() {
  2. Collection<Object> keys = getAllTaskKeys();
  3. for (Object taskKey : keys) {
  4. AbstractDelayTask task = removeTask(taskKey);
  5. if (null == task) {
  6. continue;
  7. }
  8. NacosTaskProcessor processor = getProcessor(taskKey);
  9. if (null == processor) {
  10. getEngineLog().error("processor not found for task, so discarded. " + task);
  11. continue;
  12. }
  13. try {
  14. // 尝试执行同步任务,如果失败会重试
  15. if (!processor.process(task)) {
  16. retryFailedTask(taskKey, task);
  17. }
  18. } catch (Throwable e) {
  19. getEngineLog().error("Nacos task execute error : " + e.toString(), e);
  20. retryFailedTask(taskKey, task);
  21. }
  22. }
  23. }

可以看出来基于Distro模式的同步是异步进行的,并且失败时会将任务重新入队并充实,因此不保证同步结果的强一致性,属于AP模式的一致性策略。

2.3.5.服务端流程图

Nacos源码分析 - 图43

2.4.总结

  • Nacos的注册表结构是什么样的?

    • 答:Nacos是多级存储模型,最外层通过namespace来实现环境隔离,然后是group分组,分组下就是服务,一个服务有可以分为不同的集群,集群中包含多个实例。因此其注册表结构为一个Map,类型是:
      Map<String, Map<String, Service>>,
      外层key是namespace_id,内层key是group+serviceName.
      Service内部维护一个Map,结构是:Map<String,Cluster>,key是clusterName,值是集群信息
      Cluster内部维护一个Set集合,元素是Instance类型,代表集群中的多个实例。
  • Nacos如何保证并发写的安全性?

    • 答:首先,在注册实例时,会对service加锁,不同service之间本身就不存在并发写问题,互不影响。相同service时通过锁来互斥。并且,在更新实例列表时,是基于异步的线程池来完成,而线程池的线程数量为1.
  • Nacos如何避免并发读写的冲突?

    • 答:Nacos在更新实例列表时,会采用CopyOnWrite技术,首先将Old实例列表拷贝一份,然后更新拷贝的实例列表,再用更新后的实例列表来覆盖旧的实例列表。
  • Nacos如何应对阿里内部数十万服务的并发写请求?

    • 答:Nacos内部会将服务注册的任务放入阻塞队列,采用线程池异步来完成实例更新,从而提高并发写能力。

3.服务心跳

Nacos的实例分为临时实例和永久实例两种,可以通过在yaml 文件配置:

  1. spring:
  2. application:
  3. name: order-service
  4. cloud:
  5. nacos:
  6. discovery:
  7. ephemeral: false # 设置实例为永久实例。true:临时; false:永久
  8. server-addr: 192.168.150.1:8845

临时实例基于心跳方式做健康检测,而永久实例则是由Nacos主动探测实例状态。

其中Nacos提供的心跳的API接口为:

接口描述:发送某个实例的心跳

请求类型:PUT

请求路径:

  1. /nacos/v1/ns/instance/beat

请求参数:

名称 类型 是否必选 描述
serviceName 字符串 是 服务名
groupName 字符串 否 分组名
ephemeral boolean 否 是否临时实例
beat JSON格式字符串 是 实例心跳内容

错误编码:

错误代码 描述 语义
400 Bad Request 客户端请求中的语法错误
403 Forbidden 没有权限
404 Not Found 无法找到资源
500 Internal Server Error 服务器内部错误
200 OK 正常

3.1.客户端

在2.2.4.服务注册这一节中,我们说过NacosNamingService这个类实现了服务的注册,同时也实现了服务心跳:

  1. @Override
  2. public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
  3. NamingUtils.checkInstanceIsLegal(instance);
  4. String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
  5. // 判断是否是临时实例。
  6. if (instance.isEphemeral()) {
  7. // 如果是临时实例,则构建心跳信息BeatInfo
  8. BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
  9. // 添加心跳任务
  10. beatReactor.addBeatInfo(groupedServiceName, beatInfo);
  11. }
  12. serverProxy.registerService(groupedServiceName, groupName, instance);
  13. }

3.1.1.BeatInfo

这里的BeanInfo就包含心跳需要的各种信息:

Nacos源码分析 - 图44

3.1.2.BeatReactor

而BeatReactor这个类则维护了一个线程池:

Nacos源码分析 - 图45

当调用BeatReactor的.addBeatInfo(groupedServiceName, beatInfo)方法时,就会执行心跳:

  1. public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
  2. NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
  3. String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
  4. BeatInfo existBeat = null;
  5. //fix #1733
  6. if ((existBeat = dom2Beat.remove(key)) != null) {
  7. existBeat.setStopped(true);
  8. }
  9. dom2Beat.put(key, beatInfo);
  10. // 利用线程池,定期执行心跳任务,周期为 beatInfo.getPeriod()
  11. executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
  12. MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
  13. }

心跳周期的默认值在com.alibaba.nacos.api.common.Constants类中:

Nacos源码分析 - 图46

可以看到是5秒,默认5秒一次心跳。

3.1.3.BeatTask

心跳的任务封装在BeatTask这个类中,是一个Runnable,其run方法如下:

  1. @Override
  2. public void run() {
  3. if (beatInfo.isStopped()) {
  4. return;
  5. }
  6. // 获取心跳周期
  7. long nextTime = beatInfo.getPeriod();
  8. try {
  9. // 发送心跳
  10. JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
  11. long interval = result.get("clientBeatInterval").asLong();
  12. boolean lightBeatEnabled = false;
  13. if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
  14. lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
  15. }
  16. BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
  17. if (interval > 0) {
  18. nextTime = interval;
  19. }
  20. // 判断心跳结果
  21. int code = NamingResponseCode.OK;
  22. if (result.has(CommonParams.CODE)) {
  23. code = result.get(CommonParams.CODE).asInt();
  24. }
  25. if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
  26. // 如果失败,则需要 重新注册实例
  27. Instance instance = new Instance();
  28. instance.setPort(beatInfo.getPort());
  29. instance.setIp(beatInfo.getIp());
  30. instance.setWeight(beatInfo.getWeight());
  31. instance.setMetadata(beatInfo.getMetadata());
  32. instance.setClusterName(beatInfo.getCluster());
  33. instance.setServiceName(beatInfo.getServiceName());
  34. instance.setInstanceId(instance.getInstanceId());
  35. instance.setEphemeral(true);
  36. try {
  37. serverProxy.registerService(beatInfo.getServiceName(),
  38. NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
  39. } catch (Exception ignore) {
  40. }
  41. }
  42. } catch (NacosException ex) {
  43. NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
  44. JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());
  45. } catch (Exception unknownEx) {
  46. NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, unknown exception msg: {}",
  47. JacksonUtils.toJson(beatInfo), unknownEx.getMessage(), unknownEx);
  48. } finally {
  49. executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
  50. }
  51. }

3.1.5.发送心跳

最终心跳的发送还是通过NamingProxy的sendBeat方法来实现:

  1. public JsonNode sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws NacosException {
  2. if (NAMING_LOGGER.isDebugEnabled()) {
  3. NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", namespaceId, beatInfo.toString());
  4. }
  5. // 组织请求参数
  6. Map<String, String> params = new HashMap<String, String>(8);
  7. Map<String, String> bodyMap = new HashMap<String, String>(2);
  8. if (!lightBeatEnabled) {
  9. bodyMap.put("beat", JacksonUtils.toJson(beatInfo));
  10. }
  11. params.put(CommonParams.NAMESPACE_ID, namespaceId);
  12. params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
  13. params.put(CommonParams.CLUSTER_NAME, beatInfo.getCluster());
  14. params.put("ip", beatInfo.getIp());
  15. params.put("port", String.valueOf(beatInfo.getPort()));
  16. // 发送请求,这个地址就是:/v1/ns/instance/beat
  17. String result = reqApi(UtilAndComs.nacosUrlBase + "/instance/beat", params, bodyMap, HttpMethod.PUT);
  18. return JacksonUtils.toObj(result);
  19. }

3.2.服务端

对于临时实例,服务端代码分两部分:

  • 1)InstanceController提供了一个接口,处理客户端的心跳请求
  • 2)定时检测实例心跳是否按期执行

3.2.1.InstanceController

与服务注册时一样,在nacos-naming模块中的InstanceController类中,定义了一个方法用来处理心跳请求:

  1. @CanDistro
  2. @PutMapping("/beat")
  3. @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
  4. public ObjectNode beat(HttpServletRequest request) throws Exception {
  5. // 解析心跳的请求参数
  6. ObjectNode result = JacksonUtils.createEmptyJsonNode();
  7. result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, switchDomain.getClientBeatInterval());
  8. String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY);
  9. RsInfo clientBeat = null;
  10. if (StringUtils.isNotBlank(beat)) {
  11. clientBeat = JacksonUtils.toObj(beat, RsInfo.class);
  12. }
  13. String clusterName = WebUtils
  14. .optional(request, CommonParams.CLUSTER_NAME, UtilsAndCommons.DEFAULT_CLUSTER_NAME);
  15. String ip = WebUtils.optional(request, "ip", StringUtils.EMPTY);
  16. int port = Integer.parseInt(WebUtils.optional(request, "port", "0"));
  17. if (clientBeat != null) {
  18. if (StringUtils.isNotBlank(clientBeat.getCluster())) {
  19. clusterName = clientBeat.getCluster();
  20. } else {
  21. // fix #2533
  22. clientBeat.setCluster(clusterName);
  23. }
  24. ip = clientBeat.getIp();
  25. port = clientBeat.getPort();
  26. }
  27. String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
  28. String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
  29. NamingUtils.checkServiceNameFormat(serviceName);
  30. Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}", clientBeat, serviceName);
  31. // 尝试根据参数中的namespaceId、serviceName、clusterName、ip、port等信息
  32. // 从Nacos的注册表中 获取实例
  33. Instance instance = serviceManager.getInstance(namespaceId, serviceName, clusterName, ip, port);
  34. // 如果获取失败,说明心跳失败,实例尚未注册
  35. if (instance == null) {
  36. if (clientBeat == null) {
  37. result.put(CommonParams.CODE, NamingResponseCode.RESOURCE_NOT_FOUND);
  38. return result;
  39. }
  40. Loggers.SRV_LOG.warn("[CLIENT-BEAT] The instance has been removed for health mechanism, "
  41. + "perform data compensation operations, beat: {}, serviceName: {}", clientBeat, serviceName);
  42. // 这里重新注册一个实例
  43. instance = new Instance();
  44. instance.setPort(clientBeat.getPort());
  45. instance.setIp(clientBeat.getIp());
  46. instance.setWeight(clientBeat.getWeight());
  47. instance.setMetadata(clientBeat.getMetadata());
  48. instance.setClusterName(clusterName);
  49. instance.setServiceName(serviceName);
  50. instance.setInstanceId(instance.getInstanceId());
  51. instance.setEphemeral(clientBeat.isEphemeral());
  52. serviceManager.registerInstance(namespaceId, serviceName, instance);
  53. }
  54. // 尝试基于namespaceId和serviceName从 注册表中获取Service服务
  55. Service service = serviceManager.getService(namespaceId, serviceName);
  56. // 如果不存在,说明服务不存在,返回404
  57. if (service == null) {
  58. throw new NacosException(NacosException.SERVER_ERROR,
  59. "service not found: " + serviceName + "@" + namespaceId);
  60. }
  61. if (clientBeat == null) {
  62. clientBeat = new RsInfo();
  63. clientBeat.setIp(ip);
  64. clientBeat.setPort(port);
  65. clientBeat.setCluster(clusterName);
  66. }
  67. // 如果心跳没问题,开始处理心跳结果
  68. service.processClientBeat(clientBeat);
  69. result.put(CommonParams.CODE, NamingResponseCode.OK);
  70. if (instance.containsMetadata(PreservedMetadataKeys.HEART_BEAT_INTERVAL)) {
  71. result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, instance.getInstanceHeartBeatInterval());
  72. }
  73. result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());
  74. return result;
  75. }

最终,在确认心跳请求对应的服务、实例都在的情况下,开始交给Service类处理这次心跳请求。调用了Service的processClientBeat方法

3.2.2.处理心跳请求

查看Service的service.processClientBeat(clientBeat);方法:

  1. public void processClientBeat(final RsInfo rsInfo) {
  2. ClientBeatProcessor clientBeatProcessor = new ClientBeatProcessor();
  3. clientBeatProcessor.setService(this);
  4. clientBeatProcessor.setRsInfo(rsInfo);
  5. HealthCheckReactor.scheduleNow(clientBeatProcessor);
  6. }

可以看到心跳信息被封装到了 ClientBeatProcessor类中,交给了HealthCheckReactor处理,HealthCheckReactor就是对线程池的封装,不用过多查看。

关键的业务逻辑都在ClientBeatProcessor这个类中,它是一个Runnable,其中的run方法如下:

  1. @Override
  2. public void run() {
  3. Service service = this.service;
  4. if (Loggers.EVT_LOG.isDebugEnabled()) {
  5. Loggers.EVT_LOG.debug("[CLIENT-BEAT] processing beat: {}", rsInfo.toString());
  6. }
  7. String ip = rsInfo.getIp();
  8. String clusterName = rsInfo.getCluster();
  9. int port = rsInfo.getPort();
  10. // 获取集群信息
  11. Cluster cluster = service.getClusterMap().get(clusterName);
  12. // 获取集群中的所有实例信息
  13. List<Instance> instances = cluster.allIPs(true);
  14. for (Instance instance : instances) {
  15. // 找到心跳的这个实例
  16. if (instance.getIp().equals(ip) && instance.getPort() == port) {
  17. if (Loggers.EVT_LOG.isDebugEnabled()) {
  18. Loggers.EVT_LOG.debug("[CLIENT-BEAT] refresh beat: {}", rsInfo.toString());
  19. }
  20. // 更新实例的最后一次心跳时间 lastBeat
  21. instance.setLastBeat(System.currentTimeMillis());
  22. if (!instance.isMarked()) {
  23. if (!instance.isHealthy()) {
  24. instance.setHealthy(true);
  25. Loggers.EVT_LOG
  26. .info("service: {} {POS} {IP-ENABLED} valid: {}:{}@{}, region: {}, msg: client beat ok",
  27. cluster.getService().getName(), ip, port, cluster.getName(),
  28. UtilsAndCommons.LOCALHOST_SITE);
  29. getPushService().serviceChanged(service);
  30. }
  31. }
  32. }
  33. }
  34. }

处理心跳请求的核心就是更新心跳实例的最后一次心跳时间,lastBeat,这个会成为判断实例心跳是否过期的关键指标!

3.3.3.心跳异常检测

在服务注册时,一定会创建一个Service对象,而Service中有一个init方法,会在注册时被调用:

  1. public void init() {
  2. // 开启心跳检测的任务
  3. HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
  4. for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
  5. entry.getValue().setService(this);
  6. entry.getValue().init();
  7. }
  8. }

其中HealthCheckReactor.scheduleCheck就是执行心跳检测的定时任务:

Nacos源码分析 - 图47

可以看到,该任务是5000ms执行一次,也就是5秒对实例的心跳状态做一次检测。

此处的ClientBeatCheckTask同样是一个Runnable,其中的run方法为:

  1. @Override
  2. public void run() {
  3. try {
  4. // 找到所有临时实例的列表
  5. List<Instance> instances = service.allIPs(true);
  6. // first set health status of instances:
  7. for (Instance instance : instances) {
  8. // 判断 心跳间隔(当前时间 - 最后一次心跳时间) 是否大于 心跳超时时间,默认15秒
  9. if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {
  10. if (!instance.isMarked()) {
  11. if (instance.isHealthy()) {
  12. // 如果超时,标记实例为不健康 healthy = false
  13. instance.setHealthy(false);
  14. // 发布实例状态变更的事件
  15. getPushService().serviceChanged(service);
  16. ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
  17. }
  18. }
  19. }
  20. }
  21. if (!getGlobalConfig().isExpireInstance()) {
  22. return;
  23. }
  24. // then remove obsolete instances:
  25. for (Instance instance : instances) {
  26. if (instance.isMarked()) {
  27. continue;
  28. }
  29. // 判断心跳间隔(当前时间 - 最后一次心跳时间)是否大于 实例被删除的最长超时时间,默认30秒
  30. if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {
  31. // 如果是超过了30秒,则删除实例
  32. Loggers.SRV_LOG.info("[AUTO-DELETE-IP] service: {}, ip: {}", service.getName(),
  33. JacksonUtils.toJson(instance));
  34. deleteIp(instance);
  35. }
  36. }
  37. } catch (Exception e) {
  38. Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);
  39. }
  40. }

其中的超时时间同样是在com.alibaba.nacos.api.common.Constants这个类中:

Nacos源码分析 - 图48

3.3.4.主动健康检测

对于非临时实例(ephemeral=false),Nacos会采用主动的健康检测,定时向实例发送请求,根据响应来判断实例健康状态。

入口在2.3.2小节的ServiceManager类中的registerInstance方法:

Nacos源码分析 - 图49

创建空服务时:

  1. public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException {
  2. // 如果服务不存在,创建新的服务
  3. createServiceIfAbsent(namespaceId, serviceName, local, null);
  4. }

创建服务流程:

  1. public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
  2. throws NacosException {
  3. // 尝试获取服务
  4. Service service = getService(namespaceId, serviceName);
  5. if (service == null) {
  6. // 发现服务不存在,开始创建新服务
  7. Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
  8. service = new Service();
  9. service.setName(serviceName);
  10. service.setNamespaceId(namespaceId);
  11. service.setGroupName(NamingUtils.getGroupName(serviceName));
  12. // now validate the service. if failed, exception will be thrown
  13. service.setLastModifiedMillis(System.currentTimeMillis());
  14. service.recalculateChecksum();
  15. if (cluster != null) {
  16. cluster.setService(service);
  17. service.getClusterMap().put(cluster.getName(), cluster);
  18. }
  19. service.validate();
  20. // ** 写入注册表并初始化 **
  21. putServiceAndInit(service);
  22. if (!local) {
  23. addOrReplaceService(service);
  24. }
  25. }
  26. }

关键在putServiceAndInit(service)方法中:

  1. private void putServiceAndInit(Service service) throws NacosException {
  2. // 将服务写入注册表
  3. putService(service);
  4. service = getService(service.getNamespaceId(), service.getName());
  5. // 完成服务的初始化
  6. service.init();
  7. consistencyService
  8. .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
  9. consistencyService
  10. .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
  11. Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
  12. }

进入初始化逻辑:service.init(),这个会进入Service类中:

  1. /**
  2. * Init service.
  3. */
  4. public void init() {
  5. // 开启临时实例的心跳监测任务
  6. HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
  7. // 遍历注册表中的集群
  8. for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
  9. entry.getValue().setService(this);
  10. // 完成集群初识化
  11. entry.getValue().init();
  12. }
  13. }

这里集群的初始化entry.getValue().init();会进入Cluster类型的init()方法:

  1. /**
  2. * Init cluster.
  3. */
  4. public void init() {
  5. if (inited) {
  6. return;
  7. }
  8. // 创建健康检测的任务
  9. checkTask = new HealthCheckTask(this);
  10. // 这里会开启对 非临时实例的 定时健康检测
  11. HealthCheckReactor.scheduleCheck(checkTask);
  12. inited = true;
  13. }

这里的HealthCheckReactor.scheduleCheck(checkTask);会开启定时任务,对非临时实例做健康检测。检测逻辑定义在HealthCheckTask这个类中,是一个Runnable,其中的run方法:

  1. public void run() {
  2. try {
  3. if (distroMapper.responsible(cluster.getService().getName()) && switchDomain
  4. .isHealthCheckEnabled(cluster.getService().getName())) {
  5. // 开始健康检测
  6. healthCheckProcessor.process(this);
  7. // 记录日志 。。。
  8. }
  9. } catch (Throwable e) {
  10. // 记录日志 。。。
  11. } finally {
  12. if (!cancelled) {
  13. // 结束后,再次进行任务调度,一定延迟后执行
  14. HealthCheckReactor.scheduleCheck(this);
  15. // 。。。
  16. }
  17. }
  18. }

健康检测逻辑定义在healthCheckProcessor.process(this);方法中,在HealthCheckProcessor接口中,这个接口也有很多实现,默认是TcpSuperSenseProcessor:

Nacos源码分析 - 图50

进入TcpSuperSenseProcessor的process方法:

  1. @Override
  2. public void process(HealthCheckTask task) {
  3. // 获取所有 非临时实例的 集合
  4. List<Instance> ips = task.getCluster().allIPs(false);
  5. if (CollectionUtils.isEmpty(ips)) {
  6. return;
  7. }
  8. for (Instance ip : ips) {
  9. // 封装健康检测信息到 Beat
  10. Beat beat = new Beat(ip, task);
  11. // 放入一个阻塞队列中
  12. taskQueue.add(beat);
  13. MetricsMonitor.getTcpHealthCheckMonitor().incrementAndGet();
  14. }
  15. }

可以看到,所有的健康检测任务都被放入一个阻塞队列,而不是立即执行了。这里又采用了异步执行的策略,可以看到Nacos中大量这样的设计。

而TcpSuperSenseProcessor本身就是一个Runnable,在它的构造函数中会把自己放入线程池中去执行,其run方法如下:

  1. public void run() {
  2. while (true) {
  3. try {
  4. // 处理任务
  5. processTask();
  6. // ...
  7. } catch (Throwable e) {
  8. SRV_LOG.error("[HEALTH-CHECK] error while processing NIO task", e);
  9. }
  10. }
  11. }

通过processTask来处理健康检测的任务:

  1. private void processTask() throws Exception {
  2. // 将任务封装为一个 TaskProcessor,并放入集合
  3. Collection<Callable<Void>> tasks = new LinkedList<>();
  4. do {
  5. Beat beat = taskQueue.poll(CONNECT_TIMEOUT_MS / 2, TimeUnit.MILLISECONDS);
  6. if (beat == null) {
  7. return;
  8. }
  9. tasks.add(new TaskProcessor(beat));
  10. } while (taskQueue.size() > 0 && tasks.size() < NIO_THREAD_COUNT * 64);
  11. // 批量处理集合中的任务
  12. for (Future<?> f : GlobalExecutor.invokeAllTcpSuperSenseTask(tasks)) {
  13. f.get();
  14. }
  15. }

任务被封装到了TaskProcessor中去执行了,TaskProcessor是一个Callable,其中的call方法:

  1. @Override
  2. public Void call() {
  3. // 获取检测任务已经等待的时长
  4. long waited = System.currentTimeMillis() - beat.getStartTime();
  5. if (waited > MAX_WAIT_TIME_MILLISECONDS) {
  6. Loggers.SRV_LOG.warn("beat task waited too long: " + waited + "ms");
  7. }
  8. SocketChannel channel = null;
  9. try {
  10. // 获取实例信息
  11. Instance instance = beat.getIp();
  12. // 通过NIO建立TCP连接
  13. channel = SocketChannel.open();
  14. channel.configureBlocking(false);
  15. // only by setting this can we make the socket close event asynchronous
  16. channel.socket().setSoLinger(false, -1);
  17. channel.socket().setReuseAddress(true);
  18. channel.socket().setKeepAlive(true);
  19. channel.socket().setTcpNoDelay(true);
  20. Cluster cluster = beat.getTask().getCluster();
  21. int port = cluster.isUseIPPort4Check() ? instance.getPort() : cluster.getDefCkport();
  22. channel.connect(new InetSocketAddress(instance.getIp(), port));
  23. // 注册连接、读取事件
  24. SelectionKey key = channel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
  25. key.attach(beat);
  26. keyMap.put(beat.toString(), new BeatKey(key));
  27. beat.setStartTime(System.currentTimeMillis());
  28. GlobalExecutor
  29. .scheduleTcpSuperSenseTask(new TimeOutTask(key), CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
  30. } catch (Exception e) {
  31. beat.finishCheck(false, false, switchDomain.getTcpHealthParams().getMax(),
  32. "tcp:error:" + e.getMessage());
  33. if (channel != null) {
  34. try {
  35. channel.close();
  36. } catch (Exception ignore) {
  37. }
  38. }
  39. }
  40. return null;
  41. }

3.3.总结

Nacos的健康检测有两种模式:

  • 临时实例:

    • 采用客户端心跳检测模式,心跳周期5秒
    • 心跳间隔超过15秒则标记为不健康
    • 心跳间隔超过30秒则从服务列表删除
  • 永久实例:

    • 采用服务端主动健康检测方式
    • 周期为2000 + 5000毫秒内的随机数
    • 检测异常只会标记为不健康,不会删除

那么为什么Nacos有临时和永久两种实例呢?

以淘宝为例,双十一大促期间,流量会比平常高出很多,此时服务肯定需要增加更多实例来应对高并发,而这些实例在双十一之后就无需继续使用了,采用临时实例比较合适。而对于服务的一些常备实例,则使用永久实例更合适。

与eureka相比,Nacos与Eureka在临时实例上都是基于心跳模式实现,差别不大,主要是心跳周期不同,eureka是30秒,Nacos是5秒。

另外,Nacos支持永久实例,而Eureka不支持,Eureka只提供了心跳模式的健康监测,而没有主动检测功能。

4.服务发现

Nacos提供了一个根据serviceId查询实例列表的接口:

接口描述:查询服务下的实例列表

请求类型:GET

请求路径:

  1. /nacos/v1/ns/instance/list

请求参数:

名称 类型 是否必选 描述
serviceName 字符串 是 服务名
groupName 字符串 否 分组名
namespaceId 字符串 否 命名空间ID
clusters 字符串,多个集群用逗号分隔 否 集群名称
healthyOnly boolean 否,默认为false 是否只返回健康实例

错误编码:

错误代码 描述 语义
400 Bad Request 客户端请求中的语法错误
403 Forbidden 没有权限
404 Not Found 无法找到资源
500 Internal Server Error 服务器内部错误
200 OK 正常

4.1.客户端

4.1.1.定时更新服务列表

4.1.1.1.NacosNamingService

在2.2.4小节中,我们讲到一个类NacosNamingService,这个类不仅仅提供了服务注册功能,同样提供了服务发现的功能。

Nacos源码分析 - 图51

多个重载的方法最终都会进入一个方法:

  1. @Override
  2. public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters,
  3. boolean subscribe) throws NacosException {
  4. ServiceInfo serviceInfo;
  5. // 1.判断是否需要订阅服务信息(默认为 true)
  6. if (subscribe) {
  7. // 1.1.订阅服务信息
  8. serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
  9. StringUtils.join(clusters, ","));
  10. } else {
  11. // 1.2.直接去nacos拉取服务信息
  12. serviceInfo = hostReactor
  13. .getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
  14. StringUtils.join(clusters, ","));
  15. }
  16. // 2.从服务信息中获取实例列表并返回
  17. List<Instance> list;
  18. if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
  19. return new ArrayList<Instance>();
  20. }
  21. return list;
  22. }

4.1.1.2.HostReactor

进入1.1.订阅服务消息,这里是由HostReactor类的getServiceInfo()方法来实现的:

  1. public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
  2. NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
  3. // 由 服务名@@集群名拼接 key
  4. String key = ServiceInfo.getKey(serviceName, clusters);
  5. if (failoverReactor.isFailoverSwitch()) {
  6. return failoverReactor.getService(key);
  7. }
  8. // 读取本地服务列表的缓存,缓存是一个Map,格式:Map<String, ServiceInfo>
  9. ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
  10. // 判断缓存是否存在
  11. if (null == serviceObj) {
  12. // 不存在,创建空ServiceInfo
  13. serviceObj = new ServiceInfo(serviceName, clusters);
  14. // 放入缓存
  15. serviceInfoMap.put(serviceObj.getKey(), serviceObj);
  16. // 放入待更新的服务列表(updatingMap)中
  17. updatingMap.put(serviceName, new Object());
  18. // 立即更新服务列表
  19. updateServiceNow(serviceName, clusters);
  20. // 从待更新列表中移除
  21. updatingMap.remove(serviceName);
  22. } else if (updatingMap.containsKey(serviceName)) {
  23. // 缓存中有,但是需要更新
  24. if (UPDATE_HOLD_INTERVAL > 0) {
  25. // hold a moment waiting for update finish 等待5秒中,待更新完成
  26. synchronized (serviceObj) {
  27. try {
  28. serviceObj.wait(UPDATE_HOLD_INTERVAL);
  29. } catch (InterruptedException e) {
  30. NAMING_LOGGER
  31. .error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
  32. }
  33. }
  34. }
  35. }
  36. // 开启定时更新服务列表的功能
  37. scheduleUpdateIfAbsent(serviceName, clusters);
  38. // 返回缓存中的服务信息
  39. return serviceInfoMap.get(serviceObj.getKey());
  40. }

基本逻辑就是先从本地缓存读,根据结果来选择:

  • 如果本地缓存没有,立即去nacos读取,updateServiceNow(serviceName, clusters)
    Nacos源码分析 - 图52

  • 如果本地缓存有,则开启定时更新功能,并返回缓存结果:

    • scheduleUpdateIfAbsent(serviceName, clusters)

Nacos源码分析 - 图53

Nacos源码分析 - 图54 不管是立即更新服务列表,还是定时更新服务列表,最终都会执行HostReactor中的updateService()方法:

  1. public void updateService(String serviceName, String clusters) throws NacosException {
  2. ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
  3. try {
  4. // 基于ServerProxy发起远程调用,查询服务列表
  5. String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);
  6. if (StringUtils.isNotEmpty(result)) {
  7. // 处理查询结果
  8. processServiceJson(result);
  9. }
  10. } finally {
  11. if (oldService != null) {
  12. synchronized (oldService) {
  13. oldService.notifyAll();
  14. }
  15. }
  16. }
  17. }

4.1.1.3.ServerProxy

而ServerProxy的queryList方法如下:

  1. public String queryList(String serviceName, String clusters, int udpPort, boolean healthyOnly)
  2. throws NacosException {
  3. // 准备请求参数
  4. final Map<String, String> params = new HashMap<String, String>(8);
  5. params.put(CommonParams.NAMESPACE_ID, namespaceId);
  6. params.put(CommonParams.SERVICE_NAME, serviceName);
  7. params.put("clusters", clusters);
  8. params.put("udpPort", String.valueOf(udpPort));
  9. params.put("clientIP", NetUtils.localIP());
  10. params.put("healthyOnly", String.valueOf(healthyOnly));
  11. // 发起请求,地址与API接口一致
  12. return reqApi(UtilAndComs.nacosUrlBase + "/instance/list", params, HttpMethod.GET);
  13. }

4.1.2.处理服务变更通知

除了定时更新服务列表的功能外,Nacos还支持服务列表变更时的主动推送功能。

在HostReactor类的构造函数中,有非常重要的几个步骤:

Nacos源码分析 - 图55

基本思路是:

  • 通过PushReceiver监听服务端推送的变更数据
  • 解析数据后,通过NotifyCenter发布服务变更的事件
  • InstanceChangeNotifier监听变更事件,完成对服务列表的更新

4.1.2.1.PushReceiver

我们先看PushReceiver,这个类会以UDP方式接收Nacos服务端推送的服务变更数据。

先看构造函数:

  1. public PushReceiver(HostReactor hostReactor) {
  2. try {
  3. this.hostReactor = hostReactor;
  4. // 创建 UDP客户端
  5. String udpPort = getPushReceiverUdpPort();
  6. if (StringUtils.isEmpty(udpPort)) {
  7. this.udpSocket = new DatagramSocket();
  8. } else {
  9. this.udpSocket = new DatagramSocket(new InetSocketAddress(Integer.parseInt(udpPort)));
  10. }
  11. // 准备线程池
  12. this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
  13. @Override
  14. public Thread newThread(Runnable r) {
  15. Thread thread = new Thread(r);
  16. thread.setDaemon(true);
  17. thread.setName("com.alibaba.nacos.naming.push.receiver");
  18. return thread;
  19. }
  20. });
  21. // 开启线程任务,准备接收变更数据
  22. this.executorService.execute(this);
  23. } catch (Exception e) {
  24. NAMING_LOGGER.error("[NA] init udp socket failed", e);
  25. }
  26. }

PushReceiver构造函数中基于线程池来运行任务。这是因为PushReceiver本身也是一个Runnable,其中的run方法业务逻辑如下:

  1. @Override
  2. public void run() {
  3. while (!closed) {
  4. try {
  5. // byte[] is initialized with 0 full filled by default
  6. byte[] buffer = new byte[UDP_MSS];
  7. DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
  8. // 接收推送数据
  9. udpSocket.receive(packet);
  10. // 解析为json字符串
  11. String json = new String(IoUtils.tryDecompress(packet.getData()), UTF_8).trim();
  12. NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
  13. // 反序列化为对象
  14. PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);
  15. String ack;
  16. if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
  17. // 交给 HostReactor去处理
  18. hostReactor.processServiceJson(pushPacket.data);
  19. // send ack to server 发送ACK回执,略。。
  20. } catch (Exception e) {
  21. if (closed) {
  22. return;
  23. }
  24. NAMING_LOGGER.error("[NA] error while receiving push data", e);
  25. }
  26. }
  27. }

4.1.2.2.HostReactor

通知数据的处理由交给了HostReactor的processServiceJson方法:

  1. public ServiceInfo processServiceJson(String json) {
  2. // 解析出ServiceInfo信息
  3. ServiceInfo serviceInfo = JacksonUtils.toObj(json, ServiceInfo.class);
  4. String serviceKey = serviceInfo.getKey();
  5. if (serviceKey == null) {
  6. return null;
  7. }
  8. // 查询缓存中的 ServiceInfo
  9. ServiceInfo oldService = serviceInfoMap.get(serviceKey);
  10. // 如果缓存存在,则需要校验哪些数据要更新
  11. boolean changed = false;
  12. if (oldService != null) {
  13. // 拉取的数据是否已经过期
  14. if (oldService.getLastRefTime() > serviceInfo.getLastRefTime()) {
  15. NAMING_LOGGER.warn("out of date data received, old-t: " + oldService.getLastRefTime() + ", new-t: "
  16. + serviceInfo.getLastRefTime());
  17. }
  18. // 放入缓存
  19. serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
  20. // 中间是缓存与新数据的对比,得到newHosts:新增的实例;remvHosts:待移除的实例;
  21. // modHosts:需要修改的实例
  22. if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {
  23. // 发布实例变更的事件
  24. NotifyCenter.publishEvent(new InstancesChangeEvent(
  25. serviceInfo.getName(), serviceInfo.getGroupName(),
  26. serviceInfo.getClusters(), serviceInfo.getHosts()));
  27. DiskCache.write(serviceInfo, cacheDir);
  28. }
  29. } else {
  30. // 本地缓存不存在
  31. changed = true;
  32. // 放入缓存
  33. serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
  34. // 直接发布实例变更的事件
  35. NotifyCenter.publishEvent(new InstancesChangeEvent(
  36. serviceInfo.getName(), serviceInfo.getGroupName(),
  37. serviceInfo.getClusters(), serviceInfo.getHosts()));
  38. serviceInfo.setJsonFromServer(json);
  39. DiskCache.write(serviceInfo, cacheDir);
  40. }
  41. // 。。。
  42. return serviceInfo;
  43. }

4.2.服务端

4.2.1.拉取服务列表接口

在2.3.1小节介绍的InstanceController中,提供了拉取服务列表的接口:

  1. /**
  2. * Get all instance of input service.
  3. *
  4. * @param request http request
  5. * @return list of instance
  6. * @throws Exception any error during list
  7. */
  8. @GetMapping("/list")
  9. @Secured(parser = NamingResourceParser.class, action = ActionTypes.READ)
  10. public ObjectNode list(HttpServletRequest request) throws Exception {
  11. // 从request中获取namespaceId和serviceName
  12. String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
  13. String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
  14. NamingUtils.checkServiceNameFormat(serviceName);
  15. String agent = WebUtils.getUserAgent(request);
  16. String clusters = WebUtils.optional(request, "clusters", StringUtils.EMPTY);
  17. String clientIP = WebUtils.optional(request, "clientIP", StringUtils.EMPTY);
  18. // 获取客户端的 UDP端口
  19. int udpPort = Integer.parseInt(WebUtils.optional(request, "udpPort", "0"));
  20. String env = WebUtils.optional(request, "env", StringUtils.EMPTY);
  21. boolean isCheck = Boolean.parseBoolean(WebUtils.optional(request, "isCheck", "false"));
  22. String app = WebUtils.optional(request, "app", StringUtils.EMPTY);
  23. String tenant = WebUtils.optional(request, "tid", StringUtils.EMPTY);
  24. boolean healthyOnly = Boolean.parseBoolean(WebUtils.optional(request, "healthyOnly", "false"));
  25. // 获取服务列表
  26. return doSrvIpxt(namespaceId, serviceName, agent, clusters, clientIP, udpPort, env, isCheck, app, tenant,
  27. healthyOnly);
  28. }

进入doSrvIpxt()方法来获取服务列表:

  1. public ObjectNode doSrvIpxt(String namespaceId, String serviceName, String agent,
  2. String clusters, String clientIP,
  3. int udpPort, String env, boolean isCheck,
  4. String app, String tid, boolean healthyOnly) throws Exception {
  5. ClientInfo clientInfo = new ClientInfo(agent);
  6. ObjectNode result = JacksonUtils.createEmptyJsonNode();
  7. // 获取服务列表信息
  8. Service service = serviceManager.getService(namespaceId, serviceName);
  9. long cacheMillis = switchDomain.getDefaultCacheMillis();
  10. // now try to enable the push
  11. try {
  12. if (udpPort > 0 && pushService.canEnablePush(agent)) {
  13. // 添加当前客户端 IP、UDP端口到 PushService 中
  14. pushService
  15. .addClient(namespaceId, serviceName, clusters, agent, new InetSocketAddress(clientIP, udpPort),
  16. pushDataSource, tid, app);
  17. cacheMillis = switchDomain.getPushCacheMillis(serviceName);
  18. }
  19. } catch (Exception e) {
  20. Loggers.SRV_LOG
  21. .error("[NACOS-API] failed to added push client {}, {}:{}", clientInfo, clientIP, udpPort, e);
  22. cacheMillis = switchDomain.getDefaultCacheMillis();
  23. }
  24. if (service == null) {
  25. // 如果没找到,返回空
  26. if (Loggers.SRV_LOG.isDebugEnabled()) {
  27. Loggers.SRV_LOG.debug("no instance to serve for service: {}", serviceName);
  28. }
  29. result.put("name", serviceName);
  30. result.put("clusters", clusters);
  31. result.put("cacheMillis", cacheMillis);
  32. result.replace("hosts", JacksonUtils.createEmptyArrayNode());
  33. return result;
  34. }
  35. // 结果的检测,异常实例的剔除等逻辑省略
  36. // 最终封装结果并返回 。。。
  37. result.replace("hosts", hosts);
  38. if (clientInfo.type == ClientInfo.ClientType.JAVA
  39. && clientInfo.version.compareTo(VersionUtil.parseVersion("1.0.0")) >= 0) {
  40. result.put("dom", serviceName);
  41. } else {
  42. result.put("dom", NamingUtils.getServiceName(serviceName));
  43. }
  44. result.put("name", serviceName);
  45. result.put("cacheMillis", cacheMillis);
  46. result.put("lastRefTime", System.currentTimeMillis());
  47. result.put("checksum", service.getChecksum());
  48. result.put("useSpecifiedURL", false);
  49. result.put("clusters", clusters);
  50. result.put("env", env);
  51. result.replace("metadata", JacksonUtils.transferToJsonNode(service.getMetadata()));
  52. return result;
  53. }

4.2.2.发布服务变更的UDP通知

在上一节中,InstanceController中的doSrvIpxt()方法中,有这样一行代码:

  1. pushService.addClient(namespaceId, serviceName, clusters, agent,
  2. new InetSocketAddress(clientIP, udpPort),
  3. pushDataSource, tid, app);

其实是把消费者的UDP端口、IP等信息封装为一个PushClient对象,存储PushService中。方便以后服务变更后推送消息。

PushService类本身实现了ApplicationListener接口:

Nacos源码分析 - 图56

这个是事件监听器接口,监听的是ServiceChangeEvent(服务变更事件)。

当服务列表变化时,就会通知我们:

Nacos源码分析 - 图57

4.3.总结

Nacos的服务发现分为两种模式:

  • 模式一:主动拉取模式,消费者定期主动从Nacos拉取服务列表并缓存起来,再服务调用时优先读取本地缓存中的服务列表。
  • 模式二:订阅模式,消费者订阅Nacos中的服务列表,并基于UDP协议来接收服务变更通知。当Nacos中的服务列表更新时,会发送UDP广播给所有订阅者。

与Eureka相比,Nacos的订阅模式服务状态更新更及时,消费者更容易及时发现服务列表的变化,剔除故障服务。