Eureka

  • 1. Eureka简介
    • 1.1 什么是Eureka
  • 2. Eureka实例
    • 2.1 编写Eureka Server
    • 2.2 编写Eureka Client
  • 3. Eureka源码解析
    • 3.1 Eureka的部分概念
    • 3.2 服务启动
      • 服务注册启动
      • SmartLifecycle接口
      • DefaultLifeCycleProcessor.doStart()
    • 3.3 服务注册
      • EurekaAutoServiceRegistration.start
      • EurekaServiceRegistry.register
      • EurekaClientConfiguration
      • CloudEurekaClient
      • DiscoveryClient
      • DiscoveryClient.initScheduledTasks
      • instanceInfoReplicator.onDemandUpdate()
      • InstanceInfoReplicator.this.run()
      • discoveryClient.register()
      • AbstractJerseyEurekaHttpClient.register()
    • 3.4 Eureka Server收到请求之后的处理
      • ApplicationResource.addInstance()
      • PeerAwareInstanceRegistryImpl.register
      • AbstractInstanceRegistry.register
    • 3.5 多级缓存设计
      • 多级缓存的意义
      • 服务注册的缓存失效
      • 定时同步缓存
    • 3.6 服务续约
      • DiscoveryClient.initScheduledTasks
      • 服务端收到心跳请求的处理
      • InstanceResource.statusUpdate()
      • AbstractInstanceRegistry.statusUpdate
    • 3.7 服务发现
      • DiscoveryClient构造时进行查询
      • 定时任务每隔30s更新一次本地地址列表
      • TimedSupervisorTask
      • CacheRefreshThread.refreshRegistry
      • DisccoveryClient.fetchRegistry
      • DiscoveryClient.getAndStoreFullRegistry
      • 服务端查询服务地址流程
      • ApplicationsResource.getContainers
      • responseCache.getGZIP

1. Eureka简介

1.1 什么是Eureka

和Consul,zookeeper类似,Eureka是一个用于服务注册及发现的组件,最开始主要应用于亚马逊公司旗下的云计算服务平台AWS。Eureka分为Eureka Server和Eureka Client。其中server是Eureka的服务注册中心,Client是客户端。

2. Eureka实例

2.1 编写Eureka Server

实例中存在多个Spring Boot工程,为了方便管理采取Maven多Module的结构,所以首先创建一个父工程
结构如下:

首先创建父工程demo
引入pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version><name>eureka</name><description>Demo project for Spring Boot</description><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.3.RELEASE</version><relativePath/></parent><properties><project.build.sourceEcoding>UTF-8</project.build.sourceEcoding><project.reporting.outputEcoding>UTF-8</project.reporting.outputEcoding><java.version>1.8</java.version><spring-cloud.version>Hoxton.SR4</spring-cloud.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
</project>

创建完父工程之后创建子工程server
引入server的pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>eureka-server</artifactId><version>0.0.1-SNAPSHOT</version><name>eureka-server</name><description>Demo project for Spring Boot</description><parent><groupId>com.example</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version><relativePath/></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

在工程的配置application.yml

server:port: 8761eureka:instance:prefer-ip-address: truehostname: localhostclient:registerWithEureka: falsefetchRegistry: falseserviceUrl:defaultZone:http://localhost:8761/

启动类

package com.example.eurekaserver;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {public static void main(String[] args) {SpringApplication.run(EurekaServerApplication.class, args);}
}

启动。。。

2.2 编写Eureka Client

引入pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>eureka-client</artifactId><version>0.0.1-SNAPSHOT</version><name>eureka-client</name><description>Demo project for Spring Boot</description><parent><groupId>com.example</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version><relativePath/></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

编写application.yml

eureka:client:serviceUrl:defaultZone: http://localhost:8761/eureka/server:port: 8762spring:application:name: eureka-client

启动类

package com.example.eurekaclient;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@EnableEurekaClient
@SpringBootApplication
public class EurekaClientApplication {public static void main(String[] args) {SpringApplication.run(EurekaClientApplication.class, args);}
}

定义controller方便调用

package com.example.eurekaclient.action;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;/*** @ClassName HelloController* @Description TODO* @Autuor lulinfeng* @Date 2020/9/17* @Version 1.0*/
@RestController
public class HelloController {@RequestMapping(value = "/hello", method = RequestMethod.GET)public String hello(@RequestParam String name) {return "hi , my name is" + name;}
}

启动

代表注册上了。

3. Eureka源码解析

3.1 Eureka的部分概念

1) Register-服务注册
当eureka Client向eureka Server发起注册,客户端提供自身元数据,比如IP地址,端口,运行状况指标的Url,主页地址等信息。

2)Renew-服务续约
客户端默认30s发次心跳进行服务续约,通过服务续约来告知服务器,当前客户端依然可用,如果服务器90s没有收到客户端的心跳,那么服务器就会把客户端的实例从注册表中删除。

3)Fetch Registries–获取服务注册列表信息
客户端从服务器中获取服务注册表信心,将其缓存到本地,客户端会使用服务注册列表信息来查询其他服务的内容,从而进行远程调用。注册列表定时30s更新一次,如果返回注册列表信息和本地缓存不同,那么client会自己处理这些信息,如果因为某些原因导致注册信息无法匹配的话,客户端会重新获取整个注册表信息。服务器缓存了所有服务注册列表信息,并且将整个注册列表和每个应用程序的信息压缩。
客户端和服务器可以使用json或者xml格式进行通行,默认使用json。

4)Cancel -服务下线
客户端在程序关闭的时候可以向服务器发送下线请求。发送请求之后,该客户端的实例信息
将从eureka Server的服务注册列表中删除,此下线请求不会自动完成,需要在程序关闭时调用如下代码。

DiscoveryManager.getInstance().shutdownComponent();

3.2 服务启动

服务注册启动

服务注册就是客户端向服务器提交自己服务信息,包含ip地址,端口,serviceID等信息。
服务注册是在SpringBoot应用启动的时候发起的,服务的注册取决于服务是否已经启动完毕,在StringBoot中,会等Spring容器启动并且所有配置都完成之后来进行注册,这个动作会在SpringBoot启动方法中的refreshContext中完成。

SpringApplication.run() -> this.refreshContext(context);-
>this.refresh(context);->ServletWebServerApplicationContext.refresh()-
>this.finishRefresh();->AbstractApplicationContext.finishRefresh-
>DefaultLifecycleProcessor.onRefresh()-> this.startBeans->this.start()-
>this.doStart()->

核心的方法在于AbstractApplicationContext.finishRefresh这个方法,我们看下他的源码

 protected void finishRefresh() {//清空缓存this.clearResourceCaches();//初始化LifecycleProcessor,在Spring启动的时候启动Bean,在Spring结束的时候销毁Beanthis.initLifecycleProcessor();//调用LifecycleProcessor的onRefresh方法,启动实现了Lifecycle接口的beanthis.getLifecycleProcessor().onRefresh();//发布ContextRefreshedEventthis.publishEvent((ApplicationEvent)(new ContextRefreshedEvent(this)));//注册MBean,通过JMX进行监控和管理LiveBeansView.registerApplicationContext(this);}

其中跟我们注册强相关的就是这个this.getLifecycleProcessor().onRefresh()方法
它是调用生命周期处理器的onrefresh方法,找到SmartLifecycle接口的所有实现类并且调用start方法。
后续调用的链路为:DefaultLifecycleProcessor.startBean -> start() -> doStart() -> bean.start()

SmartLifecycle接口

这边可以看下SmartLifecycle接口,当Spring容器加载原所有的Bean并且初始化之后,继续回调实现SmartLifcycle接口的类对应的方法,比如start
这边我们可以测试下写个测试类

@Service
public class TestSmartLifeCycle implements SmartLifecycle {@Overridepublic void start() {System.out.println("start");}@Overridepublic void stop() {System.out.println("stop");}@Overridepublic boolean isRunning() {return false;}
}

然后启动springBoot就可以在控制台输出了start
上文中说了DefaultLifecycleProcessor.startBean会遍历所有实现了SmartLifecycle的类,并且start。我们打个debug看下

我们可以看下他的startBeans方法
其实就是循环实现了smartLifecycle接口的类,并且调用他的start方法

private void startBeans(boolean autoStartupOnly) {Map<String, Lifecycle> lifecycleBeans = this.getLifecycleBeans();Map<Integer, DefaultLifecycleProcessor.LifecycleGroup> phases = new HashMap();lifecycleBeans.forEach((beanName, bean) -> {if (!autoStartupOnly || bean instanceof SmartLifecycle && ((SmartLifecycle)bean).isAutoStartup()) {int phase = this.getPhase(bean);DefaultLifecycleProcessor.LifecycleGroup group = (DefaultLifecycleProcessor.LifecycleGroup)phases.get(phase);if (group == null) {group = new DefaultLifecycleProcessor.LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly);phases.put(phase, group);}group.add(beanName, bean);}});if (!phases.isEmpty()) {List<Integer> keys = new ArrayList(phases.keySet());Collections.sort(keys);Iterator var5 = keys.iterator();while(var5.hasNext()) {Integer key = (Integer)var5.next();((DefaultLifecycleProcessor.LifecycleGroup)phases.get(key)).start();}}}

DefaultLifeCycleProcessor.doStart()

让我们继续回到启动阶段的doStart方法,在上一小节理解了SmartLifecycle之后我们再回过头看这个方法就理解了

private void doStart(Map<String, ? extends Lifecycle> lifecycleBeans, String beanName, boolean autoStartupOnly) {Lifecycle bean = (Lifecycle)lifecycleBeans.remove(beanName);if (bean != null && bean != this) {String[] dependenciesForBean = this.getBeanFactory().getDependenciesForBean(beanName);String[] var6 = dependenciesForBean;int var7 = dependenciesForBean.length;for(int var8 = 0; var8 < var7; ++var8) {String dependency = var6[var8];this.doStart(lifecycleBeans, dependency, autoStartupOnly);}if (!bean.isRunning() && (!autoStartupOnly || !(bean instanceof SmartLifecycle) || ((SmartLifecycle)bean).isAutoStartup())) {if (this.logger.isTraceEnabled()) {this.logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]");}try {//这边启动的Bean实例就是EurekaAutoServiceRegistrationbean.start();} catch (Throwable var10) {throw new ApplicationContextException("Failed to start bean '" + beanName + "'", var10);}if (this.logger.isDebugEnabled()) {this.logger.debug("Successfully started bean '" + beanName + "'");}}}}

此时上面start的启动的bean就是EurekaAutoServiceRegistration,因为它实现了SmartLifecycle接口,那么就会调用的start方法。

 public void start() {if (this.port.get() != 0) {if (this.registration.getNonSecurePort() == 0) {this.registration.setNonSecurePort(this.port.get());}if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {this.registration.setSecurePort(this.port.get());}}if (!this.running.get() && this.registration.getNonSecurePort() > 0) {//发起服务注册this.serviceRegistry.register(this.registration);this.context.publishEvent(new InstanceRegisteredEvent(this, this.registration.getInstanceConfig()));this.running.set(true);}}

在start方法中果然,我们就发现了this.serviceRegistry.register(this.registration);
这个就是发起服务注册。此时有个this.serviceRegistry实例,这个实例是EurekaServiceRegistry,那这个实例是怎么初始化的呢?
原来是在EurekaAutoServiceRegistration构造中赋值操作,这个构造方法是在EurekaClientAutoConfiguration中这个自动装配类中被装配和初始化。

  @Bean@ConditionalOnBean({AutoServiceRegistrationProperties.class})@ConditionalOnProperty(value = {"spring.cloud.service-registry.auto-registration.enabled"},matchIfMissing = true)public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(ApplicationContext context, EurekaServiceRegistry registry, EurekaRegistration registration) {return new EurekaAutoServiceRegistration(context, registry, registration);}

3.3 服务注册

那么我们正式开始分析服务注册的源码流程

EurekaAutoServiceRegistration.start

我们承接上面章节看下这个start方法源码,其实核心就是register方法

public class EurekaAutoServiceRegistration implements AutoServiceRegistration, SmartLifecycle, Ordered, SmartApplicationListener {@Overridepublic void start() {//省略...this.serviceRegistry.register(this.registration);this.context.publishEvent(new InstanceRegisteredEvent(this, this.registration.getInstanceConfig()));}
}

this.serviceRegistry.register(this.registration),方法调用EurekaServiceRegistry类中的registry方法来实现注册。

EurekaServiceRegistry.register

public void register(EurekaRegistration reg) {this.maybeInitializeClient(reg);if (log.isInfoEnabled()) {log.info("Registering application " + reg.getApplicationInfoManager().getInfo().getAppName() + " with eureka with status " + reg.getInstanceConfig().getInitialStatus());}//设置当前实例状态,如果实例状态发生变化,只要状态不是DOWN,那么就会被监听器监听并且执行服务注册reg.getApplicationInfoManager().setInstanceStatus(reg.getInstanceConfig().getInitialStatus());//设置健康检查处理reg.getHealthCheckHandler().ifAvailable((healthCheckHandler) -> {reg.getEurekaClient().registerHealthCheck(healthCheckHandler);});}

根据register方法来看,注册方法中没有调用Eureka方法执行注册,仅设置了状态和设置健康检查处理器,所以我们继续看下setInstanceStatus方法

public synchronized void setInstanceStatus(InstanceStatus status) {InstanceStatus next = this.instanceStatusMapper.map(status);if (next != null) {InstanceStatus prev = this.instanceInfo.setStatus(next);if (prev != null) {Iterator var4 = this.listeners.values().iterator();while(var4.hasNext()) {ApplicationInfoManager.StatusChangeListener listener = (ApplicationInfoManager.StatusChangeListener)var4.next();try {listener.notify(new StatusChangeEvent(prev, next));} catch (Exception var7) {logger.warn("failed to notify listener: {}", listener.getId(), var7);}}}}}

这个方法中,通过监听器来发布状态变更事件,listener实例为StatusChangeListener,调用这个notify的方法,典型的观察者模式,那么肯定有地方在监听这个事件。
肯定有地方初始化了这系列操作。
回过头看register的reg.getApplicationInfoManager()方法发现ApplicationInfoManager实例来自于EurekaRegistration
EurekaRegistration又是在上面提到的EurekaAutoServiceRegistration类中进行实例化的,所以是否是自动装配中做了什么
于是找到EurekaClientAutoConfiguration这个类,果然看到了Bean的一些自
动装配,其中包含 EurekaClientApplicationInfoMangagerEurekaRegistration 等。

EurekaClientConfiguration

@EurekaClientAutoConfiguration.ConditionalOnMissingRefreshScopeprotected static class EurekaClientConfiguration {@Autowiredprivate ApplicationContext context;@Autowiredprivate AbstractDiscoveryClientOptionalArgs<?> optionalArgs;protected EurekaClientConfiguration() {}@Bean(destroyMethod = "shutdown")@ConditionalOnMissingBean(value = {EurekaClient.class},search = SearchStrategy.CURRENT)public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config) {return new CloudEurekaClient(manager, config, this.optionalArgs, this.context);}@Bean@ConditionalOnMissingBean(value = {ApplicationInfoManager.class},search = SearchStrategy.CURRENT)public ApplicationInfoManager eurekaApplicationInfoManager(EurekaInstanceConfig config) {InstanceInfo instanceInfo = (new InstanceInfoFactory()).create(config);return new ApplicationInfoManager(config, instanceInfo);}@Bean@ConditionalOnBean({AutoServiceRegistrationProperties.class})@ConditionalOnProperty(value = {"spring.cloud.service-registry.auto-registration.enabled"},matchIfMissing = true)public EurekaRegistration eurekaRegistration(EurekaClient eurekaClient, CloudEurekaInstanceConfig instanceConfig, ApplicationInfoManager applicationInfoManager, @Autowired(required = false) ObjectProvider<HealthCheckHandler> healthCheckHandler) {return EurekaRegistration.builder(instanceConfig).with(applicationInfoManager).with(eurekaClient).with(healthCheckHandler).build();}}

在启动阶段我,我们可以看到CloudEurekaClient被装配,从名字上看应该是eureka客户端的工具类,实现和服务端的通信和处理。

CloudEurekaClient

我们看下CloudEurekaClient的构造

public CloudEurekaClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs<?> args, ApplicationEventPublisher publisher) {super(applicationInfoManager, config, args);this.cacheRefreshedCount = new AtomicLong(0L);this.eurekaHttpClient = new AtomicReference();this.applicationInfoManager = applicationInfoManager;this.publisher = publisher;this.eurekaTransportField = ReflectionUtils.findField(DiscoveryClient.class, "eurekaTransport");ReflectionUtils.makeAccessible(this.eurekaTransportField);}

构造中调用了父类DiscoveryClient的构造

DiscoveryClient

 DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {//省略...//是否要从eureka server上获取服务地址信息if (config.shouldFetchRegistry()) {this.registryStalenessMonitor = new ThresholdLevelsMetric(this, "eurekaClient.registry.lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});} else {this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;}//是否要注册到eureka serverif (config.shouldRegisterWithEureka()) {this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, "eurekaClient.registration.lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});} else {this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;}logger.info("Initializing Eureka in region {}", this.clientConfig.getRegion());//如果不需要注册并且不需要更新服务地址if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {logger.info("Client configured to neither register nor query for data.");this.scheduler = null;this.heartbeatExecutor = null;this.cacheRefreshExecutor = null;this.eurekaTransport = null;this.instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), this.clientConfig.getRegion());DiscoveryManager.getInstance().setDiscoveryClient(this);DiscoveryManager.getInstance().setEurekaClientConfig(config);this.initTimestampMs = System.currentTimeMillis();logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}", this.initTimestampMs, this.getApplications().size());} else {try {this.scheduler = Executors.newScheduledThreadPool(2, (new ThreadFactoryBuilder()).setNameFormat("DiscoveryClient-%d").setDaemon(true).build());this.heartbeatExecutor = new ThreadPoolExecutor(1, this.clientConfig.getHeartbeatExecutorThreadPoolSize(), 0L, TimeUnit.SECONDS, new SynchronousQueue(), (new ThreadFactoryBuilder()).setNameFormat("DiscoveryClient-HeartbeatExecutor-%d").setDaemon(true).build());this.cacheRefreshExecutor = new ThreadPoolExecutor(1, this.clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0L, TimeUnit.SECONDS, new SynchronousQueue(), (new ThreadFactoryBuilder()).setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d").setDaemon(true).build());this.eurekaTransport = new DiscoveryClient.EurekaTransport();this.scheduleServerEndpointTask(this.eurekaTransport, args);Object azToRegionMapper;if (this.clientConfig.shouldUseDnsForFetchingServiceUrls()) {azToRegionMapper = new DNSBasedAzToRegionMapper(this.clientConfig);} else {azToRegionMapper = new PropertyBasedAzToRegionMapper(this.clientConfig);}if (null != this.remoteRegionsToFetch.get()) {((AzToRegionMapper)azToRegionMapper).setRegionsToFetch(((String)this.remoteRegionsToFetch.get()).split(","));}this.instanceRegionChecker = new InstanceRegionChecker((AzToRegionMapper)azToRegionMapper, this.clientConfig.getRegion());} catch (Throwable var10) {throw new RuntimeException("Failed to initialize DiscoveryClient!", var10);}if (this.clientConfig.shouldFetchRegistry() && !this.fetchRegistry(false)) {this.fetchRegistryFromBackup();}if (this.preRegistrationHandler != null) {this.preRegistrationHandler.beforeRegistration();}//如果需要注册到Eureka server并且是开启了初始化的时候强制注册,则调用register()发起服务注册if (this.clientConfig.shouldRegisterWithEureka() && this.clientConfig.shouldEnforceRegistrationAtInit()) {try {if (!this.register()) {throw new IllegalStateException("Registration error at startup. Invalid server response.");}} catch (Throwable var9) {logger.error("Registration error at startup: {}", var9.getMessage());throw new IllegalStateException(var9);}}this.initScheduledTasks();}

根据DiscoveryClient最终的构造来看,主要做些初始化的工作,比如初始化了几个定时任务scheduler,heartbeartExecutor心跳定时任务,cacheRefreshExecutor定时去同步eureka server的实例列表.

DiscoveryClient.initScheduledTasks

initScheduledTasks启动了定时任务
如果配置了开启从注册中心刷新服务列表,则会开启cacheRefreshExecutor这个定时任务
如果开启了服务注册到eureka,则需要做几个事
1)建立心跳检测机制
2)通过内部类实例化statusChangeListener实例状态接口,这个就是我们最初分析启动过程中看到调用notify方法,实质是在这边实例化的。

 private void initScheduledTasks() {//如果配置了开启从注册中心刷新服务列表,则会开启cacheRefreshExecutor这个定时任务if (this.clientConfig.shouldFetchRegistry()) {renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();this.cacheRefreshTask = new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread());this.scheduler.schedule(this.cacheRefreshTask, (long)renewalIntervalInSecs, TimeUnit.SECONDS);}//如果开启了服务注册到Eureka,则需要做几个事情if (this.clientConfig.shouldRegisterWithEureka()) {renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound();logger.info("Starting heartbeat executor: renew interval is: {}", renewalIntervalInSecs);this.heartbeatTask = new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread());this.scheduler.schedule(this.heartbeatTask, (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);}//启动一个实例信息复制器,主要就是为了开启一个定时线程,每40秒判断实例信息是否变更,如果变更了则重新注册this.instanceInfoReplicator.start(this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());} else {logger.info("Not registering with Eureka server per configuration");}}

instanceInfoReplicator.onDemandUpdate()

这个方法的主要作用是根据实例数据是否发生变化,来触发服务注册中心的数据。

public boolean onDemandUpdate() {//限流判断if (this.rateLimiter.acquire(this.burstSize, (long)this.allowedRatePerMinute)) {if (!this.scheduler.isShutdown()) {//提交任务this.scheduler.submit(new Runnable() {public void run() {InstanceInfoReplicator.logger.debug("Executing on-demand update of local InstanceInfo");//取出之前已经提交的任务,也就是在start方法中提交的更新任务,如果任务还没有执行完成,则取消之前的任务。Future latestPeriodic = (Future)InstanceInfoReplicator.this.scheduledPeriodicRef.get();if (latestPeriodic != null && !latestPeriodic.isDone()) {InstanceInfoReplicator.logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update");//如果此任务未完成,就立即取消latestPeriodic.cancel(false);}//通过调用run方法,令任务在延时后执行,相当于周期性任务中的一次InstanceInfoReplicator.this.run();}});return true;} else {logger.warn("Ignoring onDemand update due to stopped scheduler");return false;}} else {logger.warn("Ignoring onDemand update due to rate limiter");return false;}}

InstanceInfoReplicator.this.run()

这个run方法实际上跟之前自动装配执行的服务注册方法是一致的,调用register方法进行服务注册,并且每个30s定时执行下当前run方法进行检查

public void run() {try {this.discoveryClient.refreshInstanceInfo();Long dirtyTimestamp = this.instanceInfo.isDirtyWithTime();if (dirtyTimestamp != null) {this.discoveryClient.register();this.instanceInfo.unsetIsDirty(dirtyTimestamp);}} catch (Throwable var7) {logger.warn("There was a problem with the instance info replicator", var7);} finally {ScheduledFuture next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);this.scheduledPeriodicRef.set(next);} }

discoveryClient.register()

走到这边终于到了服务注册的入口了,不多bb直接看源码

boolean register() throws Throwable {logger.info("DiscoveryClient_{}: registering service...", this.appPathIdentifier);EurekaHttpResponse httpResponse;try {httpResponse = this.eurekaTransport.registrationClient.register(this.instanceInfo);} catch (Exception var3) {logger.warn("DiscoveryClient_{} - registration failed {}", new Object[]{this.appPathIdentifier, var3.getMessage(), var3});throw var3;}if (logger.isInfoEnabled()) {logger.info("DiscoveryClient_{} - registration status: {}", this.appPathIdentifier, httpResponse.getStatusCode());}return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();}

AbstractJerseyEurekaHttpClient.register()

 public EurekaHttpResponse<Void> register(InstanceInfo info) {String urlPath = "apps/" + info.getAppName();ClientResponse response = null;EurekaHttpResponse var5;try {Builder resourceBuilder = this.jerseyClient.resource(this.serviceUrl).path(urlPath).getRequestBuilder();this.addExtraHeaders(resourceBuilder);response = (ClientResponse)((Builder)((Builder)((Builder)resourceBuilder.header("Accept-Encoding", "gzip")).type(MediaType.APPLICATION_JSON_TYPE)).accept(new String[]{"application/json"})).post(ClientResponse.class, info);var5 = EurekaHttpResponse.anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();} finally {if (logger.isDebugEnabled()) {logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", new Object[]{this.serviceUrl, urlPath, info.getId(), response == null ? "N/A" : response.getStatus()});}if (response != null) {response.close();}}return var5;}

通过阅读源码,我们很明显可以看到发起了http请求,访问Eureka-Server的apps/${APP_NAME}接口,把自身服务实例信息发给Eureka server进行保存,到这边我们就基本知道了eureka 客户端是怎么启动的时候把服务信息注册到Eureka Sever上的了。

总结一下就是:

  1. 在Spring Boot启动时,由于自动装配机制将CloudEurekaClient注入到了容器,并且执行了构造
    方法,而在构造方法中有一个定时任务每40s会执行一次判断,判断实例信息是否发生了变化,如
    果是则会发起服务注册的流程
  2. 在Spring Boot启动时,通过refresh方法,最终调用StatusChangeListener.notify进行服务状态变
    更的监听,而这个监听的方法受到事件之后会去执行服务注册。

3.4 Eureka Server收到请求之后的处理

在没分析源码实现之前,我们一定知道它肯定对请求过来的服务实例数据进行了存储。那么我们去
Eureka Server端看一下处理流程。
请求入口在: com.netflix.eureka.resources.ApplicationResource.addInstance()
大家可以发现,这里所提供的REST服务,采用的是jersey来实现的。Jersey是基于JAX-RS标准,提供
REST的实现的支持,这里就不展开分析了。

ApplicationResource.addInstance()

当EurekaClient调用register方法发起注册时,会调用ApplicationResource.addInstance方法。
服务注册就是发送一个 POST 请求带上当前实例信息到类 ApplicationResource 的 addInstance
方法进行服务注册

@POST@Consumes({"application/json", "application/xml"})public Response addInstance(InstanceInfo info, @HeaderParam("x-netflix-discovery-replication") String isReplication) {logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);if (this.isBlank(info.getId())) {return Response.status(400).entity("Missing instanceId").build();} else if (this.isBlank(info.getHostName())) {return Response.status(400).entity("Missing hostname").build();} else if (this.isBlank(info.getIPAddr())) {return Response.status(400).entity("Missing ip address").build();} else if (this.isBlank(info.getAppName())) {return Response.status(400).entity("Missing appName").build();} else if (!this.appName.equals(info.getAppName())) {return Response.status(400).entity("Mismatched appName, expecting " + this.appName + " but was " + info.getAppName()).build();} else if (info.getDataCenterInfo() == null) {return Response.status(400).entity("Missing dataCenterInfo").build();} else if (info.getDataCenterInfo().getName() == null) {return Response.status(400).entity("Missing dataCenterInfo Name").build();} else {DataCenterInfo dataCenterInfo = info.getDataCenterInfo();if (dataCenterInfo instanceof UniqueIdentifier) {String dataCenterInfoId = ((UniqueIdentifier)dataCenterInfo).getId();if (this.isBlank(dataCenterInfoId)) {boolean experimental = "true".equalsIgnoreCase(this.serverConfig.getExperimental("registration.validation.dataCenterInfoId"));if (experimental) {String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";return Response.status(400).entity(entity).build();}if (dataCenterInfo instanceof AmazonInfo) {AmazonInfo amazonInfo = (AmazonInfo)dataCenterInfo;String effectiveId = amazonInfo.get(MetaDataKey.instanceId);if (effectiveId == null) {amazonInfo.getMetadata().put(MetaDataKey.instanceId.getName(), info.getId());}} else {logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());}}}this.registry.register(info, "true".equals(isReplication));return Response.status(204).build();}}

PeerAwareInstanceRegistryImpl.register

我们先来看PeerAwareInstanceRegistryImpl的类关系图,从类关系图可以看出,

PeerAwareInstanceRegistry的最顶层接口为LeaseManagerLookupService
其中LookupService定义了最基本的发现示例的行为
LeaseManager定义了处理客户端注册续约注销等操作

ApplicationResource.addInstance最终调用的还是PeerAwareInstanceRegistryImpl.register方法
我们看下源码

public void register(InstanceInfo info, boolean isReplication) {int leaseDuration = 90;if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {//如果客户端有自己定义心跳的超时时间,那么拿客户端的时间leaseDuration = info.getLeaseInfo().getDurationInSecs();}//节点注册super.register(info, leaseDuration, isReplication);// 复制到Eureka Server集群中的其他节点this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Register, info.getAppName(), info.getId(), info, (InstanceStatus)null, isReplication);}

leaseDuration代表租约过期时间,默认是90s,当服务器90s没收到客户端的心跳,则主动从节点列表中删除该客户端
调用super.register 发起注册
将信息复制到Eureka Server集群中的其他机器,原理是,获取集群中的所有节点,然后遍历发起注册。

AbstractInstanceRegistry.register

简单来说,Eureka-Server的服务注册,实际上是将客户端传递过来的实例数据保存到Eureka-Server中的ConcurrentHashMap中。

public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {try {this.read.lock();//从registry中获得当前实例信息,根据appNameMap<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName());//增加注册次数到监控信息中EurekaMonitors.REGISTER.increment(isReplication);//如果当前appName是第一次注册,则初始化一个ConcurrentHashMapif (gMap == null) {ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap();gMap = (Map)this.registry.putIfAbsent(registrant.getAppName(), gNewMap);if (gMap == null) {gMap = gNewMap;}}//从gMap中查询已经存在的Lease信息,Lease中文翻译为租约,//实际上它把服务提供者的实例信息包装成了一个lease,里面提供了对于改服务实例的租约管理Lease<InstanceInfo> existingLease = (Lease)((Map)gMap).get(registrant.getId());// 当instance已经存在是,和客户端的instance的信息做比较,// 时间最新的那个,为有效instance信息if (existingLease != null && existingLease.getHolder() != null) {Long existingLastDirtyTimestamp = ((InstanceInfo)existingLease.getHolder()).getLastDirtyTimestamp();Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");registrant = (InstanceInfo)existingLease.getHolder();}} else {//当lease不存在时,进入到这段代码synchronized(this.lock) {if (this.expectedNumberOfClientsSendingRenews > 0) {++this.expectedNumberOfClientsSendingRenews;this.updateRenewsPerMinThreshold();}}logger.debug("No previous lease information found; it is new registration");}//构建一个leaseLease<InstanceInfo> lease = new Lease(registrant, leaseDuration);if (existingLease != null) {// 当原来存在Lease的信息时,设置serviceUpTimestamp,// 保证服务启动的时间一直是第一次注册的那个lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());}((Map)gMap).put(registrant.getId(), lease);//添加到最近注册的队列中this.recentRegisteredQueue.add(new Pair(System.currentTimeMillis(), registrant.getAppName() + "(" + registrant.getId() + ")"));// 检查实例状态是否发生变化,如果是并且存在,则覆盖原来的状态if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the overrides", registrant.getOverriddenStatus(), registrant.getId());if (!this.overriddenInstanceStatusMap.containsKey(registrant.getId())) {logger.info("Not found overridden id {} and hence adding it", registrant.getId());this.overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());}}InstanceStatus overriddenStatusFromMap = (InstanceStatus)this.overriddenInstanceStatusMap.get(registrant.getId());if (overriddenStatusFromMap != null) {logger.info("Storing overridden status {} from map", overriddenStatusFromMap);registrant.setOverriddenStatus(overriddenStatusFromMap);}InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(registrant, existingLease, isReplication);registrant.setStatusWithoutDirty(overriddenInstanceStatus);// 得到instanceStatus,判断是否是UP状态if (InstanceStatus.UP.equals(registrant.getStatus())) {lease.serviceUp();}// 设置注册类型为添加registrant.setActionType(ActionType.ADDED);// 租约变更记录队列,记录了实例的每次变化, 用于注册信息的增量获取this.recentlyChangedQueue.add(new AbstractInstanceRegistry.RecentlyChangedItem(lease));registrant.setLastUpdatedTimestamp();//让缓存失效this.invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());logger.info("Registered instance {}/{} with status {} (replication={})", new Object[]{registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication});} finally {this.read.unlock();}}

总结:
至此,我们就把服务注册在客户端和服务端的处理过程做了一个详细的分析,实际上在Eureka Server
端,会把客户端的地址信息保存到ConcurrentHashMap中存储。并且服务提供者和注册中心之间,会
建立一个心跳检测机制。
用于监控服务提供者的健康状态

3.5 多级缓存设计

Eureka Server存在三个变量
AbstractInstanceRegistry类的registry 代表是客户端注册列表存储集合

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap();

ResponseCacheImpl类的 readWriteCacheMap和readOnlyCacheMap两个集合

 private final ConcurrentMap<Key, ResponseCacheImpl.Value> readOnlyCacheMap = new ConcurrentHashMap();private final LoadingCache<Key, ResponseCacheImpl.Value> readWriteCacheMap;

默认情况下定时任务每30s将readWriteCacheMap同步至readOnlyCacheMap,每60s清理超
过90s未续约的节点,Eureka Client每30s从readOnlyCacheMap更新服务注册信息,而客户端服务的
注册则从registry更新服务注册信息。

多级缓存的意义

为什么要设计多级缓存呢?原因很简单,就是当存在大规模的服务注册和更新时,如果只是修改一个ConcurrentHashMap数据,那么势必因为锁的存在导致竞争,影响性能。
而Eureka又是AP模型,只需要满足最终可用就行。所以它在这里用到多级缓存来实现读写分离。
注册方法写的时候直接写内存注册表,写完表之后主动失效读写缓存。
获取注册信息接口先从只读缓存取,只读缓存没有再去读写缓存取,读写缓存没有再去内存注册表里取(不只是取,此处较复杂)。并且,读写缓存会更新回写只读缓存
responseCacheUpdateIntervalMs : readOnlyCacheMap 缓存更新的定时器时间间隔,默认为
30秒
responseCacheAutoExpirationInSeconds : readWriteCacheMap 缓存过期时间,默认为 180 秒

服务注册的缓存失效

在AbstractInstanceRegistry.register方法的最后,会调用
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(),
registrant.getSecureVipAddress()); 方法,使得读写缓存失效。

public void invalidate(String appName, @Nullable String vipAddress, @Nullable String secureVipAddress) {KeyType[] var4 = KeyType.values();int var5 = var4.length;for(int var6 = 0; var6 < var5; ++var6) {KeyType type = var4[var6];Version[] var8 = Version.values();int var9 = var8.length;for(int var10 = 0; var10 < var9; ++var10) {Version v = var8[var10];this.invalidate(new Key(EntityType.Application, appName, type, v, EurekaAccept.full), new Key(EntityType.Application, appName, type, v, EurekaAccept.compact), new Key(EntityType.Application, "ALL_APPS", type, v, EurekaAccept.full), new Key(EntityType.Application, "ALL_APPS", type, v, EurekaAccept.compact), new Key(EntityType.Application, "ALL_APPS_DELTA", type, v, EurekaAccept.full), new Key(EntityType.Application, "ALL_APPS_DELTA", type, v, EurekaAccept.compact));if (null != vipAddress) {this.invalidate(new Key(EntityType.VIP, vipAddress, type, v, EurekaAccept.full));}if (null != secureVipAddress) {this.invalidate(new Key(EntityType.SVIP, secureVipAddress, type, v, EurekaAccept.full));}}}}

定时同步缓存

ResponseCacheImpl的构造方法中,会启动一个定时任务,这个任务会定时检查写缓存中的数据变
化,进行更新和同步。

 private TimerTask getCacheUpdateTask() {return new TimerTask() {public void run() {ResponseCacheImpl.logger.debug("Updating the client cache from response cache");Iterator var1 = ResponseCacheImpl.this.readOnlyCacheMap.keySet().iterator();while(var1.hasNext()) {Key key = (Key)var1.next();if (ResponseCacheImpl.logger.isDebugEnabled()) {ResponseCacheImpl.logger.debug("Updating the client cache from response cache for key : {} {} {} {}", new Object[]{key.getEntityType(), key.getName(), key.getVersion(), key.getType()});}try {CurrentRequestVersion.set(key.getVersion());ResponseCacheImpl.Value cacheValue = (ResponseCacheImpl.Value)ResponseCacheImpl.this.readWriteCacheMap.get(key);ResponseCacheImpl.Value currentCacheValue = (ResponseCacheImpl.Value)ResponseCacheImpl.this.readOnlyCacheMap.get(key);if (cacheValue != currentCacheValue) {ResponseCacheImpl.this.readOnlyCacheMap.put(key, cacheValue);}} catch (Throwable var8) {ResponseCacheImpl.logger.error("Error while updating the client cache from response cache for key {}", key.toStringCompact(), var8);} finally {CurrentRequestVersion.remove();}}}};}

3.6 服务续约

服务续约就是心跳检测机制。客户端定期发送心跳来续约。

DiscoveryClient.initScheduledTasks

客户端在initScheduledTasks 方法中创建心跳检测定时任务

 this.heartbeatTask = new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread());

在这个定时任务中会执行HeartbeatThread线程,由下面的源码可以看出,它会定时调用renew方法进行续约。

 private class HeartbeatThread implements Runnable {private HeartbeatThread() {}public void run() {if (DiscoveryClient.this.renew()) {DiscoveryClient.this.lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();}}}

服务端收到心跳请求的处理

在ApplicationResource.getInstanceInfo这个接口中,会返回一个InstanceResource的实例,在该实例
下,定义了一个statusUpdate的接口来更新状态

@Path("{id}")public InstanceResource getInstanceInfo(@PathParam("id") String id) {return new InstanceResource(this, id, this.serverConfig, this.registry);}

InstanceResource.statusUpdate()

它会调用
AbstractInstanceRegistry.statusUpdate来更新指定服务提供者在服务端存储的信息中的变化

 @PUT@Path("status")public Response statusUpdate(@QueryParam("value") String newStatus, @HeaderParam("x-netflix-discovery-replication") String isReplication, @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {try {if (this.registry.getInstanceByAppAndId(this.app.getName(), this.id) == null) {logger.warn("Instance not found: {}/{}", this.app.getName(), this.id);return Response.status(Status.NOT_FOUND).build();} else {boolean isSuccess = this.registry.statusUpdate(this.app.getName(), this.id, InstanceStatus.valueOf(newStatus), lastDirtyTimestamp, "true".equals(isReplication));if (isSuccess) {logger.info("Status updated: {} - {} - {}", new Object[]{this.app.getName(), this.id, newStatus});return Response.ok().build();} else {logger.warn("Unable to update status: {} - {} - {}", new Object[]{this.app.getName(), this.id, newStatus});return Response.serverError().build();}}} catch (Throwable var5) {logger.error("Error updating instance {} for status {}", this.id, newStatus);return Response.serverError().build();}}

AbstractInstanceRegistry.statusUpdate

在这个方法中,会拿到应用对应的实例列表,然后调用Lease.renew()去进行心跳续约。

 public boolean statusUpdate(String appName, String id, InstanceStatus newStatus, String lastDirtyTimestamp, boolean isReplication) {boolean var15;try {this.read.lock();// 更新状态的次数 状态统计EurekaMonitors.STATUS_UPDATE.increment(isReplication);// 从本地数据里面获取实例信息Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(appName);Lease<InstanceInfo> lease = null;if (gMap != null) {lease = (Lease)gMap.get(id);}// 实例不存在,则直接返回,表示失败if (lease == null) {boolean var14 = false;return var14;}// 执行一下lease的renew方法,里面主要是更新了这个instance的最后更新时间。lease.renew();// 获取instance实例信息InstanceInfo info = (InstanceInfo)lease.getHolder();if (info == null) {logger.error("Found Lease without a holder for instance id {}", id);}// 当instance信息不为空时,并且实例状态发生了变化if (info != null && !info.getStatus().equals(newStatus)) {if (InstanceStatus.UP.equals(newStatus)) {lease.serviceUp();}// 将instance Id 和这个状态的映射信息放入覆盖缓存MAP里面去this.overriddenInstanceStatusMap.put(id, newStatus);// 设置覆盖状态到实例信息里面去info.setOverriddenStatus(newStatus);long replicaDirtyTimestamp = 0L;info.setStatusWithoutDirty(newStatus);if (lastDirtyTimestamp != null) {replicaDirtyTimestamp = Long.valueOf(lastDirtyTimestamp);}// 如果replicaDirtyTimestamp 的时间大于instance的//getLastDirtyTimestamp() ,则更新if (replicaDirtyTimestamp > info.getLastDirtyTimestamp()) {info.setLastDirtyTimestamp(replicaDirtyTimestamp);}info.setActionType(ActionType.MODIFIED);this.recentlyChangedQueue.add(new AbstractInstanceRegistry.RecentlyChangedItem(lease));info.setLastUpdatedTimestamp();//更新写缓存this.invalidateCache(appName, info.getVIPAddress(), info.getSecureVipAddress());}var15 = true;} finally {this.read.unlock();}return var15;}

3.7 服务发现

我们继续去看下服务的发现流程,客户端需要如下两个功能
在启动阶段获取指定服务提供者的地址列表
Eureka server端地址发生变化时候,需要动态感知

DiscoveryClient构造时进行查询

构造方法中,如果当前的客户端默认开启了fetchRegistry,则会从eureka-server中拉取数据。

DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {if (this.clientConfig.shouldFetchRegistry() && !this.fetchRegistry(false)) {this.fetchRegistryFromBackup();}}

我们看下fetchRegistry方法

private boolean fetchRegistry(boolean forceFullRegistryFetch) {Stopwatch tracer = this.FETCH_REGISTRY_TIMER.start();label122: {boolean var4;try {Applications applications = this.getApplications();if (!this.clientConfig.shouldDisableDelta() && Strings.isNullOrEmpty(this.clientConfig.getRegistryRefreshSingleVipAddress()) && !forceFullRegistryFetch && applications != null && applications.getRegisteredApplications().size() != 0 && applications.getVersion() != -1L) {this.getAndUpdateDelta(applications);} else {logger.info("Disable delta property : {}", this.clientConfig.shouldDisableDelta());logger.info("Single vip registry refresh property : {}", this.clientConfig.getRegistryRefreshSingleVipAddress());logger.info("Force full registry fetch : {}", forceFullRegistryFetch);logger.info("Application is null : {}", applications == null);logger.info("Registered Applications size is zero : {}", applications.getRegisteredApplications().size() == 0);logger.info("Application version is -1: {}", applications.getVersion() == -1L);this.getAndStoreFullRegistry();}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;}

定时任务每隔30s更新一次本地地址列表

在DiscoveryClient构造的时候,会初始化一些任务,这个在前面咱们分析过了。其中有一个任务动态
更新本地服务地址列表,叫 cacheRefreshTask 。
这个任务最终执行的是CacheRefreshThread这个线程。它是一个周期性执行的任务,具体我们来看一
下。

private void initScheduledTasks() {int renewalIntervalInSecs;int expBackOffBound;if (this.clientConfig.shouldFetchRegistry()) {renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();this.cacheRefreshTask = new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread());this.scheduler.schedule(this.cacheRefreshTask, (long)renewalIntervalInSecs, TimeUnit.SECONDS);}         }

TimedSupervisorTask

从整体上看,TimedSupervisorTask是固定间隔的周期性任务,一旦遇到超时就会将下一个周期的间隔
时间调大,如果连续超时,那么每次间隔时间都会增大一倍,一直到达外部参数设定的上限为止,一旦
新任务不再超时,间隔时间又会自动恢复为初始值。

 public void run() {Future future = null;try {//使用Future,可以设定子线程的超时时间,这样当前线程就不用无限等待了future = this.executor.submit(this.task);this.threadPoolLevelGauge.set((long)this.executor.getActiveCount());//指定等待子线程的最长时间future.get(this.timeoutMillis, TimeUnit.MILLISECONDS);//delay是个很有用的变量,后面会用到,这里记得每次执行任务成功都会将delay重置this.delay.set(this.timeoutMillis);//设置为最新的值,考虑到多线程,所以用了CASthis.threadPoolLevelGauge.set((long)this.executor.getActiveCount());this.successCounter.increment();} catch (TimeoutException var12) {logger.warn("task supervisor timed out", var12);this.timeoutCounter.increment();long currentDelay = this.delay.get();//任务线程超时的时候,就把delay变量翻倍,但不会超过外部调用时设定的最大延时时间long newDelay = Math.min(this.maxDelay, currentDelay * 2L);this.delay.compareAndSet(currentDelay, newDelay);} catch (RejectedExecutionException var13) {//一旦线程池的阻塞队列中放满了待处理任务,触发了拒绝策略,就会将调度器停掉if (!this.executor.isShutdown() && !this.scheduler.isShutdown()) {logger.warn("task supervisor rejected the task", var13);} else {logger.warn("task supervisor shutting down, reject the task", var13);}this.rejectedCounter.increment();} catch (Throwable var14) {//一旦出现未知的异常,就停掉调度器if (!this.executor.isShutdown() && !this.scheduler.isShutdown()) {logger.warn("task supervisor threw an exception", var14);} else {logger.warn("task supervisor shutting down, can't accept the task");}this.throwableCounter.increment();} finally {//这里任务要么执行完毕,要么发生异常,都用cancel方法来清理任务;if (future != null) {future.cancel(true);}//只要调度器没有停止,就再指定等待时间之后在执行一次同样的任务if (!this.scheduler.isShutdown()) {//这里就是周期性任务的原因:只要没有停止调度器,就再创建一次性任务,执行时间时dealy的值,//假设外部调用时传入的超时时间为30秒(构造方法的入参timeout),最大间隔时间为50秒(构造方法的入参expBackOffBound)//如果最近一次任务没有超时,那么就在30秒后开始新任务,//如果最近一次任务超时了,那么就在50秒后开始新任务(异常处理中有个乘以二的操作,乘以二后的60秒超过了最大间隔50秒)this.scheduler.schedule(this, this.delay.get(), TimeUnit.MILLISECONDS);}}}

CacheRefreshThread.refreshRegistry

然后我们看下CacheRefreshThread 方法

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

然后我们看下refreshRegistry方法

 @VisibleForTestingvoid refreshRegistry() {try {boolean isFetchingRemoteRegionRegistries = this.isFetchingRemoteRegionRegistries();boolean remoteRegionsModified = false;//如果部署在aws环境上,会判断最后一次远程区域更新的信息和当前远程区域信息进行比较,如果不想等,则更新String latestRemoteRegions = this.clientConfig.fetchRegistryForRemoteRegions();if (null != latestRemoteRegions) {String currentRemoteRegions = (String)this.remoteRegionsToFetch.get();//判断最后一次if (!latestRemoteRegions.equals(currentRemoteRegions)) {//省略...}}boolean success = this.fetchRegistry(remoteRegionsModified);if (success) {this.registrySize = ((Applications)this.localRegionApps.get()).size();this.lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();}//省略...} catch (Throwable var9) {logger.error("Cannot fetch registry from server", var9);}}

上述代码主要是两个功能:判断remoteRegions是否发生变化,调用fetchRegistry获取本地服务地址缓存

DisccoveryClient.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) {//调用增量更新this.getAndUpdateDelta(applications);} else {logger.info("Disable delta property : {}", this.clientConfig.shouldDisableDelta());logger.info("Single vip registry refresh property : {}", this.clientConfig.getRegistryRefreshSingleVipAddress());logger.info("Force full registry fetch : {}", forceFullRegistryFetch);logger.info("Application is null : {}", applications == null);logger.info("Registered Applications size is zero : {}", applications.getRegisteredApplications().size() == 0);logger.info("Application version is -1: {}", applications.getVersion() == -1L);//调用全量更新this.getAndStoreFullRegistry();}//重新计算和设置一致性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;}//将本地缓存更新的事件广播给所有已注册的监听器,注意该方法已被CloudEurekaClient类重写this.onCacheRefreshed();//检查刚刚更新的缓存中,有来自Eureka server的服务列表,其中包含了当前应用的状态,//当前实例的成员变量lastRemoteInstanceStatus,记录的是最后一次更新的当前应用状态,//上述两种状态在updateInstanceRemoteStatus方法中作比较 ,如果不一致,就更新lastRemoteInstanceStatus,并且广播对应的事件this.updateInstanceRemoteStatus();return true;}

DiscoveryClient.getAndStoreFullRegistry

从eureka server端获取服务注册中心的地址信息,然后更新并设置到本地缓存 localRegionApps 。

private void getAndStoreFullRegistry() throws Throwable {long currentUpdateGeneration = this.fetchRegistryGeneration.get();logger.info("Getting all instance registry info from the eureka server");Applications apps = null;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()) {apps = (Applications)httpResponse.getEntity();}logger.info("The response status is {}", httpResponse.getStatusCode());if (apps == null) {logger.error("The application is null for some reason. Not storing this information");} else if (this.fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1L)) {this.localRegionApps.set(this.filterAndShuffle(apps));logger.debug("Got full registry with apps hashcode {}", apps.getAppsHashCode());} else {logger.warn("Not updating applications as another thread is updating it already");}}

服务端查询服务地址流程

客户端发起服务地址的查询有两种,一种是全量、另一种是增量。对于全量查询请求,
会调用Eureka-server的ApplicationsResource的getContainers方法。
而对于增量请求,会调用ApplicationsResource.getContainerDifferential。

ApplicationsResource.getContainers

 @GETpublic Response getContainers(@PathParam("version") String version, @HeaderParam("Accept") String acceptHeader, @HeaderParam("Accept-Encoding") String acceptEncoding, @HeaderParam("X-Eureka-Accept") String eurekaAccept, @Context UriInfo uriInfo, @Nullable @QueryParam("regions") String regionsStr) {boolean isRemoteRegionRequested = null != regionsStr && !regionsStr.isEmpty();String[] regions = null;if (!isRemoteRegionRequested) {EurekaMonitors.GET_ALL.increment();} else {regions = regionsStr.toLowerCase().split(",");Arrays.sort(regions);EurekaMonitors.GET_ALL_WITH_REMOTE_REGIONS.increment();}// EurekaServer无法提供服务,返回403if (!this.registry.shouldAllowAccess(isRemoteRegionRequested)) {return Response.status(Status.FORBIDDEN).build();} else {CurrentRequestVersion.set(Version.toEnum(version));// 设置返回数据格式,默认JSONKeyType keyType = KeyType.JSON;String returnMediaType = "application/json";// 如果接收到的请求头部没有具体格式信息,则返回格式为XMLif (acceptHeader == null || !acceptHeader.contains("json")) {keyType = KeyType.XML;returnMediaType = "application/xml";}// 构建缓存键Key cacheKey = new Key(EntityType.Application, "ALL_APPS", keyType, CurrentRequestVersion.get(), EurekaAccept.fromString(eurekaAccept), regions);// 返回不同的编码类型的数据,去缓存中取数据的方法基本一致Response response;if (acceptEncoding != null && acceptEncoding.contains("gzip")) {response = Response.ok(this.responseCache.getGZIP(cacheKey)).header("Content-Encoding", "gzip").header("Content-Type", returnMediaType).build();} else {response = Response.ok(this.responseCache.get(cacheKey)).build();}CurrentRequestVersion.remove();return response;}}

responseCache.getGZIP

从缓存中读取数据。

public byte[] getGZIP(Key key) {ResponseCacheImpl.Value payload = this.getValue(key, this.shouldUseReadOnlyResponseCache);return payload == null ? null : payload.getGzipped();}@VisibleForTestingResponseCacheImpl.Value getValue(Key key, boolean useReadOnlyCache) {ResponseCacheImpl.Value payload = null;try {if (useReadOnlyCache) {ResponseCacheImpl.Value currentPayload = (ResponseCacheImpl.Value)this.readOnlyCacheMap.get(key);if (currentPayload != null) {payload = currentPayload;} else {payload = (ResponseCacheImpl.Value)this.readWriteCacheMap.get(key);this.readOnlyCacheMap.put(key, payload);}} else {payload = (ResponseCacheImpl.Value)this.readWriteCacheMap.get(key);}} catch (Throwable var5) {logger.error("Cannot get value for key : {}", key, var5);}return payload;}

SpringCloud系列之Eureka服务注册及发现相关推荐

  1. SpringCloud[01]Eureka服务注册与发现

    文章目录 Eureka服务注册与发现 1. Eureka基础知识 1. 什么是服务治理 2. 什么是服务注册与发现 3. Eureka包含两个组件:**Eureka Server** 和 **Eure ...

  2. 跟着狂神学SpringCloud(Rest环境搭建+Eureka服务注册与发现+ribbon+Feign负载均衡+Hystrix+服务熔断+Zuul路由网关+SpringCloud config分布)

    跟着狂神学SpringCloud SpringCloud 回顾之前的知识- JavaSE 数据库 前端 Servlet Http Mybatis Spring SpringMVC SpringBoot ...

  3. springcloud Eureka服务注册和发现

    一,Eureka基本介绍: Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务注册和发现(请对比Zookeeper). Eureka 采用了 C-S 的设计架构 ...

  4. SpringCloud系列研究---Eureka服务消费Feign

    转载自:https://www.cnblogs.com/chxuyuan/p/8358998.html SpringCloud系列研究---Eureka服务消费Feign 一.Feign简介 Feig ...

  5. 二、Eureka服务注册与发现

    SpringCloud系列目录: 一.SpringCloud简介 二.Eureka服务注册与发现 三.Eureka注册与发现之Eureka Comsumer 四.Eureka.Server Provi ...

  6. 【夯实Spring Cloud】Spring Cloud中的Eureka服务注册与发现详解

    本文属于[夯实Spring Cloud]系列文章,该系列旨在用通俗易懂的语言,带大家了解和学习Spring Cloud技术,希望能给读者带来一些干货.系列目录如下: [夯实Spring Cloud]D ...

  7. 详解Eureka服务注册与发现和Ribbon负载均衡【纯理论实战】

    Eureka服务注册与发现 Eureka简介 在介绍Eureka前,先说一下CAP原则 CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性). Availability ...

  8. Eureka服务注册与发现:什么是服务注册与发现,Server注册中心

    Eureka服务注册与发现 一套微服务架构的系统由很多单一职责的服务单元组成,而每个服务单元又有众多运行实例.例如,世界上最大的收费视频网站Netflix的系统是由600多个服务单元构成的,运行实例的 ...

  9. SpringCloud学习笔记:服务注册与发现Eureka(2)

    1. Eureka简介 Eureka是一个用于服务注册和发现的组件,分为Eureka Server和Eureka Client,Eureka Server为Eureka服务注册中心,Eureka Cl ...

最新文章

  1. 姚班天才漆子超、IOI金牌得主陈启峰等联手进军AR领域:全员顶尖科技公司技术背景,打造“5G+AI+AR”先锋...
  2. Visual Studio 2017开发linux程序之libevent使用实例
  3. 从SAP Hybris下单,同步到S/4HANA,触发生产流程
  4. [导入]Gemini翻譯為中文時的注意事項
  5. Content-Type
  6. UVA5876 Writings on the Wall 扩展KMP
  7. 松下抛却Jungle掌机研发项目
  8. 利用微信公众号实现zabbix告警
  9. Android应用系列:双击返回键退出程序
  10. 【原创】CGAL使用心得
  11. 基于Socket的.NET应用组件
  12. Python(五):list、tuple
  13. 工业级嵌入式串口转wifi模块
  14. Word文档标题编号调整
  15. 浩辰cad电气2021 安装教程
  16. CAS单点登录-配置数据库认证方式
  17. 钉钉直播 — 为什么直播屏幕共享显示黑屏?(仅针对mac端)
  18. chorme vue中使用audio自动播放问题
  19. c语言Ox5516,采药 (C语言代码)
  20. 检索 COM 类工厂中 CLSID 为{...} 的组件时失败,出现以下错误: 80070005、8000401A的错误解决方法

热门文章

  1. APP开发学习思路指导
  2. SDUST 第四次作业
  3. 完整简洁的Oracle获得汉字字符串拼音首字母和全拼的函数
  4. dp什么意思java_%~dp0是什么意思
  5. Vue 中echarts的数据参数使用、X轴Y轴
  6. python 作图:heatmap
  7. 郑州轻工业大学 2021-2022(1)期末模拟测试一 答案
  8. 一篇文章让你从JAVA零基础入门`OOP`编程12.19
  9. re:从零开始的Qt世界冒险生活
  10. 安卓zip解压软件_暴力破解~解压缩神器!