springcloud原理篇——eureka的原理分析

前面入门系列已经讲了eureka的简单使用,这篇对于eureka的原理进行解析说明

1、Eureka架构图

springcloud原理篇——eureka的原理分析_第1张图片

官方给的架构图。至于各个流程是怎么样的,看下面

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)的服务剔除。

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

上面是对于eureka的一些核心知识进行字面表达,此时会不会觉得记忆点比较少?有点模糊?那么下面我会对于整个eureka进行形象化的解释描述。

3、形象化的eureka

该图就是对于client端是怎么和server做交互从而实现的核心功能

springcloud原理篇——eureka的原理分析_第2张图片

eureka中对外暴露的常用api如下,我们可以手动去调用其api

例如:(http://localhost:8761/eureka/  是默认的server端地址,你也可以去改相应的地址)

注册新应用实例

POST http://localhost:8761/eureka/apps/SERVICES3

请求内容格式: application/xml


	services3:7f411a785a97590bf4ae9086489e3d15
    192.168.1.7
	SERVICES3
    192.168.1.7
	UP
	UNKNOWN
	8083
	443
	1
	
	    MyOwn
	
	
	services3
	demo-order2
	false
	1541584436212
	1541584436212

常用api枚举

请求方式 url 说明
GET http://localhost:8761/eureka/apps 查询所有应用实例
GET http://localhost:8761/eureka/apps/SERVICES2 根据 AppId 查询
GET http://localhost:8761/eureka/apps/SERVICES2/services2:7f411a785a97590bf4ae5176489e3d15 根据 AppId 及 instanceId 查询
GET http://localhost:8761/eureka/instances/services2:7f411a785a97590bf4ae5176489e3d15 根据 instanceId 查询
DELETE http://localhost:8761/eureka/apps/SERVICES3/services3:7f411a785a97590bf4ae9086489e3d15 注销应用实例
POST http://localhost:8761/eureka/apps/SERVICES2 注册新应用实例
PUT http://localhost:8761/eureka/apps/SERVICES2/services2:7f411a785a97590bf4ae5176489e3d15/status?value=OUT_OF_SERVICE 暂停/下线应用实例
PUT http://localhost:8761/eureka/apps/SERVICES2/services2:7f411a785a97590bf4ae5176489e3d15/status?value=UP 恢复应用实例
PUT http://localhost:8761/eureka/apps/SERVICES2/services2:7f411a785a97590bf4ae5176489e3d15 应用实例发送心跳
PUT http://localhost:8761/eureka/apps/SERVICES2/services2:7f411a785a97590bf4ae5176489e3d15/metadata?version=1.1.1 修改应用实例元数据

4、eureka怎么实现的核心功能?(Server端)

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

而我们需要更加的理解其原理,则需要进一步去了解其源码。下面则对于server端进行简单的代码分析。

下面我们先找到源码的入口:@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 bean = new FilterRegistrationBean();
        bean.setFilter(new ServletContainer(eurekaJerseyApp));
        bean.setOrder(2147483647);
        bean.setUrlPatterns(Collections.singletonList("/eureka/*"));
        return bean;
    }

   
}

说到这里,我们需要用一个流程图记录下来我们当前的进度:

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

springcloud原理篇——eureka的原理分析_第3张图片

这里要注意的是eurekaServerBootstrap这个bean,该bean在后面会有用

上面我们的类内容看完了,现在来看看他上面的注解,其中要注意一个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");

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

这里我们补一下流程图

springcloud原理篇——eureka的原理分析_第4张图片

下面我们需要进去这两个方法看看到底是怎么实现的,点进去发现是接口,此时找其实现类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;
    }

   

这里先不讲register方法,这里该方法和clent端注册服务时的方法是同一个,只是这里是将其他节点作为client将其实例注册到本节点从而达到了同步的效果(eureka的各个server是同级别的,各自都把其他节点当成client来看待)

注意:这里的注册节点由于和client注册时是同一个方法,所以等到讲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秒续约一次)两个核心功能。

最后是对于server具体主线的流程图

springcloud原理篇——eureka的原理分析_第5张图片

5、eureka client端

上面对于eureka的server端进行简单的解析,这里对于eureka的client端进行分析

这里就不进行太细致的分析,下面是我分析源码的入口:

springcloud原理篇——eureka的原理分析_第6张图片

其中client是在DiscoveryClient中注入EurekaClient(这个为原生Eureka的客户端,用来调用Eureka server的一些api)(实际netfix之前的Eureka在没加入springcloud前是原生的Eureka),这是我们则可以用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");
        }

    }

现在先讲第一个核心功能:获取服务(拉取服务列表)

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

    }

全量获取流程在上面代码注释的很明白了,先调用server的全量获取接口(使用默认/配置的地址+接口路径,用http请求调用),然后讲返回的结果放进本地缓存的注册表中。

下面对于增量获取进行分析:
 

private void getAndUpdateDelta(Applications applications) throws Throwable {
        long currentUpdateGeneration = this.fetchRegistryGeneration.get();
     //1、本地缓存注册表  
      Applications delta = null;
  //2、eurekaTransport.queryClient.getDelta调用server端的增量接口
        EurekaHttpResponse 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());
        }

    }

该方法的主要步骤:如果本地缓存为空,证明是第一次调用,此时执行全量拉取。如果本地缓存部位null,则将调用增量拉取,拉取的结果合并到本地缓存。最后进行一致哈希码比较,如果传来的哈希码不等于本地新算的哈希码,此时证明拿到的不是最新的,则需要进行一次全量拉取。

至此服务列表拉取代码说到这里结束,太细节对的就不说了。

2、服务续约:(定时任务:每30秒执行一次)

服务续约比较简单,也是通过discoveryClient调用renew()方法对server进行续约操作

server接到请求后会通过client的ip、服务名、请求时间等信息去server的服务列表中修改相应服务实例的信息。

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

到这里我们用一个流程图来总结我们上面的所有步骤:

springcloud原理篇——eureka的原理分析_第7张图片

6、server端如何处理注册接口、拉取列表接口的逻辑

这里我不打算用源码来解释,这里我直接用流程图的形式来解释其过程:

springcloud原理篇——eureka的原理分析_第8张图片

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

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

2、读写缓存(谷歌的一个map)(二级缓存):默认180秒失效,失效后会去本地注册表中那信息。

3、本地注册表:(两层map结果)
springcloud原理篇——eureka的原理分析_第9张图片

其中内层map的value对应的类Lease需要重点理解下,里面会存放这些信息

springcloud原理篇——eureka的原理分析_第10张图片

我们调用续约等接口时就是来修改这些值。

1、注册接口的处理逻辑:

接到请求后,会调用register方法,将所有的实例信息写进注册表中。此时写完后要去清理读写缓存(二级缓存)的信息

2、拉取列表接口:

先从只读缓存获取,获取不到去读写缓存中获取,再读不到去本地注册表拿。

3、续约接口:

直接去注册表中修改相应实例信息即可。

7、重新认识eureka的核心功能:

服务注册(register)、服务续约(renew)、服务同步(replicate)、获取服务(get registry)、服务调用、服务下线(cancel)、服务剔除(evict)、自我保护。

1、服务注册(register):client端会根据配置/默认的server地址使用http请求server暴露的注册接口。然后server会在server本地注册表中添加相关的注册实例,并且清除读写缓存的信息。

2、服务续约:client通过server地址访问续约接口,server接到请求后会在本地注册表中修改相应的实例信息。(用定时任务,30秒调用一次接口)

3、服务同步(replicate):各个server将其他的server节点看出client,所以各个其他server节点也是和client一样通过调用注册接口进行注册。

4、获取服务(get registry):client调用拉取服务,分全量拉取和增量拉取(看client的注册表是否为空进行判断使用哪个),拉取服务中为了保证拉取到的是最新的服务实例列表,在server和client都会对各自的注册表中计算出一个哈希码值,如果两者哈希码相同,则会任务拉取到最新的,如过不同则会再触发一次全量拉取。client默认一个定时任务每30秒拉取一次信息。

5、服务调用:server返回给client相应的调用者信息,client根据相应信息进行调用

6、服务下线(cancel):client调用server下线接口,server会去修改相应信息

7、服务剔除(evict):server有一个定时任务,每60秒会去清除server本地的注册表中90秒没有进行续约的服务实例

8、自我保护:默认开启的自我保护机制,当短时间内,统计续约失败的比例,如果达到一定阈值,则会触发自我保护的机制,在该机制下, Eureka Server不会剔除任何的微服务,等到正常后,再退出自我保护机制。自我保护开关(eureka.server.enableself-preservation: false)(默认15分钟低于85%可用则进入自我保护机制,可以设置不开启)

8、eureka该注意的点:

多级缓存机制的优点:

尽可能保证了内存注册表数据不会出现频繁的读写冲突问题。

并且进一步保证对Eureka Server的大量请求,都是快速从纯内存走,性能极高

(可以稍微估计下对于一 线互联网公司,内部上千个eureka client实例,每分钟对eureka上千次的访问,一天就是上千万次的访问)


看完多级缓存这块源码我们可以搞清楚一个常见的问题,就是当我们eureka服务实例有注册或下线或有实例发生故 障,内存注册表虽然会及时更新数据,但是客户端不一定能及时感知到,可能会过30秒才能感知到,因为客户端拉 取注册表实例这里面有一个多级缓存机制 (AP)

还有服务剔除的不是默认90秒没心跳的实例,剔除的是180秒没心跳的实例(eureka的bug导致)

不信看看这篇文章:https://blog.csdn.net/weixin_42277648/article/details/102813216

到这里可以说对于eureka的基本原理了解的比较请求了,eureka本质就通过client调用server的接口,而且一个30秒、60秒等的都是通过timer的定时任务实现的,整体来说eureka还是比较简单的。

后面会对于hystrix、nacos等的源码进行简单的分析

 

你可能感兴趣的:(SpringCloud)