Spring Cloud 组件原理系列 Eureka篇

1,955 阅读17分钟

白菜Java自习室 涵盖核心知识

Spring Cloud 组件原理系列(一)Eureka篇
Spring Cloud 组件原理系列(二)Hystrix篇
Spring Cloud 组件原理系列(三)Feign篇

1. Eureka 简介

Eureka 是 Netflix 公司开发的,Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务注册与发现,也就是说 Spring Cloud 对 Netflix Eureka 进行了二次封装。

Eureka 是一个 服务治理组件,它主要包括服务注册与发现,主要用来搭建服务注册中心;
Eureka 是一个 基于 REST 的服务,用来定位服务进行中间层服务的负载均衡和故障转移;

Eureka 相关三大角色

  • Eureka Server:提供服务的注册与发现。
  • Eureka Client - Service Provider:将自身的服务注册到 Eureka Server 中,从而使消费者能够找到。
  • Eureka Clinet - Service Consumer:服务消费方从 Eureka Server 中获取服务注册列表,从而找到消费服务。

1085769-20190212143623520-1435837428.png

2. Eureka 核心功能点

  • 服务注册 (register):Eureka Client 会通过发送 REST 请求的方式向 Eureka Server 注册自己的服务,提供自身的元数 据,比如 ip 地址、端口、运行状况指标的 url、主页地址等信息。Eureka Server 接收到注册请求后,就会把这些元数 据信息存储在一个双层的 Map 中(注册表)。

  • 服务续约 (renew):在服务注册后,Eureka Client 会维护一个心跳来持续通知 Eureka Server,说明服务一直处于可 用状态,防止被剔除。Eureka Client 在默认的情况下会每隔30秒 (eureka.instance.leaseRenewallIntervalInSeconds) 发送一次心跳来进行服务续约。

  • 服务同步 (replicate):Eureka Server 之间会互相进行注册,构建 Eureka Server 集群,不同 Eureka Server 之间会进 行服务同步,用来保证服务信息的一致性。

  • 获取服务 (get registry):服务消费者(Eureka Client)在启动的时候,会发送一个 REST 请求给 Eureka Server,获 取上面注册的服务清单,并且缓存在 Eureka Client 本地,默认缓存 30 秒 (eureka.client.registryFetchIntervalSeconds)。同时,为了性能考虑,Eureka Server 也会维护一份只读的服务清 单缓存,该缓存每隔 30 秒更新一次。

  • 服务调用:服务消费者在获取到服务清单后,就可以根据清单中的服务列表信息,查找到其他服务的地址,从而进行 远程调用。Eureka 有 Region 和 Zone 的概念,一个 Region 可以包含多个 Zone,在进行服务调用时,优先访问处于同 一个 Zone 中的服务提供者。

  • 服务下线 (cancel):当 Eureka Client 需要关闭或重启时,就不希望在这个时间段内再有请求进来,所以,就需要提前 先发送 REST 请求给 Eureka Server,告诉 Eureka Server 自己要下线了,Eureka Server 在收到请求后,就会把该服务 状态置为下线(DOWN),并把该下线事件传播出去。

  • 服务剔除 (evict):有时候,服务实例可能会因为网络故障等原因导致不能提供服务,而此时该实例也没有发送请求给 Eureka Server 来进行服务下线,所以,还需要有服务剔除的机制。Eureka Server 在启动的时候会创建一个定时任务,每隔一段时间(默认60秒),从当前服务清单中把超时没有续约(默认90秒, 但实际代码中是180秒(eureka 的 bug,因为 eureka 已经大量使用所以没去修改)eureka.instance.leaseExpirationDurationInSeconds)的服务剔除。

  • 自我保护 (self-preservation):既然 Eureka Server 会定时剔除超时没有续约的服务,那就有可能出现一种场景,网络一段时间内发生了 异常,所有的服务都没能够进行续约,Eureka Server 就把所有的服务都剔除了,这样显然不太合理。所以,就有了自我保护机制,当短时间内,统计续约失败的比例,如果达到一定阈值,则会触发自我保护的机制,在该机制下, Eureka Server 不会剔除任何的微服务,等到正常后,再退出自我保护机制。自我保护开关 (eureka.server.enableself-preservation: false)(默认 15 分钟低于 85% 可用则进入自我保护机制,可以设置不开启)。

上面是对于 Eureka 的一些核心知识进行字面表达,此时会不会觉得记忆点比较少?有点模糊?那么下面我会对于整个 Eureka 进行形象化的解释描述。下图就是对于 client 端是怎么和 server 做交互从而实现的核心功能的形象化:

20201104134751632.png

Eureka Server REST API 介绍

OperationHTTP actionDescription
Register new application instancePOST /eureka/v2/apps/appIDInput: JSON/XML payload HTTP Code: 204 on success
De-register application instanceDELETE /eureka/v2/apps/appID/instanceIDHTTP Code: 200 on success
Send application instance heartbeatPUT /eureka/v2/apps/appID/instanceIDHTTP Code: * 200 on success * 404 if instanceID doesn’t exist
Query for all instancesGET /eureka/v2/appsHTTP Code: 200 on success Output: JSON/XML
Query for all appID instancesGET /eureka/v2/apps/appIDHTTP Code: 200 on success Output: JSON/XML
Query for a specific appID/instanceIDGET /eureka/v2/apps/appID/instanceIDHTTP Code: 200 on success Output: JSON/XML
Query for a specific instanceIDGET /eureka/v2/instances/instanceIDHTTP Code: 200 on success Output: JSON/XML
Take instance out of servicePUT /eureka/v2/apps/appID/instanceID/status?value=OUT_OF_SERVICEHTTP Code: * 200 on success * 500 on failure
Move instance back into service (remove override)DELETE /eureka/v2/apps/appID/instanceID/status?value=UP (The value=UP is optional, it is used as a suggestion for the fallback status due to removal of the override)HTTP Code: * 200 on success * 500 on failure
Update metadataPUT /eureka/v2/apps/appID/instanceID/metadata?key=valueHTTP Code: * 200 on success * 500 on failure
Query for all instances under a particular vip addressGET /eureka/v2/vips/vipAddress* HTTP Code: 200 on success Output: JSON/XML * 404 if the vipAddress does not exist.
Query for all instances under a particular secure vip addressGET /eureka/v2/svips/svipAddress* HTTP Code: 200 on success Output: JSON/XML * 404 if the svipAddress does not exist.

3. Server 端如何实现核心功能

Eureka 分为两个部分,一个 是 server 端、一个 client 端,而 server 很像一个 web 程序,对外提供一些 api 接口,client 端通过调用 server 端的 api 接口从而实现对 server 的注册、获取实例等功能。

3.1. @EnableEurekaServer 注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 这里会导入一个 EurekaServerMarkerConfiguration 类,该类中会注入一个 Marker 的 bean
@Import({EurekaServerMarkerConfiguration.class})  
public @interface EnableEurekaServer {
}

进入 EurekaServerMarkerConfiguration 类中,这里要注入一个 Maker 的 bean,该 bean 的作用是激活EurekaServerAutoConfiguration(重要:即 server 端的自动配置类)

@Bean
public EurekaServerMarkerConfiguration.Marker eurekaServerMarkerBean() {
    return new EurekaServerMarkerConfiguration.Marker();
}

    class Marker {
        Marker() {
        }
    }

此时我们激活 server 端的自动配置类的条件已经满足了,进入 EurekaServerAutoConfiguration 看看里面有什么?这里我们只关注几个 bean,其他代码省略。

@Configuration(
    proxyBeanMethods = false
)
@Import({EurekaServerInitializerConfiguration.class})
@ConditionalOnBean({Marker.class})
@EnableConfigurationProperties({EurekaDashboardProperties.class, InstanceRegistryProperties.class})
@PropertySource({"classpath:/eureka/server.properties"})
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
    
    //(1)加载EurekaController, spring‐cloud提供了一些额外的接口,用来获取eurekaServer的信息
    @Bean
    @ConditionalOnProperty(
        prefix = "eureka.dashboard",
        name = {"enabled"},
        matchIfMissing = true
    )
    public EurekaController eurekaController() {
        return new EurekaController(this.applicationInfoManager);
    }
 
    //(2) 初始化集群注册表   
    @Bean
    public PeerAwareInstanceRegistry peerAwareInstanceRegistry(ServerCodecs serverCodecs) {
        this.eurekaClient.getApplications();
        return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig, serverCodecs, this.eurekaClient, this.instanceRegistryProperties.getExpectedNumberOfClientsSendingRenews(), this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
    }
 
    //(3)配置服务节点信息,这里的作用主要是为了配置Eureka的peer节点,
    // 也就是说当有收到有节点注册上来的时候,需要通知给那些服务节点(互为一个集群)
    @Bean
    @ConditionalOnMissingBean
    public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry, ServerCodecs serverCodecs, ReplicationClientAdditionalFilters replicationClientAdditionalFilters) {
        return new EurekaServerAutoConfiguration.RefreshablePeerEurekaNodes(registry, this.eurekaServerConfig, this.eurekaClientConfig, serverCodecs, this.applicationInfoManager, replicationClientAdditionalFilters);
    }
 
    //(4)EurekaServer 的上下文
    @Bean
    public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs, PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
        return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs, registry, peerEurekaNodes, this.applicationInfoManager);
    }
 
    //(5)这个类的作用是spring‐cloud和原生eureka的胶水代码,通过这个类来启动EurekaSever,
    // 后面这个类会在EurekaServerInitializerConfiguration被调用,进行eureka启动
    @Bean
    public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry, EurekaServerContext serverContext) {
        return new EurekaServerBootstrap(this.applicationInfoManager, this.eurekaClientConfig, this.eurekaServerConfig, registry, serverContext);
    }
 
    //(6)配置拦截器,ServletContainer里面实现了jersey框架,通过他来实现eurekaServer对外的restFull接口
    @Bean
    public FilterRegistrationBean<?> jerseyFilterRegistration(Application eurekaJerseyApp) {
        FilterRegistrationBean<Filter> bean = new FilterRegistrationBean();
        bean.setFilter(new ServletContainer(eurekaJerseyApp));
        bean.setOrder(2147483647);
        bean.setUrlPatterns(Collections.singletonList("/eureka/*"));
        return bean;
    }
 
}

整个过程:主要即注册一个 Maker 的 bean 去激活 EurekaServerAutoConfiguration,然后 EurekaServerAutoConfiguration 类中分别注册了多个初始化 bean。

Eureka 自动配置流程:

  1. @EnableEurekaServer 注解;

  2. @Import 导入 EurekaServerMarkerConfiguration;

  3. 注入 Marker,初始化 @Bean;

  4. 激活 EurekaServerAutoConfiguration:

  • EurekaServerConfig:初始化 eurekaServer 的相关配置
  • EurekaController:初始化一些接口,用来获取 eurekaServer 的信息
  • PeerAwareInstanceRegistry:初始化集群注册表
  • PeerEurekaNodes:初始化集群节点集合
  • EurekaServerContext:eurekaServer 的上下文信息
  • EurekaServerBootstrap:初始化 spring cloud 包装的 eureka 启动类
  • FilterRegistrationBean:初始化 jersey 过滤器

3.2. EurekaServerInitializerConfiguration 类

注意这个 EurekaServerInitializerConfiguration 类,因为他是 服务同步和服务剔除 的调用关键。

@Configuration(
    proxyBeanMethods = false
)
@Import({EurekaServerInitializerConfiguration.class})
@ConditionalOnBean({Marker.class})
@EnableConfigurationProperties({EurekaDashboardProperties.class, InstanceRegistryProperties.class})
@PropertySource({"classpath:/eureka/server.properties"})
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
    // ......
}

其中会使用前面注册的 eurekaServerBootstrap 去调用 contextInitialized() 方法(该方法回去初始化 server 端的运行环境和上下文)

public class EurekaServerInitializerConfiguration 
    implements ServletContextAware, SmartLifecycle, Ordered {
 
    // 因为实现了SmartLifecycle接口,
    // 所以会在初始化完后去调用isAutoStartup如果为true,则会去调用该start方法
    public void start() {
        (new Thread(() -> {
            try {
                this.eurekaServerBootstrap.contextInitialized(this.servletContext);
                log.info("Started Eureka Server");
                this.publish(new EurekaRegistryAvailableEvent(this.getEurekaServerConfig()));
                this.running = true;
                this.publish(new EurekaServerStartedEvent(this.getEurekaServerConfig()));
            } catch (Exception var2) {
                log.error("Could not initialize Eureka servlet context", var2);
            }
 
        })).start();
    }
 
 
    public boolean isAutoStartup() {
        return true;
    }
 
}

所以我们需要进去 contextInitialized() 看看,这里的 server 端上下文很重要,里面就有关于服务同步和服务剔除的设置。

   public void contextInitialized(ServletContext context) {
        try {
            // 1、初始化server端的运行环境
            this.initEurekaEnvironment();
            // 2、初始化server端的上下文
            this.initEurekaServerContext();
            context.setAttribute(EurekaServerContext.class.getName(), this.serverContext);
        } catch (Throwable var3) {
            log.error("Cannot bootstrap eureka server :", var3);
            throw new RuntimeException("Cannot bootstrap eureka server :", var3);
        }
    }

我们需要进入 initEurekaServerContext() 上下文方法中看看,服务同步和服务剔除是怎么进行设置的。

    protected void initEurekaServerContext() throws Exception {
 
        // 1、初始化eureka server上下文
        EurekaServerContextHolder.initialize(this.serverContext);
        log.info("Initialized server context");

        // 2、从相邻的eureka节点复制注册表(服务同步方法)
        int registryCount = this.registry.syncUp();
        
        // 3、 默认每30秒发送心跳,1分钟就是2次 (服务续约、服务剔除)
        // 修改 eureka 状态为 up,同时开启一个定时任务,用于清理60秒没有心跳的客户端,自动下线
        this.registry.openForTraffic(this.applicationInfoManager, registryCount);
        EurekaMonitors.registerAllStats();
    }

SpringBoot 自动配置 Eureka 流程:

  1. @SpringBootApplication

  2. 组合注解 @EnableAutoConfiguration

  3. @Import 注解 指向 AutoConfigurationImportSelector

  4. 扫描所有包里的 spring.factories 文件

  5. 扫描到了 spring-cloud-netfix-eureka-server.jar 里的 spring.factories 文件,文件里的 EnableAutoConfiguration 对应 EurekaServerAutoConfiguration,此时就会进行如下初始化:

  • EurekaServerConfig:初始化 eurekaServer 的相关配置

  • EurekaController:初始化一些接口,用来获取 eurekaServer 的信息

  • PeerAwareInstanceRegistry:初始化集群注册表

  • PeerEurekaNodes:初始化集群节点集合

  • EurekaServerContext:eurekaServer 的上下文信息

  • EurekaServerBootstrap:初始化 spring cloud 包装的 eureka 启动类

  • FilterRegistrationBean:初始化 jersey 过滤器

  1. @Import 注解 指向 EurekaServerInitializerConfiguration

  2. EurekaServerInitializerConfiguration 实现了接口 SmartLifecycle,根据 isAutoStartup() 方法的返回值,去自动调用 start() 方法

  3. 初始化 EurekaServer,调用 eurekaServerBootstrap.contextInitialized(),此时主要做几件事:

  • initEurekaEnvironment():初始化 EurekaServer 的运行环境
  • initEurekaServerContext():初始化 EurekaServer 的上下文: (1)syncUp():从相邻的eureka节点复制注册表到本地(服务同步方法)
    (2)openForTraffic():默认每30秒发送心跳,1分钟就是2次 (服务续约、服务剔除),修改 eureka 状态为 up,同时开启一个定时任务,用于清理60秒没有心跳的客户端,自动下线

我们需要知道 syncUp() 和 openForTraffic() 这两个方法看看到底是怎么实现的,点进去发现是接口,此时找其实现类 PeerAwareInstanceRegistryImpl 的

syncUp() 服务同步方法

 public int syncUp() {
 
        // 省略代码
        
        while(var4.hasNext()) {
            Application app = (Application)var4.next();
            Iterator var6 = app.getInstances().iterator();

            while(var6.hasNext()) {
                InstanceInfo instance = (InstanceInfo)var6.next();

                try {
                    if (this.isRegisterable(instance)) {

                        // 只需要关注该方法:将其他节点的实例注册到本节点
                        this.register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
                        ++count;
                    }
                } catch (Throwable var9) {
                    logger.error("During DS init copy", var9);
                }
            }
        }
    }

    return count;
}

注意:这里的注册节点由于和 client 注册时是同一个方法,所以等到讲 client 端时再去说明该方法的实现(eureka 的各个 server 是同级别的,各自都把其他节点当成 client 来看待)。

openForTraffic() 服务续约、剔除方法

 public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
        this.expectedNumberOfClientsSendingRenews = count;
        this.updateRenewsPerMinThreshold();
        logger.info("Got {} instances from neighboring DS node", count);
        logger.info("Renew threshold is: {}", this.numberOfRenewsPerMinThreshold);
        this.startupTime = System.currentTimeMillis();
        if (count > 0) {
            this.peerInstancesTransferEmptyOnStartup = false;
        }
 
        Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
        boolean isAws = Name.Amazon == selfName;
        if (isAws && this.serverConfig.shouldPrimeAwsReplicaConnections()) {
            logger.info("Priming AWS connections for all replicas..");
            this.primeAwsReplicas(applicationInfoManager);
        }
 
        logger.info("Changing status to UP");
        // 1、设置实例的状态为UP
        applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
        // 2、 开启定时任务,默认60秒执行一次,用于清理60秒之内没有续约的实例
        // 这里是每60秒对90秒内没有续约的数据进行清除
        super.postInit();
    }

我们进入 postInit() 方法:

protected void postInit() {
        this.renewsLastMin.start();
        if (this.evictionTaskRef.get() != null) {
            ((AbstractInstanceRegistry.EvictionTask)this.evictionTaskRef.get()).cancel();
        }
 
        this.evictionTaskRef.set(new AbstractInstanceRegistry.EvictionTask());
 
        // 定时任务中进行服务剔除任务,定时时间是getEvictionIntervalTimerInMs,默认是60秒
        // 60秒进行一次剔除90秒没续约的服务
        this.evictionTimer.schedule((TimerTask)this.evictionTaskRef.get(), this.serverConfig.getEvictionIntervalTimerInMs(), this.serverConfig.getEvictionIntervalTimerInMs());
    }

至此,我们的 server 端的主线进行分析。在 server 端中我们主要对于服务同步(具体实现后面在说)、服务剔除(用定时任务每 60 秒剔除一次失效的服务(90 秒没续约的服务))(默认 30 秒续约一次)两个核心功能。

4. Client 端如何实现核心功能

上面对于 eureka 的 server 端自动配置流程的解析,这里对于 eureka 的 client 端进行分析:

  1. (SpringBoot 自动配置加载不再重复说明)

  2. 针对客户端,扫描到 spring-cloud-netfix-eureka-client.jar 里的 spring.factories 文件,文件里的 EnableAutoConfiguration 对应 EurekaClientAutoConfiguration, 此时就会进行如下初始化:

  • EurekaClientConfigBean:初始化 eurekaClient 的相关配置

  • EurekaClient 的初始化:注入 DiscoveryClient(Spring Cloud 对原生 EurekaClient 的包装类)

  1. @AutoConfigurationAfter, 同理使用 Marker 来 EnableDiscoveryClientConfiguration;

其中 client 是在 DiscoveryClient 中注入 EurekaClient(这个为原生 Eureka 的客户端,用来调用Eureka server 的一些 API),这时我们则可以用 DiscoveryClient 来调用 server 端的 API。

我们这里直接进入其中的 initScheduledTasks() 方法,该方法涉及服务获取、续约、注册等核心:

private void initScheduledTasks() {

        int renewalIntervalInSecs;
        int expBackOffBound;
        
        if (this.clientConfig.shouldFetchRegistry()) {

        // 服务注册列表更新的周期时间
        renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();
        expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();

        // 1、定时更新服务注册列表(获取服务列表)
        // 其中CacheRefreshThread()方法是该线程执行更新的具体逻辑
        this.scheduler.schedule(new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
    }

    if (this.clientConfig.shouldRegisterWithEureka()) {
    
        // 2、服务续约的周期时间
        renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        logger.info("Starting heartbeat executor: renew interval is: {}", renewalIntervalInSecs);
        
        // 服务定时续约,其中HeartbeatThread()方法是对续约的具体方法
        this.scheduler.schedule(new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);

        this.instanceInfoReplicator = new InstanceInfoReplicator(this, this.instanceInfo, this.clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
        this.statusChangeListener = new StatusChangeListener() {
            public String getId() {
                return "statusChangeListener";
            }

            public void notify(StatusChangeEvent statusChangeEvent) {
                if (InstanceStatus.DOWN != statusChangeEvent.getStatus() && InstanceStatus.DOWN != statusChangeEvent.getPreviousStatus()) {
                    DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent);
                } else {
                    DiscoveryClient.logger.warn("Saw local status change event {}", statusChangeEvent);
                }

                DiscoveryClient.this.instanceInfoReplicator.onDemandUpdate();
            }
        };
        if (this.clientConfig.shouldOnDemandUpdateStatusChange()) {
            this.applicationInfoManager.registerStatusChangeListener(this.statusChangeListener);
        }

        // 3、start方法是进行服务注册
        this.instanceInfoReplicator.start(this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        logger.info("Not registering with Eureka server per configuration");
    }

}

4.1. 拉取服务列表

拉取服务列表定时的使用 DiscoveryClient 调用 CacheRefreshThread() 方法:

class CacheRefreshThread implements Runnable {
        CacheRefreshThread() {
        }
 
        public void run() {
            DiscoveryClient.this.refreshRegistry();
        }
    }

再进去 refreshRegistry() 方法:

@VisibleForTesting
    void refreshRegistry() {

             // 获取注册信息方法
            boolean success = this.fetchRegistry(remoteRegionsModified);
            if (success) {
                this.registrySize = ((Applications)this.localRegionApps.get()).size();
                this.lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();
            }
    }

进入 fetchRegistry() 获取注册信息方法:

private boolean fetchRegistry(boolean forceFullRegistryFetch) {
        Stopwatch tracer = this.FETCH_REGISTRY_TIMER.start();
 
        label122: {
            boolean var4;
            try {
                 // 取出本地缓存之前获取的服务列表信息
 
                Applications applications = this.getApplications();
                
                // 判断多个条件,确定是否触发全量更新,如下任一个满足都会全量更新:
                // 1. 是否禁用增量更新;
                // 2. 是否对某个region特别关注;
                // 3. 外部调用时是否通过入参指定全量更新;
                // 4. 本地还未缓存有效的服务列表信息;
                if (!this.clientConfig.shouldDisableDelta() && Strings.isNullOrEmpty(this.clientConfig.getRegistryRefreshSingleVipAddress()) && !forceFullRegistryFetch && applications != null && applications.getRegisteredApplications().size() != 0 && applications.getVersion() != -1L) {                     
                    // 1、增量更新
                    this.getAndUpdateDelta(applications);
                } else {
                    // 2、全量更新
                    this.getAndStoreFullRegistry();
                }
                    // 3、重新计算和设置一致性Hash码
                applications.setAppsHashCode(applications.getReconcileHashCode());
                this.logTotalInstances();
                break label122;
            } catch (Throwable var8) {
                logger.error("DiscoveryClient_{} - was unable to refresh its cache! status = {}", new Object[]{this.appPathIdentifier, var8.getMessage(), var8});
                var4 = false;
            } finally {
                if (tracer != null) {
                    tracer.stop();
                }
            }
            return var4;
        }
 
        this.onCacheRefreshed();
        this.updateInstanceRemoteStatus();
        return true;
    }

这里有一个重点,如果一个 client 在对 server 进行拉取注册表时,获取本地缓存找是否有信息,没有则为第一次去拉取,此时调用全量拉取方法 getAndStoreFullRegistry()(即拉取所有列表信息),如果本地有则调用增量拉取 getAndUpdateDelta() 方法

注意:每次拉取完后会根据 client 端的本地缓存的注册表进行计算出一个 hashcode 值,这个值很重要,再后面会提到。

getAndStoreFullRegistry() 全量拉取方法

 private void getAndStoreFullRegistry() throws Throwable {
        long currentUpdateGeneration = this.fetchRegistryGeneration.get();
        logger.info("Getting all instance registry info from the eureka server");
        // 1、这个Applications 就是client本地缓存的注册表
        Applications apps = null;
        // 2、eurekaTransport.queryClient.getApplications方法就是调用了server端的全量获取接口
        EurekaHttpResponse<Applications> httpResponse = this.clientConfig.getRegistryRefreshSingleVipAddress() == null ? this.eurekaTransport.queryClient.getApplications((String[])this.remoteRegionsRef.get()) : this.eurekaTransport.queryClient.getVip(this.clientConfig.getRegistryRefreshSingleVipAddress(), (String[])this.remoteRegionsRef.get());
        if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
          // 3、如果获取成功,则会放入本地缓存的注册表中          
          apps = (Applications)httpResponse.getEntity();
        } 
    }

getAndUpdateDelta() 增量拉取方法

private void getAndUpdateDelta(Applications applications) throws Throwable {
        long currentUpdateGeneration = this.fetchRegistryGeneration.get();
      // 1、本地缓存注册表  
      Applications delta = null;
        // 2、eurekaTransport.queryClient.getDelta调用server端的增量接口
        EurekaHttpResponse<Applications> httpResponse = this.eurekaTransport.queryClient.getDelta((String[])this.remoteRegionsRef.get());
        if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
            // 3、将增量合并到注册表中,注意:不管是全量还是增量,server都会传来以一个hashcode值
            // 其值是根据server中的本地注册表实例计算来的(和我们之前说的client端也会根据本地的注册实例注册一个hashcode值)
            // 此时如果server传来的hashcode值=client接收到实例后新计算的hashcode值,则证明我们拿到的是最新的实例注册表,
            // 如果不等于则表示拿的不是最新的,此时会再进行一次全量拉取请求
            delta = (Applications)httpResponse.getEntity();
        }
        // 4、如果本地缓存的注册表为空,即第一次请求,此时直接调用全量拉取方法
        if (delta == null) {
            logger.warn("The server does not allow the delta revision to be applied because it is not safe. Hence got the full registry.");
            this.getAndStoreFullRegistry();
        } else if (this.fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1L)) {
            logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
            String reconcileHashCode = "";
            if (this.fetchRegistryUpdateLock.tryLock()) {
                try {
                    this.updateDelta(delta);
                    reconcileHashCode = this.getReconcileHashCode(applications);
                } finally {
                    this.fetchRegistryUpdateLock.unlock();
                }
            } else {
                logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
            }
            // 5、一致性哈希码不同,就在reconcileAndLogDifference方法中做全量更新
            if (!reconcileHashCode.equals(delta.getAppsHashCode()) || this.clientConfig.shouldLogDeltaDiff()) {
                this.reconcileAndLogDifference(delta, reconcileHashCode);
            }
        } else {
            logger.warn("Not updating application delta as another thread is updating it already");
            logger.debug("Ignoring delta update with apps hashcode {}, as another thread is updating it already", delta.getAppsHashCode());
        }
 
    }

该方法的主要步骤:

  1. 如果本地缓存为空,证明是第一次调用,此时执行全量拉取。
  2. 如果本地缓存不为空,则将调用增量拉取,拉取的结果合并到本地缓存。
  3. 最后进行一致哈希码比较,如果传来的哈希码不等于本地新算的哈希码,此时证明拿到的不是最新的,则需要进行一次全量拉取(其实是伪增量)。

4.2. 服务续约

定时任务:每30秒执行一次

服务续约比较简单,也是通过 discoveryClient 调用 renew() 方法对 server 进行续约操作。 server 接到请求后会通过 client 的 ip、服务名、请求时间等信息去 server 的服务列表中修改相应服务实例的信息。

4.3. 服务注册

服务注册则通过一个线程中的 run() 方法去调用 server 的一个 regiester() 方法,server 端接到请求后会向注册表中添加相应实例信息。

5. Server 端如何处理注册接口、拉取列表接口

20201104172011868.png

在说明两个接口的处理逻辑前,我们先需要明白 eureka 的内部构造,eureka 中存在两个缓存:

  • 只读缓存(concurrenthashmap)(一级缓存):默认30秒失效,失效后会从二级缓存读写缓存中获取(每30秒使用定时任务进行获取),再获取不到则直接到本地注册表中(gMap:也是一个map)获取。
  • 读写缓存(谷歌的一个map)(二级缓存):默认180秒失效,失效后会去本地注册表中那信息。

注册续约和拉取列表逻辑

  1. 注册接口:接到请求后,会调用 register() 方法,将所有的实例信息写进注册表中。此时写完后要去清理读写缓存(二级缓存)的信息。
  2. 拉取列表接口:先从只读缓存获取,获取不到去读写缓存中获取,再读不到去本地注册表拿。
  3. 续约接口:直接去注册表中修改相应实例信息即可。

多级缓存机制的优点:

  • 尽可能保证了内存注册表数据不会出现频繁的读写冲突问题;
  • 并且进一步保证对Eureka Server的大量请求,都是快速从纯内存走,性能极高。

多级缓存机制常见问题:

就是当我们eureka服务实例有注册或下线或有实例发生故障,内存注册表虽然会及时更新数据,但是客户端不一定能及时感知到,可能会过30秒才能感知到,因为客户端拉 取注册表实例这里面有一个多级缓存机制。

Spring Cloud 组件原理系列(一)Eureka篇
Spring Cloud 组件原理系列(二)Hystrix篇
Spring Cloud 组件原理系列(三)Feign篇