• 二、Eureka Client源码解析


    一、Eureka Client源码的重要API

    1、InstanceInfo

    该类用于保存一个微服务主机的信息。一个该类实例就代表了一个微服务主机。该主机注册到Eureka Server就是将其InstanceInfo写入到了Eureka注册表,且被其它Server读取到的该Server的信息也是这个InstanceInfo。 

      // 记录当前InstanceInfo在Server端被修改的时间戳
        private volatile Long lastUpdatedTimestamp;
        // 记录当前InstanceInfo在Client端被修改的时间戳
        private volatile Long lastDirtyTimestamp;
    
        // 记录当前Client在Server端的状态
        private volatile InstanceStatus status = InstanceStatus.UP;
        // 该状态用于计算Client在Server端的状态status(在Client提交注册请求与Renew续约请求 时)
        private volatile InstanceStatus overriddenStatus = InstanceStatus.UNKNOWN;
      // 重写了euqals()方法:只要两个InstanceInfo的instanceId相同,那么这两个 InstanceInfo就相同
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            InstanceInfo other = (InstanceInfo) obj;
            String id = getId();
            if (id == null) {
                if (other.getId() != null) {
                    return false;
                }
            } else if (!id.equals(other.getId())) {
                return false;
            }
            return true;
        }

      

    2、Application

    一个Application实例中保存着一个特定微服务的所有提供者实例。 
        // 保存着当前name所指定的微服务名称的所有InstanceInfo
        private final Set<InstanceInfo> instances;
    
        // key为instanceId,value为instanceInfo
        private final Map<String, InstanceInfo> instancesMap;

      

    3、Applications

    该类封装了来自于Eureka Server的所有注册信息。我们可以称其为“客户端注册表”。只所以要强调“客户端”是因为,服务端的注册表不是这样表示的,是一个Map。
        // key为微服务名称,value为Application
        private final Map<String, Application> appNameApplicationMap;

    4、Jersey框架

    Spring Cloud中Eureka Client与Eureka Server的通信,及Eureka Server间的通信,均采用的是Jersey框架。

    Jersey框架是一个开源的RESTful框架,实现了JAX-RS规范。该框架的作用与SpringMVC是相同的,其也是用户提交URI后,在处理器中进行路由匹配,路由到指定的后台业务。这个路由功能同样也是通过处理器完成的,只不过这里的处理器不叫Controller,而叫Resource。 

    二、Eureka Client解析入口中的重要类

    1、EurekaClientAutoConfiguration类

     

    这是一个配置类,在应用启动时会创建该类中满足条件的Bean。其中就包含EurekaClient。

    2、封装配置文件中的两个属性的类

    EurekaClientConfigBean 

    其读取的是eureka.client前辍的配置信息。这个类已经被@ConfigurationProperties注解了,所以这些配置信息可以被自动封装并注册到容器。

    EurekaInstanceConfigBean

    其读取的是eureka.instance的属性值。这个类也已经被@ConfigurationProperties注解了,所以这些配置信息可以被自动封装并注册到容器。 

    3、EurekaClient 

    这就是我们寻找的客户端实例。默认情况下创建的Eureka Client本身可以实现动态更新,即配置文件中相关的配置信息发生了变更,这个Client注册到Server的信息也会自动变更到Eureka Server的注册表中。 

    三、Eureka Client源码解析 

    1、解析总流程

    注:强制注册失败时,会抛出异常, 无法执行后续流程。如定时更新等。

    2、定时更新“客户端注册表”解析

        //  定时更新“客户端注册表”解析
            if (clientConfig.shouldFetchRegistry()) {
                // registry cache refresh timer,客户端从服务端下载更新注册表的时间间隔,默认为30s
                int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
                //  指定client从server更新注册表的最大时间间隔指数(倍数),默认为10倍
                int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
                //  创建任务
                cacheRefreshTask = new TimedSupervisorTask(
                        "cacheRefresh",
                        scheduler,
                        cacheRefreshExecutor,
                        registryFetchIntervalSeconds,
                        TimeUnit.SECONDS,
                        expBackOffBound,
                        new CacheRefreshThread()
                );
                //  开启一次性的定时任务
                scheduler.schedule(
                        cacheRefreshTask,
                        registryFetchIntervalSeconds, TimeUnit.SECONDS);
            }

    思考:为什么一次性的定时任务能循环多次执行?

    TimedSupervisorTask的run()方法,在finally()中会循环开启任务。
    @Override
        public void run() {
            Future<?> future = null;
            try {
                future = executor.submit(task);
                threadPoolLevelGauge.set((long) executor.getActiveCount());
                future.get(timeoutMillis, TimeUnit.MILLISECONDS);  // block until done or timeout
                delay.set(timeoutMillis);
                threadPoolLevelGauge.set((long) executor.getActiveCount());
                successCounter.increment();
            } catch (TimeoutException e) {
                logger.warn("task supervisor timed out", e);
                timeoutCounter.increment();
    
                long currentDelay = delay.get();
                long newDelay = Math.min(maxDelay, currentDelay * 2);
                delay.compareAndSet(currentDelay, newDelay);
    
            } catch (RejectedExecutionException e) {
                if (executor.isShutdown() || scheduler.isShutdown()) {
                    logger.warn("task supervisor shutting down, reject the task", e);
                } else {
                    logger.warn("task supervisor rejected the task", e);
                }
    
                rejectedCounter.increment();
            } catch (Throwable e) {
                if (executor.isShutdown() || scheduler.isShutdown()) {
                    logger.warn("task supervisor shutting down, can't accept the task");
                } else {
                    logger.warn("task supervisor threw an exception", e);
                }
    
                throwableCounter.increment();
            } finally {
                if (future != null) {
                    future.cancel(true);
                }
                //  如果定时器未关闭,则循环执行
                if (!scheduler.isShutdown()) {
                    scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
                }
            }
        }

    3、定时续约解析

    //  默认心跳间隔,30秒
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);
    
            // Heartbeat timer
            heartbeatTask = new TimedSupervisorTask(
                    "heartbeat",
                    scheduler,
                    heartbeatExecutor,
                    renewalIntervalInSecs,
                    TimeUnit.SECONDS,
                    expBackOffBound,
                    new HeartbeatThread()
            );
            scheduler.schedule(
                    heartbeatTask,
                    renewalIntervalInSecs, TimeUnit.SECONDS);

    4、定时更新Client信息给Server

    • 通过监听器监听配置文件变更,如果发生变更,则Client提交register()。
    • 默认每30秒定时更新一次。
    • 通过限流器,限制配置文件频繁变更

    5、总结

    Client提交register()请求的情况有三种:

    • 在应用启动时就可以直接进行register(),不过,需要提前在配置文件中配置
    • 在renew时,如果server端返回的是NOT_FOUND,则提交register()
    • 当Client的配置信息发生了变更,则Client提交register()

    四、服务离线源码解析 

    服务离线,即某服务不能对外提供服务了。服务离线的原因有两种:服务下架与服务下线。  

    1、基于Actuator监控器实现

    提交如下POST请求,可实现相应的服务离线操作。
    • 服务下架:http://localhost:8081/actuator/shutdown,无需请求体
    • 服务下线:http://localhost:8081/actuator/serviceregistry,请求体为
    //  下线
      {
        "status":"OUT_OF_SERVICE"
      } 
    //  上线
      {
        "status":"UP"
      }

    需要引入actuator依赖,并开启配置

    management:
      endpoints:
        web:
          exposure:
            include: "*"
    
      # 开启shutdown监控终端
      endpoint:
        shutdown:
          enabled: true

    注意:从Spring Cloud 2020.0.0版本开始,服务平滑上下线的监控终端由sevice-registry变更为了serviceregistry

    2、直接向EurekaServer提交请求 

    可以通过直接向Eureka Server提交不同的请求的方式来实现指定服务离线操作。

    • 服务下架:通过向eureka server发送DELETE请求来删除指定client的服务
      http://${server}:${port}/eureka/apps/${serviceName}/${instanceId}
    • 服务下线:通过向eureka server发送PUT请求来修改指定client的status,其中${value}的取值为:OUT_OF_SERVICE或UP
      http://${server}:${port}/eureka/apps/${serviceName}/${instanceId}/status?value=${value}

    3、特殊状态CANCEL_OVERRIDE 

    用户提交的状态修改请求中指定的状态,除了InstanceInfo的内置枚举类InstanceStatus中定义的状态外,还可以是CANCEL_OVERRIDE状态。

    若用户提交的状态为CANCEL_OVERRIDE,则Client会通过Jersey向Server提交一个DELETE请求,用于在Server端将对应InstanceInfo的overridenStatus修改为UNKNWON,即删除了原来的overridenStatus的状态值。此时,该Client发送的心跳Server是不接收的。Server会向该Client返回404。

    4、服务下架源码解析

    @PreDestroy
        @Override
        public synchronized void shutdown() {
            if (isShutdown.compareAndSet(false, true)) {
                logger.info("Shutting down DiscoveryClient ...");
    
                if (statusChangeListener != null && applicationInfoManager != null) {
                    //  注销监听器
                    applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
                }
    
                //  关闭定时任务等
                cancelScheduledTasks();
    
                // If APPINFO was registered
                if (applicationInfoManager != null
                        && clientConfig.shouldRegisterWithEureka()
                        && clientConfig.shouldUnregisterOnShutdown()) {
                    applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
                    //  注销服务
                    unregister();
                }
    
                if (eurekaTransport != null) {
                    //  下架
                    eurekaTransport.shutdown();
                }
    
                heartbeatStalenessMonitor.shutdown();
                registryStalenessMonitor.shutdown();
    
                Monitors.unregisterObject(this);
    
                logger.info("Completed shut down of DiscoveryClient");
            }
        }

    5、服务下线源码解析

     应用不会挂掉,只是修改状态,不再被Euraka发现。

    @Override
        public void setStatus(EurekaRegistration registration, String status) {
            InstanceInfo info = registration.getApplicationInfoManager().getInfo();
    
            // TODO: howto deal with delete properly?
            if ("CANCEL_OVERRIDE".equalsIgnoreCase(status)) {
                registration.getEurekaClient().cancelOverrideStatus(info);
                return;
            }
    
            // TODO: howto deal with status types across discovery systems?
            InstanceInfo.InstanceStatus newStatus = InstanceInfo.InstanceStatus.toEnum(status);
            registration.getEurekaClient().setStatus(newStatus, info);
        }
  • 相关阅读:
    阿里云物联网平台: 使用阿里云物联网平台提供的自定义Topic通信控制(Air202,TCP透传指令)
    ESA2GJK1DH1K基础篇: 源码使用注意事项和程序优化
    ESP8266 SDK开发: 常见问题及程序BUG修复
    ESP8266 SDK开发: 微信小程序篇-微信小程序通过UDP实现和ESP8266局域网通信控制
    ESP8266 SDK开发: 物联网篇-ESP8266连接阿里云物联网平台,使用阿里云提供的物模型Topic通信控制
    ESP8266 SDK开发: 物联网篇-ESP8266连接阿里云物联网平台,使用阿里云提供的自定义Topic通信控制
    ESA2GJK1DH1K基础篇: APP使用SmartConfig绑定Wi-Fi 设备并通过MQTT控制设备(V1.0)(AT+TCP非透传指令)
    【spring源码分析】@Value注解原理
    【vue】npm、node版本查看及npm常用命令
    【vue】常用操作
  • 原文地址:https://www.cnblogs.com/shiblog/p/16086843.html
Copyright © 2020-2023  润新知