【SpringCloud】Eureka Client源码分析
上一节Eureka Server 源码分析讲述了 Eureka Server 的原理及部分源码,今天咱们来看看 Eureka Client 端的源码,功能点类似 Eureka Server。
3.7、Eureka Client 源码分析
Eureka Client 通过 Starter 的方式引入依赖, SpringBoot 将会为项目使用以下的自动配置类:
- EurekaClientAutoConfiguration:Eureka Client 自动配置类,负责 Eureka Client 中关键Bean的配置和初始化;
- RibbonEurekaAutoConfiguration:Ribbon 负载均衡相关配置;
- EurekaDiscoveryClientConfiguration:配置自动注册、服务发现和应用的健康检查器。
3.7.1、读取应用自身配置信息
DiscoveryClient 是 Spring Cloud 中用于进行服务发现的顶级接口,也是核心接口,在 Netflix Eureka 或者 Alibaba Nacos 或者 Consul 中都有相应的具体实现类。
public interface DiscoveryClient extends Ordered {/** * Default order of the discovery client. */int DEFAULT_ORDER = 0;/** * A human-readable description of the implementation, used in HealthIndicator. * @return The description. */ //获取实现类的描述String description();/** * Gets all ServiceInstances associated with a particular serviceId. * @param serviceId The serviceId to query. * @return A List of ServiceInstance. */ //通过服务id获取服务实例的信息List<ServiceInstance> getInstances(String serviceId);/** * @return All known service IDs. */ //获取所有服务的实例idList<String> getServices();/** * Default implementation for getting order of discovery clients. * @return order */@Overridedefault int getOrder() {return DEFAULT_ORDER;}}
而在 Eureka 方面的实现,主要的实现类即为 EurekaDiscoveryClient。但是仔细看 EurekaDiscoveryClient 代码中会发现它会使用原生的 Eureka 中的代码:
public class EurekaDiscoveryClient implements DiscoveryClient { //other... //引入原生的EurekaClient接口 private final EurekaClient eurekaClient; @Override public String description() { return DESCRIPTION; } @Override public List<ServiceInstance> getInstances(String serviceId) { List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId, false); List<ServiceInstance> instances = new ArrayList<>(); for (InstanceInfo info : infos) { instances.add(new EurekaServiceInstance(info)); } return instances; } @Override public List<String> getServices() { Applications applications = this.eurekaClient.getApplications(); if (applications == null) { return Collections.emptyList(); } List<Application> registered = applications.getRegisteredApplications(); List<String> names = new ArrayList<>(); for (Application app : registered) { if (app.getInstances().isEmpty()) { continue; } names.add(app.getName().toLowerCase()); } return names; }}
此时的 EurekaClient 接口所在的包为 com.netflix.discovery
,也就是说 Spring Cloud 通过内部组合方式调用了原生 Eureka 中的服务发现方法。而该 EurekaClient 接口的实现类默认是 DiscoveryClient 类,而该类属于原生 Eureka 中的服务发现类,所在的包为com.netflix.discovery
,是不是有点迷糊了。
仔细看代码,就会发现 Spring Cloud 中 DiscoveryClient 接口中的几个方法都是依靠 Eureka 原生接口 EurekaClient 来实现的,而原生 EurekaClient 默认指定的实现类为 DiscoveryClient ,所以归根到底主要看 DiscoveryClient 源码。
3.7.2、服务发现:DiscoveryClient
在讲解 Eureka Server 的时候,InstanceRegistry 也实现了 LookupService 接口, 同样原生的 EurekaClient 也实现了该接口,并在原来的基础上新增了很多检索服务的方法,有兴趣的朋友可以查看:
- 提供了多种方式获取 InstanceInfo,例如根据区域、地址等方式;
- 提供了为客户端注册和获取服务健康检查处理器的能力。
除去一般的检索服务的接口,主要关注 EurekaClient中的两个接口方法,分别是:
//DiscoveryClient#registerHealthCheck// 为Eureka Client注册健康检查处理器public void registerHealthCheck(HealthCheckHandler healthCheckHandler) { if (instanceInfo == null) { logger.error("Cannot register a healthcheck handler when instance info is null!"); } if (healthCheckHandler != null) { this.healthCheckHandlerRef.set(healthCheckHandler); // schedule an onDemand update of the instanceInfo when a new healthcheck handler is registered if (instanceInfoReplicator != null) { instanceInfoReplicator.onDemandUpdate(); } }}//DiscoveryClient#registerEventListener// 监听Client服务实例信息的更新public void registerEventListener(EurekaEventListener eventListener) { this.eventListeners.add(eventListener);}
Eureka Server 一般通过心跳 (heartbeat)来识别一个实例的状态。Eureka Client 中存在一个定时任务定时通过 HealthCheckHandlerClient 检测当前 Client 的状态 ,如 Client 的状态发生改变, 将会触发新的注册事件 ,更新 Eureka Server 注册表中该服务实例的相关信息。
HealthCheckHandler接口代码如下:
public interface HealthCheckHandler { InstanceInfo.InstanceStatus getStatus(InstanceInfo.InstanceStatus currentStatus);}
在spring-cloud-netflix-eureka-client
中的实现主要是EurekaHealthCheckHandler, 它主要使用了spring-cloud-actuator
中的 HealthAggregator 和 HealthIndicator,用于监测服务实例的状态。
而 EurekaEventListener注册的事件监听模式属于观察者模式,当服务实例的状态发生改变的时候,就会触发事件,仔细观察 EurekaClient中有个方法:
//DiscoveryClient#fireEventprotected void fireEvent(final EurekaEvent event) { for (EurekaEventListener listener : eventListeners) { try { listener.onEvent(event); } catch (Exception e) { logger.info("Event {} throw an exception for listener {}", event, listener, e.getMessage()); } }}
该fireEvent
方法即为触发的事件。
3.7.3、DiscoveryClient构造函数
在 DiscoveryClient 构造函数中,Eureka Client 会执行从 Eureka Server 中拉取注册表信息、服务注册、 初始化发送心跳、缓存刷新( 重新拉取注册表信息 )和按需注册定时任务等操作,可以说 DiscoveryClient 的构造函数贯穿 Eureka Client 启动阶段的各项工作。
@InjectDiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) { if (args != null) { this.healthCheckHandlerProvider = args.healthCheckHandlerProvider; this.healthCheckCallbackProvider = args.healthCheckCallbackProvider; this.eventListeners.addAll(args.getEventListeners()); this.preRegistrationHandler = args.preRegistrationHandler; } else { this.healthCheckCallbackProvider = null; this.healthCheckHandlerProvider = null; this.preRegistrationHandler = null; } this.applicationInfoManager = applicationInfoManager; InstanceInfo myInfo = applicationInfoManager.getInfo(); clientConfig = config; staticClientConfig = clientConfig; transportConfig = config.getTransportConfig(); instanceInfo = myInfo; if (myInfo != null) { appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId(); } else { logger.warn("Setting instanceInfo to a passed in null value"); } this.backupRegistryProvider = backupRegistryProvider; this.endpointRandomizer = endpointRandomizer; this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo); localRegionApps.set(new Applications()); fetchRegistryGeneration = new AtomicLong(0); remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions()); remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(",")); 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; } 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; } logger.info("Initializing Eureka in region {}", clientConfig.getRegion()); if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) { logger.info("Client configured to neither register nor query for data."); scheduler = null; heartbeatExecutor = null; cacheRefreshExecutor = null; eurekaTransport = null; instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion()); // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance() // to work with DI'd DiscoveryClient DiscoveryManager.getInstance().setDiscoveryClient(this); DiscoveryManager.getInstance().setEurekaClientConfig(config); initTimestampMs = System.currentTimeMillis(); logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}", initTimestampMs, this.getApplications().size()); return; // no need to setup up an network tasks and we are done } try { // 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 = new EurekaTransport(); scheduleServerEndpointTask(eurekaTransport, args); AzToRegionMapper azToRegionMapper; if (clientConfig.shouldUseDnsForFetchingServiceUrls()) { azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig); } else { azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig); } if (null != remoteRegionsToFetch.get()) { azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(",")); } instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion()); } catch (Throwable e) { throw new RuntimeException("Failed to initialize DiscoveryClient!", e); } if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) { fetchRegistryFromBackup(); } // call and execute the pre registration handler before all background tasks (inc registration) is started if (this.preRegistrationHandler != null) { this.preRegistrationHandler.beforeRegistration(); } 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); } } // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch initScheduledTasks(); try { Monitors.registerObject(this); } catch (Throwable e) { logger.warn("Cannot register timers", e); } // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance() // to work with DI'd DiscoveryClient DiscoveryManager.getInstance().setDiscoveryClient(this); DiscoveryManager.getInstance().setEurekaClientConfig(config); initTimestampMs = System.currentTimeMillis(); logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}", initTimestampMs, this.getApplications().size());}
3.7.4、读取应用配置信息
忽略掉构造函数中的大部分赋值操作,逐步分析:
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;}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;}
上述代码看到了熟悉的配置,eureka.client.fetch-registry
和 eureka.client.register-with-eureka
。如果eureka.client.fetch-registry
为true
的时候表示 Eureka Client 将从 Eureka Server 中拉取注册表信息。而eureka.client.register-with-eureka
为true
表示 Eureka Client 将注册到 Eureka Server 中。所以如果上述的两个配置均为false
,那么 DiscoveryClient 的初始化将直接结束,表示客户端既不进行服务注册,也不进行服务发现。
// default size of 2 - 1 each for heartbeat and cacheRefreshscheduler = 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 handoffcacheRefreshExecutor = new ThreadPoolExecutor( 1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadFactoryBuilder() .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d") .setDaemon(true) .build()); // use direct handoff
接着定义了基于线程池的定时器线程池 ScheduledExecutorService ,线程池的大小为2,一个线程用于发送心跳,一个线程用于缓存刷新,同时定义了发送心跳和缓存刷新的线程池。
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) { fetchRegistryFromBackup();}
3.7.5、拉取注册表信息
如果 EurekaClientConfig 的 shouldFetchRegistry
为true
时, fetchRegistry
方法将会被调用 。在Eureka Client 向 Eureka Server 注册前,需要先从 Eureka Server 拉取注册表中的信息,这是服务发现的前提。通过将 Eureka Server 中的注册表信息缓存到本地,就可以就近获取其它服务的相关信息, 从而减少与 Eureka Server 的网络通信。
//DiscoveryClient#fetchRegistryprivate boolean fetchRegistry(boolean forceFullRegistryFetch) { Stopwatch tracer = FETCH_REGISTRY_TIMER.start(); try { // If the delta is disabled or if it is the first time, get all // applications //获取所有的服务实例信息 Applications applications = getApplications(); //判断增量式拉取被禁止,或者Applications为null,将进行全量式拉取 if (clientConfig.shouldDisableDelta() || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress())) || forceFullRegistryFetch || (applications == null) || (applications.getRegisteredApplications().size() == 0) || (applications.getVersion() == -1)) //Client application does not have latest library supporting delta { logger.info("Disable delta property : {}", clientConfig.shouldDisableDelta()); logger.info("Single vip registry refresh property : {}", 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() == -1)); //全量拉取注册表信息 getAndStoreFullRegistry(); } else { //增量拉取注册表信息 getAndUpdateDelta(applications); } //计算应用集合一致性hashcode applications.setAppsHashCode(applications.getReconcileHashCode()); //打印注册表上所有服务实例的数量 logTotalInstances(); } catch (Throwable e) { logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e); return false; } finally { if (tracer != null) { tracer.stop(); } } // Notify about cache refresh before updating the instance remote status //缓存刷新 onCacheRefreshed(); // Update remote status based on refreshed data held in the cache //更新服务实例 updateInstanceRemoteStatus(); // registry was fetched successfully, so return true return true;}
一般来说,在 Eureka 客户端,除了第一次拉取全量注册表信息,之后的信息拉取都会尝试只进行增量式拉取。
1)、全量式拉取的方法如下:
//DiscoveryClient#getAndStoreFullRegistryprivate void getAndStoreFullRegistry() throws Throwable { long currentUpdateGeneration = fetchRegistryGeneration.get(); logger.info("Getting all instance registry info from the eureka server"); Applications apps = null; EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null ? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get()) : eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get()); //如果响应码为200,表示成功 if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) { apps = 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"); //使用CAS判断更新版本是否发生变化,以免拉取的脏数据覆盖本地注册表信息 } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) { //从apps中筛选出状态为UP的服务实例, 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"); }}
不管是调用eurekaTransport.queryClient.getApplications(...)
,还是调用eurekaTransport.queryClient.getVip(...)
,两者内部都是调用同一个方法getApplicationsInternal
,而且交给 Jersey 客户端实现:
//AbstractJerseyEurekaHttpClient#getApplications@Overridepublic EurekaHttpResponse<Applications> getApplications(String... regions) { //请求路径为/eureka/apps/ return getApplicationsInternal("apps/", regions);}//AbstractJerseyEurekaHttpClient#getDelta@Overridepublic EurekaHttpResponse<Applications> getDelta(String... regions) { //请求路径为/eureka/apps/delta return getApplicationsInternal("apps/delta", regions);}//AbstractJerseyEurekaHttpClient#getVip@Overridepublic EurekaHttpResponse<Applications> getVip(String vipAddress, String... regions) { //请求路径为/eureka/vips/ return getApplicationsInternal("vips/" + vipAddress, regions);}
查看getApplicationsInternal
方法:
//AbstractJerseyEurekaHttpClient#getApplicationsInternalprivate EurekaHttpResponse<Applications> getApplicationsInternal(String urlPath, String[] regions) { ClientResponse response = null; String regionsParamValue = null; try { //使用Jersey客户端发送请求 WebResource webResource = jerseyClient.resource(serviceUrl).path(urlPath); if (regions != null && regions.length > 0) { regionsParamValue = StringUtil.join(regions); webResource = webResource.queryParam("regions", regionsParamValue); } Builder requestBuilder = webResource.getRequestBuilder(); addExtraHeaders(requestBuilder); response = requestBuilder.accept(MediaType.APPLICATION_JSON_TYPE).get(ClientResponse.class); Applications applications = null; if (response.getStatus() == Status.OK.getStatusCode() && response.hasEntity()) { applications = response.getEntity(Applications.class); } return anEurekaHttpResponse(response.getStatus(), Applications.class) .headers(headersOf(response)) .entity(applications) .build(); } finally { if (logger.isDebugEnabled()) { logger.debug("Jersey HTTP GET {}/{}?{}; statusCode={}", serviceUrl, urlPath, regionsParamValue == null ? "" : "regions=" + regionsParamValue, response == null ? "N/A" : response.getStatus() ); } if (response != null) { response.close(); } }}
通过跟踪调试,在该方法内会发现会发送相关的请求url,接口路径为/eureka/apps
,请求方式为GET
,如图所示:
2)、增量式拉取注册表信息代码如下:
//DiscoveryClient#getAndUpdateDeltaprivate void getAndUpdateDelta(Applications applications) throws Throwable { long currentUpdateGeneration = fetchRegistryGeneration.get(); Applications delta = null; //发送增量式拉取注册表信息请求 EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get()); if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) { delta = httpResponse.getEntity(); } 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."); getAndStoreFullRegistry(); } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) { logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode()); String reconcileHashCode = ""; if (fetchRegistryUpdateLock.tryLock()) { try { updateDelta(delta); reconcileHashCode = getReconcileHashCode(applications); } finally { fetchRegistryUpdateLock.unlock(); } } else { logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta"); } // There is a diff in number of instances for some reason if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) { //如果hashCode不一致,则进行全量式拉取 reconcileAndLogDifference(delta, reconcileHashCode); // this makes a remoteCall } } 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()); }}
增量式拉取方式,一般发生在第一次全量拉取注册表信息之后,拉取的信息定义为从某一段时间之后发生的所有变更信息。增量式拉取的目的是为了维护 Eureka Client 本地的注册表信息与 Eureka Server 注册表信息的一致性,防止数据过久而失效,同时采用增量式拉取的方式减少了拉取注册表信息的通信量。Eureka Client 中有一个注册表缓存刷新定时器TimedSupervisorTask类型的 cacheRefreshTask
专门负责维护两者之间信息的同步性。但是当增量式拉取出现意外时,定时器将执行全量拉取以更新本地缓存的注册表信息。
回到上述的增量式拉取注册表信息的代码中getDelta
同样调用getApplicationsInternal
方法,请求路径为/eureka/delta
。如果获取失败,会进行全量拉取注册表信息,否则就通过CAS判断一致性,如果一致则更新本地缓存并计算应用的一致性hashCode。最后再判断计算出来的hashCode和 Eureka Server传递的delta
上的appsHashCode
进行比较,比对客户端和服务端上注册表的差异。如果不一致,将再次调用reconcileAndLogDifference
全量式拉取注册表数据保证 Eureka Server 与 Eureka Client 之间注册表数据的一致。
//DiscoveryClient#reconcileAndLogDifferenceprivate void reconcileAndLogDifference(Applications delta, String reconcileHashCode) throws Throwable { logger.debug("The Reconcile hashcodes do not match, client : {}, server : {}. Getting the full registry", reconcileHashCode, delta.getAppsHashCode()); RECONCILE_HASH_CODES_MISMATCH.increment(); long currentUpdateGeneration = fetchRegistryGeneration.get(); //全量式拉取注册表信息 EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null ? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get()) : eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get()); Applications serverApps = httpResponse.getEntity(); if (serverApps == null) { logger.warn("Cannot fetch full registry from the server; reconciliation failure"); return; } if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) { localRegionApps.set(this.filterAndShuffle(serverApps)); getApplications().setVersion(delta.getVersion()); logger.debug( "The Reconcile hashcodes after complete sync up, client : {}, server : {}.", getApplications().getReconcileHashCode(), delta.getAppsHashCode()); } else { logger.warn("Not setting the applications map as another thread has advanced the update generation"); }}
仔细观察reconcileAndLogDifference
就会发现,它同getAndStoreFullRegistry
的逻辑非常相似,在此就不累赘了。
3.7.6、服务注册
拉取完 Eureka Server 的注册表信息后,将对服务实例进行注册,代码如下:
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); }}//DiscoveryClient#register//注册方法boolean register() throws Throwable { logger.info(PREFIX + "{}: registering service...", appPathIdentifier); EurekaHttpResponse<Void> httpResponse; try { //发送注册请求 httpResponse = eurekaTransport.registrationClient.register(instanceInfo); } catch (Exception e) { logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e); throw e; } if (logger.isInfoEnabled()) { logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode()); } //Status.NO_CONTENT.getStatusCode()==204 return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();}
Eureka Client 会将自身服务实例元数据封装在 InstanceInfo对象中,并发送到 Eureka Server中进行服务注册请求。
当Eureka Server 返回 204 状态码时,说明服务注册成功。
进入到eurekaTransport.registrationClient.register(...)
方法内,可以观察到 Eureka Client 的请求路径为/eureka/apps/${APP_NAME}
,使用POST
请求方式。
3.7.7、初始化定时器
接着,服务注册完成之后,代码到了下面这一行:
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetchinitScheduledTasks();
从英文翻译可以获取到它进行了初始化定时任务。Eureka Client为了维持自己在 Eureka Server 注册表上的租约,需要通过发送心跳的方式与 Eureka Server 进行通信。同时 Eureka Server 注册表中的服务实例也是动态变化的,为了保持 Eureka Client 与 Eureka Server 的注册表信息的一致性, Eureka Client 要定时向Eureka Server 拉取注册表信息并更新本地缓存。为了监控Eureka Client 应用信息和状态的变化, Eureka Client 设置了一个按需注册定时器,定时检查应用信息或者状态的变化, 并在发生变化时向 Eureka Server 重新注册,避免注册表中的本服务实例信息不可用。
//DiscoveryClient#initScheduledTasksprivate void initScheduledTasks() { if (clientConfig.shouldFetchRegistry()) { // registry cache refresh timer //注册表缓存刷新定时器 int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds(); int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound(); //缓存刷新定时器 cacheRefreshTask = new TimedSupervisorTask( "cacheRefresh", scheduler, cacheRefreshExecutor, registryFetchIntervalSeconds, TimeUnit.SECONDS, expBackOffBound, new CacheRefreshThread() ); scheduler.schedule( cacheRefreshTask, registryFetchIntervalSeconds, TimeUnit.SECONDS); } if (clientConfig.shouldRegisterWithEureka()) { int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound(); logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs); // Heartbeat timer //心跳定时器 heartbeatTask = new TimedSupervisorTask( "heartbeat", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread() ); scheduler.schedule( heartbeatTask, renewalIntervalInSecs, TimeUnit.SECONDS); // 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()); } else { logger.info("Not registering with Eureka server per configuration"); }}
在DiscoveryClient的initScheduledTasks
方法中初始化了三个定时器任务,一个用于向 Eureka Server 拉取注册表信息刷新本地缓存;一个用于向Eureka Server 发送心跳;一个用于进行按需注册的操作。
通过 ScheduledExecutorService 的schedule
的方式提交缓存刷新定时任务和发送心跳定时任务,任务执行的方式为延时执行并且不循环,而这两个任务的定时循环逻辑由TimedSupervisorTask来实现。而TimedSupervisorTask继承了TimerTask,提供了执行定时任务的功能,具体定时任务的逻辑在run
方法中:
//TimedSupervisorTask#runpublic void run() { Future> future = null; try { //提交任务并执行 future = executor.submit(task); threadPoolLevelGauge.set((long) executor.getActiveCount()); //等待任务执行结果,带有超时时间 future.get(timeoutMillis, TimeUnit.MILLISECONDS); // block until done or timeout //设置下次任务执行的时间间隔 delay.set(timeoutMillis); threadPoolLevelGauge.set((long) executor.getActiveCount()); successCounter.increment(); } catch (TimeoutException e) { //任务超时 logger.warn("task supervisor timed out", e); timeoutCounter.increment(); long currentDelay = delay.get(); long newDelay = Math.min(maxDelay, currentDelay * 2); delay.compareAndSet(currentDelay, newDelay); } catch (RejectedExecutionException e) { if (executor.isShutdown() || scheduler.isShutdown()) { logger.warn("task supervisor shutting down, reject the task", e); } else { logger.warn("task supervisor rejected the task", e); } //任务被拒绝 rejectedCounter.increment(); } catch (Throwable e) { if (executor.isShutdown() || scheduler.isShutdown()) { logger.warn("task supervisor shutting down, can't accept the task"); } else { logger.warn("task supervisor threw an exception", e); } throwableCounter.increment(); } finally { //如果任务还未结束,就直接取消 if (future != null) { future.cancel(true); } //如果定时任务服务未关闭, 定义下一次任务 if (!scheduler.isShutdown()) { scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS); } }}
run
方法中存在以下的任务调度过程:
scheduler
初始化并延迟执行TimedSupervisorTask;- TimedSupervisorTask将
task
提交给executor
中执行,task
和executor
在初始化TimedSupervisorTask时传入; - 若
task
正常执行,TimedSupervisorTask将自己提交到scheduler
,延迟delay
时间后再次执行; - 若
task
执行超时,计算新的delay
,TimedSupervisorTask将自己提交到scheduler
,延迟delay
时间后再执行。
TimedSupervisorTask通过这种不断循环提交任务的方式,完成定时执行任务的要求。
在DiscoveryClient的initScheduledTasks
方法中,提交缓存刷新的定时任务的线程为CacheRefreshThread,提交发送心跳定时任务的线程为HeartbeatThread,两者均继承自Runnable。
//缓存刷新定时任务线程class CacheRefreshThread implements Runnable { public void run() { refreshRegistry(); }}//发送心跳定时任务线程private class HeartbeatThread implements Runnable { public void run() { if (renew()) { lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis(); } }}
3.7.7.1、缓存刷新定时任务&发送心跳定时任务
缓存刷新定时任务执行的逻辑代码为:
//DiscoveryClient#refreshRegistryvoid refreshRegistry() { try { boolean isFetchingRemoteRegionRegistries = isFetchingRemoteRegionRegistries(); boolean remoteRegionsModified = false; // This makes sure that a dynamic change to remote regions to fetch is honored. String latestRemoteRegions = clientConfig.fetchRegistryForRemoteRegions(); if (null != latestRemoteRegions) { String currentRemoteRegions = remoteRegionsToFetch.get(); if (!latestRemoteRegions.equals(currentRemoteRegions)) { // Both remoteRegionsToFetch and AzToRegionMapper.regionsToFetch need to be in sync synchronized (instanceRegionChecker.getAzToRegionMapper()) { if (remoteRegionsToFetch.compareAndSet(currentRemoteRegions, latestRemoteRegions)) { String[] remoteRegions = latestRemoteRegions.split(","); remoteRegionsRef.set(remoteRegions); instanceRegionChecker.getAzToRegionMapper().setRegionsToFetch(remoteRegions); remoteRegionsModified = true; } else { logger.info("Remote regions to fetch modified concurrently," + " ignoring change from {} to {}", currentRemoteRegions, latestRemoteRegions); } } } else { // Just refresh mapping to reflect any DNS/Property change instanceRegionChecker.getAzToRegionMapper().refreshMapping(); } } boolean success = fetchRegistry(remoteRegionsModified); if (success) { registrySize = localRegionApps.get().size(); lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis(); } if (logger.isDebugEnabled()) { StringBuilder allAppsHashCodes = new StringBuilder(); allAppsHashCodes.append("Local region apps hashcode: "); allAppsHashCodes.append(localRegionApps.get().getAppsHashCode()); allAppsHashCodes.append(", is fetching remote regions? "); allAppsHashCodes.append(isFetchingRemoteRegionRegistries); for (Map.Entry<String, Applications> entry : remoteRegionVsApps.entrySet()) { allAppsHashCodes.append(", Remote region: "); allAppsHashCodes.append(entry.getKey()); allAppsHashCodes.append(" , apps hashcode: "); allAppsHashCodes.append(entry.getValue().getAppsHashCode()); } logger.debug("Completed cache refresh task for discovery. All Apps hash code is {} ", allAppsHashCodes); } } catch (Throwable e) { logger.error("Cannot fetch registry from server", e); }}
CacheRefreshThread 依托fetchRegistry
方式进行缓存刷新,具体逻辑可以在之前的章节中(#3.7.5)查看。
而HeartbeatThread通过定时发送心跳请求,维持在 Eureka Server注册表中的租约。
//DiscoveryClient#renewboolean renew() { EurekaHttpResponse<InstanceInfo> httpResponse; try { //发送心跳请求,参数为服务名,服务id和服务实例 httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null); logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode()); //如果响应码为404,则将当前实例进行注册 if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) { REREGISTER_COUNTER.increment(); logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName()); long timestamp = instanceInfo.setIsDirtyWithTime(); //注册 boolean success = register(); if (success) { instanceInfo.unsetIsDirty(timestamp); } return success; } return httpResponse.getStatusCode() == Status.OK.getStatusCode(); } catch (Throwable e) { logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e); return false; }}
方法内一开始使用Jersey客户端进行发送心跳的请求,根据续租提交的appName,instanceId来更新注册表中的服务实例信息。当注册表中不存在当前该服务实例时,将返回404状态码,同时发送请求的 Eureka Client 会进行重新注册;如果续约成功,则返回200状态码。
//AbstractJerseyEurekaHttpClient#sendHeartBeatpublic EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) { String urlPath = "apps/" + appName + '/' + id; ClientResponse response = null; try { WebResource webResource = jerseyClient.resource(serviceUrl) .path(urlPath) .queryParam("status", info.getStatus().toString()) .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString()); if (overriddenStatus != null) { webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name()); } Builder requestBuilder = webResource.getRequestBuilder(); addExtraHeaders(requestBuilder); response = requestBuilder.put(ClientResponse.class); EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response)); if (response.hasEntity() && !HTML.equals(response.getType().getSubtype())) { //don't try and deserialize random html errors from the server eurekaResponseBuilder.entity(response.getEntity(InstanceInfo.class)); } return eurekaResponseBuilder.build(); } finally { if (logger.isDebugEnabled()) { logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus()); } if (response != null) { response.close(); } }}
从sendHeartBeat
方法中,可以发现服务续约调用的接口以及传递的参数:
接口地址为apps/${APP_NAMAE}/${INSTANCEINFO_ID}
,方法为PUT
请求,参数主要有status
、lastDirtyTimestamp
、overriddenStatus
。
3.7.7.2、按时注册定时任务
按需注册定时任务的作用是,当 Eureka Client 中的 InstanceInfo 或者 status
发生变化时,重新向 Eureka Server 发起注册请求,更新注册表中的服务实例信息,保证 Eureka Server 注册表中服务实例信息有效性和可用性。按需注册定时任务的代码如下:
// InstanceInfo replicatorinstanceInfoReplicator = new InstanceInfoReplicator( this, instanceInfo, clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2); // burstSizestatusChangeListener = 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());
按需注册定义了一个定时任务,同时也注册了状态改变监控器,在应用状态发生变化时,刷新服务实例信息和检查应用状态的变化,在服务实例信息发生改变的情况下向 Eureka Server 重新发起注册操作。而状态改变监控器的主要逻辑在 InstanceInfoReplicator 的 run
方法中:
//InstanceInfoReplicator#runpublic void run() { try { //服务实例刷新 discoveryClient.refreshInstanceInfo(); Long dirtyTimestamp = instanceInfo.isDirtyWithTime(); if (dirtyTimestamp != null) { //如果有dirty标记,则对服务进行注册 discoveryClient.register(); //将dirty标记清除掉 instanceInfo.unsetIsDirty(dirtyTimestamp); } } catch (Throwable t) { logger.warn("There was a problem with the instance info replicator", t); } finally { //执行下一次定时任务 Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS); scheduledPeriodicRef.set(next); }}
//DiscoveryClient#refreshInstanceInfovoid refreshInstanceInfo() { //刷新数据中心 applicationInfoManager.refreshDataCenterInfoIfRequired(); //刷新实例信息 applicationInfoManager.refreshLeaseInfoIfRequired(); InstanceStatus status; try { status = getHealthCheckHandler().getStatus(instanceInfo.getStatus()); } catch (Exception e) { logger.warn("Exception from healthcheckHandler.getStatus, setting status to DOWN", e); status = InstanceStatus.DOWN; } if (null != status) { //实例状态变更 applicationInfoManager.setInstanceStatus(status); }}
在run
方法中,首先调用了DiscoveryClient中的refreshInstanceInfo
方法刷新当前服务实例信息,查看当前服务实例信息和状态是否发生了改变。如果发生变化则会向 Eureka Server 重新对服务实例进行注册,同时变更状态,最后在finally
代码块中,定义了下一次的延时任务,用于再次调用run
方法。还记得在 Eureka 原生接口中定义了HealthCheckHandler健康检查器,通过getStatus
方法,结合Spring Cloud中的 Actuator进行状态检测。
//EurekaHealthCheckHandler#getStatuspublic InstanceStatus getStatus(InstanceStatus instanceStatus) { return getHealthStatus();}//EurekaHealthCheckHandler#getHealthStatusprotected InstanceStatus getHealthStatus() { final Status status; if (statusAggregator != null) { status = getStatus(statusAggregator); } else { status = getStatus(getHealthIndicator()); } return mapToInstanceStatus(status);}
在refreshInstanceInfo
方法中,如果状态发生改变,则会触发事件:
//ApplicationInfoManager.javapublic synchronized void setInstanceStatus(InstanceStatus status) { InstanceStatus next = instanceStatusMapper.map(status); if (next == null) { return; } InstanceStatus prev = instanceInfo.setStatus(next); if (prev != null) { //循环遍历事件变更监听器的notify方法 for (StatusChangeListener listener : listeners.values()) { try { listener.notify(new StatusChangeEvent(prev, next)); } catch (Exception e) { logger.warn("failed to notify listener: {}", listener.getId(), e); } } }}
记得在initScheduledTasks
方法中声明了一个状态变更监听器,重写了getId
和notify
方法,上述服务一旦状态发生了变更,则会触发该监听器。监听器的逻辑中会触发InstanceInfoReplicator 的 onDemandUpdate
方法,方法中提交了一个线程,而该线程其实还是通过InstanceInfoReplicator 的run
方法来实现。
//InstanceInfoReplicator#onDemandUpdatepublic boolean onDemandUpdate() { if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) { if (!scheduler.isShutdown()) { scheduler.submit(new Runnable() { @Override public void run() { logger.debug("Executing on-demand update of local InstanceInfo"); Future latestPeriodic = scheduledPeriodicRef.get(); if (latestPeriodic != null && !latestPeriodic.isDone()) { logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update"); latestPeriodic.cancel(false); } 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; }}
同时为了防止重复执行run
方法,该方法内会判断上次已提交的但未完成的任务,如果未完成则会先执行cancel
方法取消上次的任务,再执行最新的按需注册任务。
在initScheduledTasks
最后会启动按需注册的定时任务,
//DiscoveryClient#initScheduledTasksinstanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());//InstanceInfoReplicator#startpublic void start(int initialDelayMs) { if (started.compareAndSet(false, true)) { instanceInfo.setIsDirty(); // for initial register Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS); scheduledPeriodicRef.set(next); }}//InstanceInfo#setIsDirtypublic synchronized void setIsDirty() { isInstanceInfoDirty = true;//dirty标记 lastDirtyTimestamp = System.currentTimeMillis();}
3.7.8、服务下线
当应用服务在关闭的时候, Eureka Client 会向 Eureka Server 注销自身在注册表中的信息,由@PreDestroy 注解标识 DiscoveryClient 中对象销毁前执行的清理方法,代码如下:
//DiscoveryClient#shutdown@PreDestroy@Overridepublic synchronized void shutdown() { //同步操作,确保只会执行一次 if (isShutdown.compareAndSet(false, true)) { logger.info("Shutting down DiscoveryClient ..."); //注销状态变更监听器StatusChangeListener if (statusChangeListener != null && applicationInfoManager != null) { applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId()); } //取消定时任务 cancelScheduledTasks(); // If APPINFO was registered if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka() && clientConfig.shouldUnregisterOnShutdown()) { //标记服务状态为DOWN applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN); //服务实例注销 unregister(); } if (eurekaTransport != null) { eurekaTransport.shutdown(); } heartbeatStalenessMonitor.shutdown(); registryStalenessMonitor.shutdown(); Monitors.unregisterObject(this); logger.info("Completed shut down of DiscoveryClient"); }}
在销毁DiscoveryClient之前,会进行一系列清理工作,包括注销 ApplicationInfoManager中的 StatusChangeListener、取消定时任务、服务下线和关闭Jersey客户端。主要关注unregister
服务下线方法,
//DiscoveryClient#unregistervoid unregister() { // It can be null if shouldRegisterWithEureka == false if(eurekaTransport != null && eurekaTransport.registrationClient != null) { try { logger.info("Unregistering ..."); EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId()); logger.info(PREFIX + "{} - deregister status: {}", appPathIdentifier, httpResponse.getStatusCode()); } catch (Exception e) { logger.error(PREFIX + "{} - de-registration failed{}", appPathIdentifier, e.getMessage(), e); } }}
//AbstractJerseyEurekaHttpClient#cancelpublic EurekaHttpResponse<Void> cancel(String appName, String id) { String urlPath = "apps/" + appName + '/' + id; ClientResponse response = null; try { Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder(); addExtraHeaders(resourceBuilder); response = resourceBuilder.delete(ClientResponse.class); return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build(); } finally { if (logger.isDebugEnabled()) { logger.debug("Jersey HTTP DELETE {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus()); } if (response != null) { response.close(); } }}
通过跟踪调试,可以发现服务下线调用的接口地址为apps/${APP_NAME}/${INSTANCEINFO_ID}
,HTTP请求方式为DELETE
。
更多分享正在路上,尽情期待...