• Nacos系列【26】源码分析篇之客户端自动注册


    有道无术,术尚可求,有术无道,止于术。

    版本說明:

    spring-boot:2.6.3

    spring-cloud:2021.0.1

    spring-cloud-alibaba:2.2.8.RELEASE

    Nacos:2.1.1

    前言

    紧接上篇,接下来分析下客户端自动注册的流程。

    1. 加载日志系统

    Spring Boot开启启动,会加载spring.factories中的自动配置类:

    • NacosDiscoveryAutoConfiguration
    • RibbonNacosAutoConfiguration
    • NacosDiscoveryEndpointAutoConfiguration
    • NacosServiceRegistryAutoConfiguration
    • NacosReactiveDiscoveryClientConfiguration
    • NacosConfigServerAutoConfiguration
    • NacosDiscoveryClientConfigServiceBootstrapConfiguration
    • NacosDiscoveryAutoConfiguration

    在环境准备阶段,NacosLoggingListener监听器首先工作,因为它监听的事件是ApplicationEnvironmentPreparedEvent,其作用是加载日志系统:

     public void onApplicationEvent(ApplicationEvent applicationEvent) {
            // 如果存在`logback`相关的类,加载logback日志系统LogbackNacosLogging,否则使用Log4J2NacosLogging,然后加载配置
            NacosLogging.getInstance().loadConfiguration();
        }
    
    • 1
    • 2
    • 3
    • 4

    在加载配置的最后阶段,可以看到其读取了classpath:nacos-logback.xm日志配置文件,并创建了一个LoggerContext 日志上下文:

        private LoggerContext loadConfigurationOnStart() {
            String location = this.getLocation("classpath:nacos-logback.xml");
            try {
                LoggerContext loggerContext = (LoggerContext)StaticLoggerBinder.getSingleton().getLoggerFactory();
                NacosJoranConfigurator configurator = new NacosJoranConfigurator();
                configurator.setContext(loggerContext);
                configurator.doNacosConfigure(ResourceUtils.getResourceUrl(location));
                return loggerContext;
            } catch (Exception var4) {
                throw new IllegalStateException("Could not initialize Logback Nacos logging from " + location, var4);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    而这个时候,当前应用对应的日志系统都还没有加载,所以Nacos 客户端的一些日志级别配置,需要在源码配置文件中修改,或者通过JVM启动参数才能起效:
    在这里插入图片描述

    2. 启动Grpc 客户端

    Nacos 2.x采用Grpc通信,gRPC是由google开发的一个高性能、通用的开源RPC框架,主要面向移动应用开发且基于HTTP/2协议标准而设计,同时支持大多数流行的编程语言。

    使用gRPC框架,需要开启客户端和服务端,可以在GitHub学习下,这里就不再研究了。

    GrpcSdkClient就是Nacos 进行通信的客户端,利用Grpc来实现服务器连接、消息处理等功能,启动逻辑在其start()方法中。在方法中会创建一些定时任务,以及和Nacos服务端中的GrpcSdkServer建立长连接。

    3. 自动配置类

    接着开始进行自动配置,注入配置类及Bean 对象,首先是服务管理器:
    在这里插入图片描述
    接着是配置类:
    在这里插入图片描述
    服务发现功能类:
    在这里插入图片描述
    服务发现客户端:
    在这里插入图片描述
    整个生命周期监测者NacosWatch
    在这里插入图片描述
    等等。。。其他类就不一一介绍了。

    4. NacosWatch

    NacosDiscoveryClientConfiguration自动配置类中,注册了NacosWatch
    在这里插入图片描述
    调用NacosWatch构造方法:

        public NacosWatch(NacosServiceManager nacosServiceManager, NacosDiscoveryProperties properties) {
        	// 服务管理器
            this.nacosServiceManager = nacosServiceManager;
            // YML 配置
            this.properties = properties;
            // 多线程定时任务
            this.taskScheduler = getTaskScheduler();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    NacosWatch实现了ApplicationEventPublisherAware接口,作用是获取初始化事件发布器:
    在这里插入图片描述
    NacosWatch还实现了SmartLifecycle,该接口主要是作用是所有的bean都创建完成之后,可以执行初始化操作,在退出时执行资源销毁工作。
    在这里插入图片描述
    所以在应用启动后,执行重写了SmartLifecyclestart()方法:

        public void start() {
            if (this.running.compareAndSet(false, true)) {
                // 添加名称为app-service001:DEFAULT_GROUP 的监听器 
                EventListener eventListener = (EventListener)this.listenerMap.computeIfAbsent(this.buildKey(), (event) -> {
                    return new EventListener() {
                        public void onEvent(Event event) {
                            if (event instanceof NamingEvent) {
                                List instances = ((NamingEvent)event).getInstances();
                                Optional instanceOptional = NacosWatch.this.selectCurrentInstance(instances);
                                instanceOptional.ifPresent((currentInstance) -> {
                                    NacosWatch.this.resetIfNeeded(currentInstance);
                                });
                            }
    
                        }
                    };
                });
                // 获取NamingService
                NamingService namingService = this.nacosServiceManager.getNamingService(this.properties.getNacosProperties());
    
                try {
                	// 订阅事件
                    namingService.subscribe(this.properties.getService(), this.properties.getGroup(), Arrays.asList(this.properties.getClusterName()), eventListener);
                } catch (Exception var4) {
                    log.error("namingService subscribe failed, properties:{}", this.properties, var4);
                }
    			// 定时任务去发布心跳事件
                this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(this::nacosServicesWatch, this.properties.getWatchDelay());
            }
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    5. 自动注册

    自动注册由NacosAutoServiceRegistration类来完成,继承了AbstractAutoServiceRegistration抽象类,类关系图如下:
    在这里插入图片描述
    实现了监听器接口,监听WebServerInitializedEvent 事件,所以在Web容器初始化完成后,会进入到onApplicationEvent 方法:

        public void onApplicationEvent(WebServerInitializedEvent event) {
            this.bind(event);
        }
    
    • 1
    • 2
    • 3

    在这里插入图片描述
    接着进入到AbstractAutoServiceRegistrationbind()方法:

        @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();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    start()方法中,调用注册方法,并发布一些事件和日志:

        public void start() {
            // 没有开启自动注册,打印debug 日志
            if (!this.isEnabled()) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Discovery Lifecycle disabled. Not starting");
                }
    
            } else {
                if (!this.running.get()) { // 开始标记,最初为false
                    // 发布实例预注册事件
                    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());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    注册方法,调用的是NacosServiceRegistryregister(Registration registration)方法:

            public void register(Registration registration) {
            // 服务名为空,打印警告日志
            if (StringUtils.isEmpty(registration.getServiceId())) {
                log.warn("No service to register for nacos client...");
            } else {
                NamingService namingService = this.namingService();// // NamingService
                String serviceId = registration.getServiceId(); // 服务名=>app-service001
                String group = this.nacosDiscoveryProperties.getGroup();// 配置的分组,默认 DEFAULT_GROUP
                // 创建实例对象
                Instance instance = this.getNacosInstanceFromRegistration(registration);
                try {
                    // 调用NamingService 注册实例
                    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});
                    }
                }
    
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在获取实例的方法中,可以看到设置了很多实例信息:

    private Instance getNacosInstanceFromRegistration(Registration registration) {
            Instance instance = new Instance();
            instance.setIp(registration.getHost()); // 当前服务IP=》192.168.142.1
            instance.setPort(registration.getPort()); // 启动端口=》 9005
            instance.setWeight((double)this.nacosDiscoveryProperties.getWeight()); // 权重=》1.0
            instance.setClusterName(this.nacosDiscoveryProperties.getClusterName()); // 集群名称=》DEFAULT
            instance.setEnabled(this.nacosDiscoveryProperties.isInstanceEnabled()); // 是否主动注册的实例
            instance.setMetadata(registration.getMetadata()); // 元数据
            instance.setEphemeral(this.nacosDiscoveryProperties.isEphemeral()); // 临时实例还是永久实例
            return instance;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在注册实例registerInstance方法中,会调用客户端代理进行注册:

            public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException 
            // 检查是否合法:心跳时间、服务剔除时间、集群名称是否合法
            // 心跳机制:
            //  Nacos Server会开启一个定时任务用来检查注册服务实例的健康状况,
            //  对于超过15s没有收到客户端心跳的实例会将它的healthy属性设置为false(客户端服务发现时不会发现)。
            //  如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)
            NamingUtils.checkInstanceIsLegal(instance);
            // 调用客户端代理进行注册
            this.clientProxy.registerService(serviceName, groupName, instance);
        }
    		// 获取客户端执行代理进行注册
            public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
                this.getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    获取到的通信注册客户端为NamingGrpcClientProxy

        // 获取执行客户端代理,如果是临时实例,则会使用RPC,不是则使用HTTP。
        // 通信层统一到 gRPC 协议,同时完善了客户端和服务端的流量控制和负载均衡能力,提升的整体吞吐。
        // 由于通信使用了 RPC 方式,因此某一客户端的所有请求(无论是注册还是订阅)都通过同一个链接和同一个服务节点进行,不像之前通过 HTTP 连接可能每次请求都请求在不同的 Nacos 节点上,这就导致了服务发现的数据内容由原来的无状态化变为了与连接状态绑定的一种有状态数据。
        private NamingClientProxy getExecuteClientProxy(Instance instance) {
                return (NamingClientProxy)(instance.isEphemeral() ? this.grpcClientProxy : this.httpClientProxy);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    NamingGrpcClientProxy 缓存实例信息,接着继续注册:

        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});
            // 缓存放入内存ConcurrentMap,DEFAULT_GROUP@@app-service001=》实例信息
            this.redoService.cacheInstanceForRedo(serviceName, groupName, instance);
            // 调用客户端注册
            this.doRegisterService(serviceName, groupName, instance);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    最终,发送gprc请求到服务端,客户端完成注册:

    public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException {
            // 创建请求对象
            InstanceRequest request = new InstanceRequest(this.namespaceId, serviceName, groupName, "registerInstance", instance);
            // 发送请求到服务端
            this.requestToServer(request, Response.class);
            this.redoService.instanceRegistered(serviceName, groupName);
        }
        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 (NacosException var4) {
                throw var4;
            } catch (Exception var5) {
                throw new NacosException(500, "Request nacos server failed: ", var5);
            }
    
            throw new NacosException(500, "Server return invalid response");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    可以在控制台看到打印的注册日志:
    在这里插入图片描述

  • 相关阅读:
    SpringBoot SpringBoot 基础篇 1 快速上手SpringBoot 2 知识加油站 - REST 开发 2.2 入门案例
    本周大新闻|苹果再强调禁用“元宇宙”一词,TDK推出全球最小RGB激光模块
    自主研究,开发并产业化的一套UWB精确定位系统源码 UWB源码
    Forescout宣布任命Rik Ferguson为安全情报副总裁
    Allegro DFM Ravel Rule 板外异物检查
    HTTP流量神器Goreplay核心源码详解
    阿里云易立:以增效促降本,容器服务全面进入智能化时代
    对于C++STL及其时间复杂度的总结
    VC6创建工程的各种类型
    Java --- JVM的执行引擎
  • 原文地址:https://blog.csdn.net/qq_43437874/article/details/126483750