Nacos 服务端健康检查及客户端服务订阅机制源码分析(三)(下)

简介: Nacos 服务端健康检查及客户端服务订阅机制源码分析(三)(下)

DefaultPublisher 事件发布者

public class DefaultPublisher extends Thread implements EventPublisher
• 1

默认发布者的源码,查看以后可以发现它继承自 Thread,也就是它是一个线程类;同时,它又实现了 EventPublisher,也就是发布者

@Override
public void init(Class<? extends Event> type, int bufferSize) {
  // 守护线程
  setDaemon(true);
  // 设置线程名称
  setName("nacos.publisher-" + type.getName());
  this.eventType = type;
  this.queueMaxSize = bufferSize;
  // 阻塞队列初始化
  this.queue = new ArrayBlockingQueue<>(bufferSize);
  start();
}
@Override
public synchronized void start() {
  if (!initialized) {
    // start just called once
    super.start();
    if (queueMaxSize == -1) {
      queueMaxSize = ringBufferSize;
    }
    initialized = true;
  }
}

看它的 init 初始化方法,从这里可以看出当 DefaultPublisher 被初始化时,是以守护线程的方式进行运作的,其中还初始化了一个阻塞队列,最后调用了 start 方法:在这其中调用了 super.start 方法启动了线程,接着看 run 方法的运行逻辑

@Override
public void run() {
  openEventHandler();
}
void openEventHandler() {
  try {
    // This variable is defined to resolve the problem which message overstock in the queue.
    int waitTimes = 60;
    // To ensure that messages are not lost, enable EventHandler when
    // waiting for the first Subscriber to register
    // 死循环延迟,线程启动最大延时 60 秒,这个主要是为了解决消息积压问题
    for (; ; ) {
      if (shutdown || hasSubscriber() || waitTimes <= 0) {
        break;
      }
      ThreadUtils.sleep(1000L);
      waitTimes--;
    }
    // 死循环不断的从队列中取出 Event,并通知订阅者 Subscriber 执行 Event
    for (; ; ) {
      if (shutdown) {
        break;
      }
      // 从队列中取出 Event
      final Event event = queue.take();
      receiveEvent(event);
      UPDATER.compareAndSet(this, lastEventSequence, Math.max(lastEventSequence, event.sequence()));
    }
  } catch (Throwable ex) {
    LOGGER.error("Event listener exception : ", ex);
  }
}

run 方法中调用 openEventHandler 方法,这里面写了两个死循环

  • 第一个循环:可以理解为延时效果,也就是说线程启动时最大延时为 60 秒,在这 60 秒中每间隔一秒会判断「当前线程是否关闭、是否有订阅者、是否等待超过 60 秒」如果满足其中一个条件,就可以提前退出当前死循环
  • 第二个循环:真正处理业务逻辑的,会从阻塞队列中取出一个事件,然后通过 receiveEvent 方法执行

队列中的事件从何而来?

@Override
public boolean publish(Event event) {
  checkIsStart();
  boolean success = this.queue.offer(event);
  if (!success) {
    LOGGER.warn("Unable to plug in due to interruption, synchronize sending time, event : {}", event);
    receiveEvent(event);
    return true;
  }
  return true;
}

其实就是 DefaultPublisher#publish 发布事件方法被调用了,往阻塞队列中存入事件,如果存入失败,会直接调用 receiveEvent「直接理解为如果存入队列失败,则立即执行,不走队列了」

receiveEvent 方法

// 通知和通知订阅者进行事件处理
void receiveEvent(Event event) {
  if (!hasSubscriber()) {
    LOGGER.warn("[NotifyCenter] the {} is lost, because there is no subscriber.", event);
    return;
  }
  // 通知订阅者执行 Event
  for (Subscriber subscriber : subscribers) {
    if (!subscriber.scopeMatches(event)) {
      continue;
    }
    // Whether to ignore expiration events
    if (subscriber.ignoreExpireEvent() && lastEventSequence > currentEventSequence) {
      LOGGER.debug("[NotifyCenter] the {} is unacceptable to this subscriber, because had expire",
                   event.getClass());
      continue;
    }
    // Because unifying smartSubscriber and subscriber, so here need to think of compatibility.
    // Remove original judge part of codes.
    notifySubscriber(subscriber, event);
  }
}

在这里就是遍历 subscribers(订阅者集合),然后通知订阅者执行事件,那么 subscribers 订阅者是从何而为的呢?这个还是要回到 NacosNamingService#init 方法中

// NacosNamingService#init
// 将 Subsribe 注册到 Publisher 中
NotifyCenter.registerSubscriber(changeNotifier);
// NotifyCenter#addSubscriber
// NotifyCenter#registerSubscriber->NotifyCenter#addSubscriber
private static void addSubscriber(final Subscriber consumer, Class<? extends Event> subscribeType,
                                  EventPublisherFactory factory) {
  final String topic = ClassUtils.getCanonicalName(subscribeType);
  synchronized (NotifyCenter.class) {
    // MapUtils.computeIfAbsent is a unsafe method.
    MapUtil.computeIfAbsent(INSTANCE.publisherMap, topic, factory, subscribeType, ringBufferSize);
  }
  // 获取事件对应的发布者
  EventPublisher publisher = INSTANCE.publisherMap.get(topic);
  if (publisher instanceof ShardedEventPublisher) {
    ((ShardedEventPublisher) publisher).addSubscriber(consumer, subscribeType);
  } else {
    // 添加到对应的 subscribers 集合
    publisher.addSubscriber(consumer);
  }
}

registerSubscriber 方法最终会调用 NotifyCenter#addSubscriber 方法「核心逻辑:将订阅的事件、发布者、订阅者三者关系进行绑定,发布者与事件通过 Map 进行维护,发布者与订阅者通过关联关系进行维护」

// DefaultPublisher.java
@Override
public void notifySubscriber(final Subscriber subscriber, final Event event) {
  LOGGER.debug("[NotifyCenter] the {} will received by {}", event, subscriber);
  // 执行订阅事件
  final Runnable job = () -> subscriber.onEvent(event);
  // 执行者,该订阅是否指定了线程池:如果有则拉起一个线程进行异步执行事件
  final Executor executor = subscriber.executor();
  if (executor != null) {
    executor.execute(job);
  } else {
    try {
      job.run();
    } catch (Throwable e) {
      LOGGER.error("Event callback exception: ", e);
    }
  }
}

总结

整体服务订阅的事件机制还是比较复杂的,因为用到了事件的形式,逻辑比较绕,并且其中还有守护线程、死循环、阻塞队列等;重点理解的是 NotifyCenter 对事件发布者、事件订阅者和事件之间关系的维护,而这一关系维护的入口就位于 NacosNamingService#init 方法当中,以下是核心流程:事件发布、事件处理

流程图如下分析:

ServiceInfoHolder 中通过 NotifyCenter 发布了 InstancesChangeEvent 事件

NotifyCenter 进行发布事件,核心逻辑如下:

  • 通过 InstancesChangeEvent 事件类型,获取对应的 CanonicalName
  • 把 CanonicalName 作为 Key,从 NotifyCenter#publisherMap 中获取到对应的事件发布者(EventPublisher)
  • EventPublisher 将 InstancesChangeEvent 事件进行发布

InstancesChangeEvent 事件发布处理逻辑:

  • 通过 EventPublisher 默认实现类 DefaultPublisher 进行 InstancesChangeEvent 事件发布
  • DefaultPublisher 本身以守护线程的方式运行,在执行业务逻辑前,先判断该线程是否启动
  • 如果启动,则将事件添加到 BlockingQueue 中,队列默认大小:16384
  • 如果未启动,直接抛出异常结束
  • 添加到队列失败,则直接调用 DefaultPublisher#receiveEvent 方法,接收事件并通知订阅者进行处理
  • 通过订阅者创建一个 Runnable 对象,执行订阅者 Event,Event 事件便是执行订阅时传入的

添加到队列成功,则进入到事件的处理阶段

  • DefaultPublisher 初始化时会创建一个阻塞(BlockingQueue)队列,并标记线程启动
  • DefaultPublisher 本身继承于 Thread,当执行 super#start 方法时,会调用它的 run 方法
  • run 方法核心业务逻辑是通过 openEventHandler 方法进行处理的
  • openEventHandler 方法通过两个 for 循环,从阻塞队列中获取时间信息
  • one.用于让线程启动时在 60s 内检查执行条件,如果条件满足则立即执行第二个死循环
  • two.循环为死循环,从阻塞队列中获取 Event,并调用 DefaultPublisher#receiveEvent 方法,接收事件并通知订阅者进行处理

结尾

至此「Nacos 服务端注册、客户端服务发现源码分析」分析到这里,基本只需要掌握大致的脉路即可

欢迎大家在评论框分享您的看法,喜欢该文章帮忙给个赞👍和收藏,感谢!!

分享个人学习源码的几部曲

  • 设计模式掌握为前提,程序员的内功修炼法,🙅不分语言
  • 不要太追究于细节,捋清大致脉路即可;太过于追究于细节,你会越捋越乱
  • 关注重要的类和方法、核心逻辑
  • 掌握 Debug 技巧,在关键的类和方法多停留,多作分析和记录

更多技术文章可以查看:vnjohn 个人博客


目录
相关文章
|
19天前
|
数据管理 Nacos 开发者
"Nacos架构深度解析:一篇文章带你掌握业务层四大核心功能,服务注册、配置管理、元数据与健康检查一网打尽!"
【10月更文挑战第23天】Nacos 是一个用于服务注册发现和配置管理的平台,支持动态服务发现、配置管理、元数据管理和健康检查。其业务层包括服务注册与发现、配置管理、元数据管理和健康检查四大核心功能。通过示例代码展示了如何在业务层中使用Nacos,帮助开发者构建高可用、动态扩展的微服务生态系统。
63 0
|
19天前
|
SQL 关系型数据库 数据库连接
"Nacos 2.1.0版本数据库配置写入难题破解攻略:一步步教你排查连接、权限和配置问题,重启服务轻松解决!"
【10月更文挑战第23天】在使用Nacos 2.1.0版本时,可能会遇到无法将配置信息写入数据库的问题。本文将引导你逐步解决这一问题,包括检查数据库连接、用户权限、Nacos配置文件,并提供示例代码和详细步骤。通过这些方法,你可以有效解决配置写入失败的问题。
44 0
|
3月前
|
Kubernetes Nacos 微服务
【技术难题破解】Nacos v2.2.3 + K8s 微服务注册:强制删除 Pod 却不消失?!7步排查法+实战代码,手把手教你解决Nacos Pod僵死问题,让服务瞬间满血复活!
【8月更文挑战第15天】Nacos作为微服务注册与配置中心受到欢迎,但有时会遇到“v2.2.3 k8s 微服务注册nacos强制删除 pod不消失”的问题。本文介绍此现象及其解决方法,帮助开发者确保服务稳定运行。首先需检查Pod状态与事件、配置文件及Nacos配置,确认无误后可调整Pod生命周期管理,并检查Kubernetes版本兼容性。若问题持续,考虑使用Finalizers、审查Nacos日志或借助Kubernetes诊断工具。必要时,可尝试手动强制删除Pod。通过系统排查,通常能有效解决此问题。
72 0
|
3月前
|
Java Nacos 开发工具
【Nacos】心跳断了怎么办?!8步排查法+实战代码,手把手教你解决Nacos客户端不发送心跳检测问题,让服务瞬间恢复活力!
【8月更文挑战第15天】Nacos是一款广受好评的微服务注册与配置中心。然而,“客户端不发送心跳检测”的问题时有发生,可能导致服务实例被视为离线。本文介绍如何排查此类问题:确认Nacos服务器地址配置正确;检查网络连通性;查看客户端日志;确保Nacos SDK版本兼容;调整心跳检测策略;验证服务实例注册状态;必要时重启应用;检查影响行为的环境变量。通过这些步骤,通常可定位并解决问题,保障服务稳定运行。
217 0
|
4月前
|
Java Nacos 数据库
使用 nacos 搭建注册中心及配置中心
使用 nacos 搭建注册中心及配置中心
100 5
|
11天前
|
负载均衡 应用服务中间件 Nacos
Nacos配置中心
Nacos配置中心
41 1
Nacos配置中心
|
4月前
|
NoSQL Java Nacos
SpringCloud集成Seata并使用Nacos做注册中心与配置中心
SpringCloud集成Seata并使用Nacos做注册中心与配置中心
137 3
|
7天前
|
监控 Java 测试技术
Nacos 配置中心变更利器:自定义标签灰度
本文是对 MSE Nacos 应用自定义标签灰度的功能介绍,欢迎大家升级版本进行试用。
|
10天前
|
网络安全 Nacos 开发者
Nacos作为流行的微服务注册与配置中心,“节点提示暂时不可用”是常见的问题之一
Nacos作为流行的微服务注册与配置中心,其稳定性和易用性备受青睐。然而,“节点提示暂时不可用”是常见的问题之一。本文将探讨该问题的原因及解决方案,帮助开发者快速定位并解决问题,确保服务的正常运行。通过检查服务实例状态、网络连接、Nacos配置、调整健康检查策略等步骤,可以有效解决这一问题。
22 4
|
10天前
|
Java 网络安全 Nacos
Nacos作为流行的微服务注册与配置中心,其稳定性和易用性备受青睐。
Nacos作为流行的微服务注册与配置中心,其稳定性和易用性备受青睐。然而,实际使用中常遇到“客户端不发送心跳检测”的问题。本文深入探讨该问题的原因及解决方案,帮助开发者快速定位并解决问题,确保服务正常运行。通过检查客户端配置、网络连接、日志、版本兼容性、心跳策略、注册状态、重启应用和环境变量等步骤,系统地排查和解决这一问题。
26 3