源码分析之Eureka客户端源码解析 说明 本文所分析代码版本为spring boot/cloud 2.2.6。
预备知识 一些注解的说明 eureka中用到几个比较有意思的注解,简化程序实现。
@ConfigurationProperties(“eureka.instance”) 表示从外部配置文件中(properties或是yml文件)读取”eureka.instance”对应的配置。
@ConditionalOnBean/@ConditionalOnClass 1 2 3 4 @ConditionalOnBean // 当给定的在bean存在时,则实例化当前Bean @ConditionalOnMissingBean // 当给定的在bean不存在时,则实例化当前Bean @ConditionalOnClass // 当给定的类名在类路径上存在,则实例化当前Bean @ConditionalOnMissingClass // 当给定的类名在类路径上不存在,则实例化当前Bean
可以参考这篇文章:SpringBoot(16)—@ConditionalOnBean与@ConditionalOnClass
比如EurekaClientAutoConfiguration类定义中,类上面注解了@ConditionalOnClass(EurekaClientConfig.class),表示当在类路径中存在EurekaClientConfig.class,则实例化当前EurekaClientAutoConfiguration。
@ImplementedBy google guice注解,指定接口默认的实现类。
@Singleton jdk 提供的注解,将当前类实现为单例模式。
eureka架构
eureka客户端源码 eureka客户端工作流程 \
下面将简要说明上图的过程。
eureka客户端比较关键的类如下:
其中,DiscoveryClient是核心,它实现了EurekaClient接口,是该接口的默认实现(@ImplementedBy传入的是DiscoveryClient.class)。另外,@Singleton注解声明了DiscoveryClient是单例。
DiscoverClient 该类的关键方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //服务注册相关 boolean register() void unregister() //服务续约 boolean renew() //关闭eureka client public synchronized void shutdown() //检查当前client状态,当client状态发生变化时,将会触发新的注册事件,去更新eureka server的注册表中的服务实例信息。 public void registerHealthCheck(HealthCheckHandler healthCheckHandler) //注册事件监听器,当实例信息有变时,触发对应的处理事件。 public void registerEventListener(EurekaEventListener eventListener)
其构造方法DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer)中,此方法中依次执行了 从eureka server中拉取注册表,服务注册,初始化发送心跳,缓存刷新(定时拉取注册表信息),按需注册定时任务等,贯穿了Eureka Client启动阶段的各项工作。
此处给出一些简单的分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) { ...... //shouldFetchRegistry,其实现类为EurekaClientConfigBean,找到它其实对应于:eureka.client.fetch-register,true:表示client从server拉取注册表信息。 if (config.shouldFetchRegistry()) { this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L}); } else { this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC; } //shouldRegisterWithEureka,点其实现类EurekaClientConfigBean,找到它其实对应于:eureka.client.register-with-eureka:true:表示client将注册到server。 if (config.shouldRegisterWithEureka()) { this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L}); } else { this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC; } ...... // default size of 2 - 1 each for heartbeat and cacheRefresh scheduler = Executors.newScheduledThreadPool(2, new ThreadFactoryBuilder() .setNameFormat("DiscoveryClient-%d") .setDaemon(true) .build()); //用于发送心跳 heartbeatExecutor = new ThreadPoolExecutor( 1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadFactoryBuilder() .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d") .setDaemon(true) .build() ); // use direct handoff //用于刷新缓存 cacheRefreshExecutor = new ThreadPoolExecutor( 1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadFactoryBuilder() .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d") .setDaemon(true) .build() ); // use direct handoff //eurekaTransport是eureka Client和eureka server进行http交互jersey客户端。点开EurekaTransport,看到许多httpclient相关的属性。 eurekaTransport = new EurekaTransport(); if (clientConfig.shouldFetchRegistry()) { .... //从eureka server拉取注册表中的信息,将注册表缓存到本地,可以就近获取其他服务信息,减少于server的交互。 boolean primaryFetchRegistryResult = fetchRegistry(false); ...... } //register()进行注册,若注册失败则抛出异常 if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) { try { if (!register() ) { throw new IllegalStateException("Registration error at startup. Invalid server response."); } } catch (Throwable th) { logger.error("Registration error at startup: {}", th.getMessage()); throw new IllegalStateException(th); } } //启动调度任务 initScheduledTasks(); }
DiscoveryClient构造方法小结 构造方法中主要进行如下工作:
初始化各种信息
从eureka server拉取注册表信息
向server注册自己
初始化3个任务
以下对重要过程进行分析:
拉取注册表过程 对应private boolean fetchRegistry(boolean forceFullRegistryFetch)方法,参考注释、源码可以知道逻辑如下:
1 2 3 4 5 6 7 如果增量式拉取被禁止或第一次拉取注册表,则进行全量拉取:getAndStoreFullRegistry()。 否则进行增量拉取注册表信息getAndUpdateDelta(applications)。 一般情况,在Eureka client第一次启动,会进行全量拉取。之后的拉取都尽量尝试只进行增量拉取。 拉取服务注册表: 全量拉取:getAndStoreFullRegistry(); 增量拉取:getAndUpdateDelta(applications);
全量拉取 getAndStoreFullRegistry() 1 2 3 4 5 6 7 进入getAndStoreFullRegistry() 方法,有一方法:eurekaTransport.queryClient.getApplications。 通过debug发现 实现类是AbstractJerseyEurekaHttpClient,点开,debug出 webResource地址为:http://root:root@eureka-7900:7900/eureka/apps/,此端点用于获取server中所有的注册表信息。 getAndStoreFullRegistry()可能被多个线程同时调用,导致新拉取的注册表被旧的覆盖(如果新拉取的动作设置apps阻塞的情况下)。 此时用了AutomicLong来进行版本管理,如果更新时版本不一致,不保存apps。 通过这个判断fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1),如果版本一致,并设置新版本(+1), 接着执行localRegionApps.set(this.filterAndShuffle(apps));过滤并洗牌apps。点开this.filterAndShuffle(apps)实现,继续点apps.shuffleAndIndexInstances,继续点shuffleInstances,继续点application.shuffleAndStoreInstances,继续点_shuffleAndStoreInstances,发现if (filterUpInstances && InstanceStatus.UP != instanceInfo.getStatus())。只保留状态为UP的服务。
增量拉取getAndUpdateDelta(applications) 1 2 3 4 5 6 7 8 9 10 11 回到刚才的fetchRegistry方法中,getAndUpdateDelta,增量拉取。通过getDelta方法,看到实际拉取的地址是:apps/delta,如果获取到的delta为空,则全量拉取。 通常来讲是3分钟之内注册表的信息变化(在server端判断),获取到delta后,会更新本地注册表。 增量式拉取是为了维护client和server端 注册表的一致性,防止本地数据过久,而失效,采用增量式拉取的方式,减少了client和server的通信量。 client有一个注册表缓存刷新定时器,专门负责维护两者之间的信息同步,但是当增量出现意外时,定时器将执行,全量拉取以更新本地缓存信息。更新本地注册表方法updateDelta,有一个细节。 if (ActionType.ADDED.equals(instance.getActionType())) ,public enum ActionType { ADDED, // Added in the discovery server MODIFIED, // Changed in the discovery server DELETED // Deleted from the discovery server }, 在InstanceInfo instance中有一个instance.getActionType(),ADDED和MODIFIED状态的将更新本地注册表applications.addApplication,DELETED将从本地剔除掉existingApp.removeInstance(instance)。
服务注册过程 1 2 DiscoveryClient构造函数中,拉取fetchRegistry完后进行register注册。由于构造函数开始时已经将服务实例元数据封装好了instanceInfo,所以此处之间向server发送instanceInfo, 通过方法httpResponse = eurekaTransport.registrationClient.register(instanceInfo);看到String urlPath = "apps/" + info.getAppName();又是一个server端点,退上去f7,httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();204状态码,则注册成功。
初始化3个定时任务 有3个:
心跳定时任务:client会定时向server发送心跳,维持自己服务租约的有效性,用心跳定时任务实现;
缓存定时任务:server中会有不同的服务实例注册或是下线,所以client需要定时从server拉取注册表信息,用缓存定时任务实现。
按需注册的定时任务,当instanceinfo和status发生变化时,需要向server同步,去更新自己在server中的实例信息。保证server注册表中服务实例信息的有效和可用。
参见initScheduledTasks()。
心跳定时任务和缓存刷新定时任务是有scheduler 的 schedule提交的,查看scheduler变量声明上,看到注释:
1 2 3 A scheduler to be used for the following 3 tasks: - updating service urls - scheduling a TimedSuperVisorTask。
由此可以知道循环逻辑是由TimedSuperVisorTask实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 //心跳定时任务 new TimedSupervisorTask( "heartbeat", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread()) //HeartbeatThread线程内部run方法定义如下: class CacheRefreshThread implements Runnable{ public void run() { if (renew()) { lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis(); } } } //缓存定时任务 scheduler.schedule( new TimedSupervisorTask( "cacheRefresh", scheduler, cacheRefreshExecutor, registryFetchIntervalSeconds, TimeUnit.SECONDS, expBackOffBound, new CacheRefreshThread() ), //CacheRefreshThread线程内部run方法定义如下: class CacheRefreshThread implements Runnable { public void run() { refreshRegistry(); } } //还有一个定时任务,按需注册。当instanceinfo和status发生变化时,需要向server同步,去更新自己在server中的实例信息。保证server注册表中服务实例信息的有效和可用。 // InstanceInfo replicator instanceInfoReplicator = new InstanceInfoReplicator( this, instanceInfo, clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2); // burstSize statusChangeListener = new ApplicationInfoManager.StatusChangeListener() { @Override public String getId() { return "statusChangeListener"; } @Override public void notify(StatusChangeEvent statusChangeEvent) { if (InstanceStatus.DOWN == statusChangeEvent.getStatus() || InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) { // log at warn level if DOWN was involved logger.warn("Saw local status change event {}", statusChangeEvent); } else { logger.info("Saw local status change event {}", statusChangeEvent); } instanceInfoReplicator.onDemandUpdate(); } }; if (clientConfig.shouldOnDemandUpdateStatusChange()) { applicationInfoManager.registerStatusChangeListener(statusChangeListener); } instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds()); //此定时任务有2个部分, // 1:定时刷新服务实例信息和检查应用状态的变化,在服务实例信息发生改变的情况下向server重新发起注册。InstanceInfoReplicator点进去。看到一个方法 public void run() { try { discoveryClient.refreshInstanceInfo();//刷新instanceinfo。 //如果实例信息有变,返回数据更新时间。 Long dirtyTimestamp = instanceInfo.isDirtyWithTime(); if (dirtyTimestamp != null) { discoveryClient.register();//注册服务实例。 instanceInfo.unsetIsDirty(dirtyTimestamp); } } catch (Throwable t) { logger.warn("There was a problem with the instance info replicator", t); } finally { //延时执行下一个检查任务。用于再次调用run方法,继续检查服务实例信息和状态的变化。 Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS); scheduledPeriodicRef.set(next); } } refreshInstanceInfo点进去,看方法注释:如果有变化,在下次心跳时,同步向server。 //2.注册状态改变监听器,在应用状态发生变化时,刷新服务实例信息,在服务实例信息发生改变时向server注册。 看这段 statusChangeListener = new ApplicationInfoManager.StatusChangeListener() { @Override public String getId() { return "statusChangeListener"; } @Override public void notify(StatusChangeEvent statusChangeEvent) { if (InstanceStatus.DOWN == statusChangeEvent.getStatus() || InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) { // log at warn level if DOWN was involved logger.warn("Saw local status change event {}", statusChangeEvent); } else { logger.info("Saw local status change event {}", statusChangeEvent); } instanceInfoReplicator.onDemandUpdate(); } };如果状态发生改变,调用onDemandUpdate(),点onDemandUpdate进去,看到InstanceInfoReplicator.this.run(); //总结:两部分,一部分自己去检查,一部分等待状态监听事件。 //初始化定时任务完成,最后一步启动步骤完成。接下来就是正常服务于业务。然后消亡。
服务下线 服务下线:在应用关闭时,client会向server注销自己,在Discoveryclient销毁前,会执行下面清理方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 //此方法上有一个注解,表示:在销毁前执行此方法。unregisterStatusChangeListener注销监听器。 @PreDestroy @Override public synchronized void shutdown() { ...... //取消定时任务。 cancelScheduledTasks(); ...... //服务下线。 unregister(); //关闭jersy客户端 eurekaTransport.shutdown(); //其他操作 ...... } unregister点进去。cancel点进去。AbstractJerseyEurekaHttpClient。String urlPath = "apps/" + appName + '/' + id;看到url和http请求delete方法。
更多 更进一步的启动流程分析,可以参考这篇文章:Eureka之Client端注册
下面这张图即来自该文章,感觉还可以。
在一个spring cloud项目中,eureka客户端中的关键类DiscoveryClient是怎样实例化的? 首先,需要了解spring boot如何使用spring factories机制加载第三方库,这个可以参考我之前整理的这篇文章:源码分析之Spring Boot如何利用Spring Factories机制进行自动注入 。
以笔者本人使用eureka时所使用的spring-cloud-netflix-eureka-client-2.2.2.RELEASE为例,在pom文件中引入该包后,可以IDEA中看到该包下包含了spring.factories
该文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\ org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\ org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\ org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\ org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration,\ org.springframework.cloud.netflix.eureka.reactive.EurekaReactiveDiscoveryClientConfiguration,\ org.springframework.cloud.netflix.eureka.loadbalancer.LoadBalancerEurekaAutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration
我们点EurekaDiscoveryClientConfigServiceBootstrapConfiguration,可以看到如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @ConditionalOnClass(ConfigServicePropertySourceLocator.class) @ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled", matchIfMissing = false) @Configuration(proxyBeanMethods = false) @Import({ EurekaDiscoveryClientConfiguration.class, // this emulates // @EnableDiscoveryClient, the import // selector doesn't run before the // bootstrap phase EurekaClientAutoConfiguration.class, EurekaReactiveDiscoveryClientConfiguration.class, ReactiveCommonsClientAutoConfiguration.class }) public class EurekaDiscoveryClientConfigServiceBootstrapConfiguration { }
在EurekaClientAutoConfiguration中,可以找到如下代码:
1 2 CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, this.optionalArgs, this.context);
而CloudEurekaClient类,实际上继承自DiscoveryClient:
1 2 3 public class CloudEurekaClient extends DiscoveryClient { ...... }
所以,在spring boot启动时,将通过spring factories机制,实例化DiscoveryClient。