当前位置: 首页 > news >正文

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

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

版本說明:

spring-boot:2.6.3

spring-cloud:2021.0.1

spring-cloud-alibaba:2.2.8.RELEASE

Nacos:2.1.1

文章目录

    • 前言
    • 1. 加载日志系统
    • 2. 启动Grpc 客户端
    • 3. 自动配置类
    • 4. NacosWatch
    • 5. 自动注册

前言

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

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();
    }

在加载配置的最后阶段,可以看到其读取了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);
        }
    }

而这个时候,当前应用对应的日志系统都还没有加载,所以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();
    }

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());
        }

    }

5. 自动注册

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

    public void onApplicationEvent(WebServerInitializedEvent event) {
        this.bind(event);
    }

在这里插入图片描述
接着进入到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();
        }
    }

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());
    }

注册方法,调用的是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});
                }
            }

        }
    }

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

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;
    }

在注册实例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);
    }

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

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

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);
    }

最终,发送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");
    }

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

相关文章:

  • DBeaver常用快捷键(含复制当前行)
  • Java ThreadPoolExecutor的拒绝策略
  • 操作系统——磁盘操作
  • DSPE-PEG-FSHB,FSHB-PEG-DSPE,磷脂-聚乙二醇-靶向多肽FSHB
  • JAVA 力扣练习题:回文数
  • 【Git】credential.helper
  • PDF格式分析(六十九)——注释字典
  • mysql45讲记录
  • 软件测试工程师要摆正自己的心态和位置
  • Vue的双向绑定及应用
  • 编程的基础知识
  • Vue介绍和入门,包括配置等
  • 编程猫创作工具:新版Kitten新体验
  • SpringBean面试题
  • Linux installation of Davinci Adaptive IDE
  • 2017 年终总结 —— 在路上
  • CentOS从零开始部署Nodejs项目
  • Debian下无root权限使用Python访问Oracle
  • es6要点
  • JS+CSS实现数字滚动
  • learning koa2.x
  • Material Design
  • Next.js之基础概念(二)
  • vue脚手架vue-cli
  • win10下安装mysql5.7
  • 半理解系列--Promise的进化史
  • 工作中总结前端开发流程--vue项目
  • 爬虫进阶 -- 神级程序员:让你的爬虫就像人类的用户行为!
  • 微信开源mars源码分析1—上层samples分析
  • 无服务器化是企业 IT 架构的未来吗?
  • 消息队列系列二(IOT中消息队列的应用)
  • 一起来学SpringBoot | 第三篇:SpringBoot日志配置
  • 一些css基础学习笔记
  • 移动端解决方案学习记录
  • - 语言经验 - 《c++的高性能内存管理库tcmalloc和jemalloc》
  • ​DB-Engines 12月数据库排名: PostgreSQL有望获得「2020年度数据库」荣誉?
  • #vue3 实现前端下载excel文件模板功能
  • #传输# #传输数据判断#
  • (+4)2.2UML建模图
  • (2022 CVPR) Unbiased Teacher v2
  • (done) NLP “bag-of-words“ 方法 (带有二元分类和多元分类两个例子)词袋模型、BoW
  • (LeetCode C++)盛最多水的容器
  • (二)Linux——Linux常用指令
  • (分享)一个图片添加水印的小demo的页面,可自定义样式
  • (免费领源码)Java#ssm#MySQL 创意商城03663-计算机毕业设计项目选题推荐
  • (转)setTimeout 和 setInterval 的区别
  • (转)编辑寄语:因为爱心,所以美丽
  • (轉)JSON.stringify 语法实例讲解
  • ***微信公众号支付+微信H5支付+微信扫码支付+小程序支付+APP微信支付解决方案总结...
  • .Net Framework 4.x 程序到底运行在哪个 CLR 版本之上
  • .netcore 如何获取系统中所有session_ASP.NET Core如何解决分布式Session一致性问题
  • [C puzzle book] types
  • [DNS网络] 网页无法打开、显示不全、加载卡顿缓慢 | 解决方案
  • [I2C]I2C通信协议详解(一) --- 什么是I2C
  • [LeetCode周赛复盘] 第 312 场周赛20220925