源码解析-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);
    }
复制代码

 

posted @   幻月hah  阅读(222)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
历史上的今天:
2019-04-01 打开一个vue项目
2019-04-01 VUE开发环境配置
点击右上角即可分享
微信分享提示