死磕Nacos系列:Nacos在我的SpringCloud项目中做了什么?

Nacos服务注册

我们一个SpringCloud项目中集成了Nacos,当项目启动成功后,就可以在Nacos管理界面上看到我们项目的注册信息,还可以看到项目的健康状态等等信息:

那Nacos是什么时候进行了哪些操作的呢?今天我们来一探究竟,Let's go!

Nacos客户端

如何集成SpringCloud Alibaba Nacos?

在Maven项目中,先引入依赖:

xml 复制代码
<!-- nacos注册依赖 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>2.2.3.RELEASE</version>
</dependency>

SpringBoot应用中的启动类:

less 复制代码
@EnableDiscoveryClient
@SpringBootApplication
public class ServiceApplication {
​
    public static void main(String[] args) {
        SpringApplication.run(ServiceApplication.class, args);
    }
}

配置文件bootstrap.yaml:

yaml 复制代码
server:
  port: 9001
spring:
  application:
    name: ServiceA
  cloud:
    nacos:
      discovery:
        # NacosServer地址
        server-addr: 127.0.0.1:8848
      # NacosServer鉴权模式下的用户名
      username: nacos
      # NacosServer鉴权模式下的密码
      password: nacos

第一步从哪里下手?

了解过SpringBoot的starter的同学都知道,引入一个starter,我们可以去看这个starter中的spring.factories文件,因为这里会告诉你SpringBoot在启动中,会自动加载这个starter的哪些配置类。

ini 复制代码
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
​

在自动加载的类中,NacosServiceRegistryAutoConfiguration是处理服务实例自动注册的类。

走进服务自动注册

NacosServiceRegistryAutoConfiguration中,创建了NacosServiceRegistryNacosRegistrationNacosAutoServiceRegistration的bean。

其中NacosServiceRegistry继承自SpringCloudCommon下的ServiceRegistry,实现了服务注册、取消注册的能力;

NacosRegistration继承自SpringCloudCommon的Registration,表明它描述的是一个注册信息,其中包含了服务的ip、端口、元信息、访问协议等信息;

NacosAutoServiceRegistration就是利用上面的能力和数据,在合适的时候被调用进行自动注册。

那什么时候才是合适的时候呢?我们可以看到,NacosAutoServiceRegistration继承自AbstractAutoServiceRegistration、实现了ApplicationListener接口,那就说明需要被WebServerInitializedEvent事件驱动。WebServerInitializedEventApplicationContext刷新完成,且Web服务初始化完成后发布的一个事件。

接收到事件通知后,就会进行注册:

csharp 复制代码
protected void register() {
   this.serviceRegistry.register(getRegistration());
}

NacosServiceRegistry是怎样实现注册的?

在Nacos中,ServiceRegistry其实是使用的NamingService的能力完成注册的。

ini 复制代码
public void register(Registration registration) {
​
   ....
       
   NamingService namingService = namingService();
   String serviceId = registration.getServiceId();
   String group = nacosDiscoveryProperties.getGroup();
​
   Instance instance = getNacosInstanceFromRegistration(registration);
​
   try {
      namingService.registerInstance(serviceId, group, instance);
      .....
   }
   catch (Exception e) {
      ....
   }
}

NamingService提供了服务注册,下线、订阅消息,查询等功能,是很核心的一个能力提供者。

NamingServiceNacosServiceManager负责管理,具体由NamingFactory通过进行创建,具体实现类是NacosNamingService

NacosNamingService在实例化的时候创建了很多组件,如

  • EventDispatcher

  • 一个单线程的线程池+死循环

  • 承接Service信息改变的业务,并通知到各个订阅了此Service的监听器

  • NamingProxy

    • 两个核心线程的定时线程池
    • 每隔30秒拉取远端Server列表
    • 如果token过期,重新登录获取token
  • BeatReactor

    • 定时线程池
    • 核心线程数量可自定义,默认是核心数的一半,最少1个核心线程数
    • 默认每隔5秒向服务端发送心跳
  • HostReactor

    • 提供主动查询更新和被动接受服务信息推送的能力

    • PushReceiver

      • 一个单线程的定时线程池+死循环
      • 接受来自服务端通过udp推送的数据,并向服务端发送ack确认信息

NacosNamingService的注册实例方法中:

ini 复制代码
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    if (instance.isEphemeral()) {
        BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    serverProxy.registerService(groupedServiceName, groupName, instance);
}

可以看到,如果是临时实例(可通过配置文件配置,默认是临时实例),那就会向线程池中丢一个心跳任务。

再通过NamingProxy进行注册:

less 复制代码
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
    
    NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
            instance);
    
    final Map<String, String> params = new HashMap<String, String>(16);
    params.put(CommonParams.NAMESPACE_ID, namespaceId);
    params.put(CommonParams.SERVICE_NAME, serviceName);
    params.put(CommonParams.GROUP_NAME, groupName);
    params.put(CommonParams.CLUSTER_NAME, 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()));
    
    reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
    
}

这里就是将服务实例信息通过标准的Restful接口和Server进行通信。

Nacos服务端

UtilAndComs.nacosUrlInstance对应的请求路径是:/nacos/v1/ns/instance

在Nacos源码中, 这个请求的处理是在naming模块下的InstanceControllerV2#register中进行的。

less 复制代码
@CanDistro
@PostMapping
@Secured(action = ActionTypes.WRITE)
public Result<String> register(InstanceForm instanceForm) throws NacosException {
    // 检查实例的参数
    instanceForm.validate();
    // 检查实例的权重
    checkWeight(instanceForm.getWeight());
    // 构造一个实例对象
    Instance instance = buildInstance(instanceForm);
    // 注册
    instanceServiceV2.registerInstance(instanceForm.getNamespaceId(), buildCompositeServiceName(instanceForm), instance);
    // 发送RegisterInstanceTraceEvent事件
    NotifyCenter.publishEvent(new RegisterInstanceTraceEvent(System.currentTimeMillis(), "",
            false, instanceForm.getNamespaceId(), instanceForm.getGroupName(), instanceForm.getServiceName(),
            instance.getIp(), instance.getPort()));
    return Result.success("ok");
}

上面代码就是检查参数,构造Instance服务实例对象,使用instanceServiceV2类进行注册,再发布RegisterInstanceTraceEvent事件。

接下来进入instanceServiceV2.registerInstance看看:

ini 复制代码
@Override
public void registerInstance(Service service, Instance instance, String clientId) throws NacosException {
    NamingUtils.checkInstanceIsLegal(instance);
​
    Service singleton = ServiceManager.getInstance().getSingleton(service);
    if (!singleton.isEphemeral()) {
        throw new NacosRuntimeException(NacosException.INVALID_PARAM,
                String.format("Current service %s is persistent service, can't register ephemeral instance.",
                        singleton.getGroupedServiceName()));
    }
    Client client = clientManager.getClient(clientId);
    if (!clientIsLegal(client, clientId)) {
        return;
    }
    InstancePublishInfo instanceInfo = getPublishInfo(instance);
    client.addServiceInstance(singleton, instanceInfo);
    client.setLastUpdatedTime();
    client.recalculateRevision();
    NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
    NotifyCenter
            .publishEvent(new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), false));
}

上述源码其实就三个步骤:

1、将Service的信息维护到ServiceManager中。

2、从ClientManager中获取Client信息。

3、向NotifyCenter发送事件通知。

Nacos Web

在Nacos的后台web系统中,服务列表是通过GET /nacos/v1/ns/catalog/services获取的,我们可以在Nacos的naming模块下找到相应的控制类的处理逻辑:

less 复制代码
@Secured(action = ActionTypes.READ)
@GetMapping("/services")
public Object listDetail(@RequestParam(required = false) boolean withInstances,
        @RequestParam(defaultValue = Constants.DEFAULT_NAMESPACE_ID) String namespaceId,
        @RequestParam(required = false) int pageNo, @RequestParam(required = false) int pageSize,
        @RequestParam(name = "serviceNameParam", defaultValue = StringUtils.EMPTY) String serviceName,
        @RequestParam(name = "groupNameParam", defaultValue = StringUtils.EMPTY) String groupName,
        @RequestParam(name = "instance", defaultValue = StringUtils.EMPTY) String containedInstance,
        @RequestParam(required = false) boolean hasIpCount) throws NacosException {
    
    if (withInstances) {
        return judgeCatalogService().pageListServiceDetail(namespaceId, groupName, serviceName, pageNo, pageSize);
    }
    return judgeCatalogService()
            .pageListService(namespaceId, groupName, serviceName, pageNo, pageSize, containedInstance, hasIpCount);
}

跟踪源码,最终我们可以确认到最基础的数据是从ServiceManagernamespaceSingletonMaps中获取到的,namespaceSingletonMaps是一个Map,存储了namespaceService的对应关系。

最后,放上一张整个过程的示意图:

总结

今天的文章里面讲解了一个SpringBoot应用是怎样注册到NacosServer中的,以及Nacos管理界面的数据来源。

文中涉及到了SpringCloudCommon的知识,这里可以简单提一下,SpringCloudCommon是SpringCloud的一系列标准,其抽象了服务注册与发现、负载均衡器、熔断等模型,SpringCloudAlibaba只是按照这个标准具体的一个实现,如SpringCloudNetflix就是另一套实现。

在整个Nacos的体系中,还有很多技术是待深入的,比如NamingService中各个组件具体的实现方式,NotifyCenter的实现方式,服务信息的持久化、保证数据一致性的策略等等,有兴趣的小伙伴可以持续关注我的后续文章。

最后,放上一张Nacos架构图,带你敲响Nacos的大门。

相关推荐
javachen__4 分钟前
SpringBoot整合P6Spy实现全链路SQL监控
spring boot·后端·sql
uzong6 小时前
技术故障复盘模版
后端
GetcharZp6 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程6 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研6 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi7 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
甄超锋8 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
阿华的代码王国8 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy8 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack8 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt