Eureka源码分析之Eureka-Client(二)
一、前言
上篇文章提到EurekaInstanceConfig以及EurekaClientConfig两个配置接口通过硬编码+配置文件读取配置信息来创建配置对象,而这一切都是为了创建EurekaClient而生,本文主要是分析EurekaClient的创建。先上个EurekaClient的类结构图:

二、接口/类简单描述
1. LookupService
该接口一共有四个方法
package com.netflix.discovery.shared; import java.util.List; import com.netflix.appinfo.InstanceInfo; public interface LookupService<T> { Application getApplication(String appName); Applications getApplications(); List<InstanceInfo> getInstancesById(String id); InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure); }
这个接口主要定义了一系列从eureka-server注册中心获取服务的接口,InstanceInfo对应的某个服务实例,Application对应的时某个App,该App下可能有多个InstanceInfo实例,而Applications则包含多个Application。从包含角度来说 Applications > Application > InstanceInfo
2. EurekaClient
定义了一系列EurekaClient的接口
package com.netflix.discovery; import java.util.List; import java.util.Set; import javax.annotation.Nullable; import com.google.inject.ImplementedBy; import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.appinfo.HealthCheckCallback; import com.netflix.appinfo.HealthCheckHandler; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.shared.Applications; import com.netflix.discovery.shared.LookupService; @ImplementedBy(DiscoveryClient.class) public interface EurekaClient extends LookupService { // ======================== // getters for InstanceInfo // ======================== public Applications getApplicationsForARegion(@Nullable String region); public Applications getApplications(String serviceUrl); public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure); public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure, @Nullable String region); public List<InstanceInfo> getInstancesByVipAddressAndAppName(String vipAddress, String appName, boolean secure); // ========================== // getters for local metadata // ========================== public InstanceInfo.InstanceStatus getInstanceRemoteStatus(); @Deprecated public List<String> getDiscoveryServiceUrls(String zone); @Deprecated public List<String> getServiceUrlsFromConfig(String instanceZone, boolean preferSameZone); @Deprecated public List<String> getServiceUrlsFromDNS(String instanceZone, boolean preferSameZone); // =========================== // healthcheck related methods // =========================== @Deprecated public void registerHealthCheckCallback(HealthCheckCallback callback); public void registerHealthCheck(HealthCheckHandler healthCheckHandler); public void registerEventListener(EurekaEventListener eventListener); public boolean unregisterEventListener(EurekaEventListener eventListener); public HealthCheckHandler getHealthCheckHandler(); // ============= // other methods // ============= public void shutdown(); public ApplicationInfoManager getApplicationInfoManager(); }
从注释中就可以看到,这些接口可分为以下四种类型:
1. 从注册中心获取其他服务实例InstanceInfo相关
2. 获取自身服务InstanceInfo元信息相关
3. 健康检查相关
4. 其他
3. DiscoveryClient
直接看构造方法
1 // Eureka-Client初始化,后两个参数一般不会用到 2 @Inject 3 DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, 4 Provider<BackupRegistry> backupRegistryProvider) { 5 if (args != null) { 6 this.healthCheckHandlerProvider = args.healthCheckHandlerProvider; 7 this.healthCheckCallbackProvider = args.healthCheckCallbackProvider; 8 this.eventListeners.addAll(args.getEventListeners()); 9 this.preRegistrationHandler = args.preRegistrationHandler; 10 } else { 11 this.healthCheckCallbackProvider = null; 12 this.healthCheckHandlerProvider = null; 13 this.preRegistrationHandler = null; 14 } 15 16 this.applicationInfoManager = applicationInfoManager; 17 InstanceInfo myInfo = applicationInfoManager.getInfo(); 18 19 clientConfig = config; 20 staticClientConfig = clientConfig; 21 transportConfig = config.getTransportConfig(); 22 instanceInfo = myInfo; 23 if (myInfo != null) { 24 // 没实际用途,主要用于logger 25 appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId(); 26 } else { 27 logger.warn("Setting instanceInfo to a passed in null value"); 28 } 29 30 // 备份注册中心,目前没有提供实现 31 this.backupRegistryProvider = backupRegistryProvider; 32 33 this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo); 34 // 初始化应用集合在本地的缓存,size=0 35 localRegionApps.set(new Applications()); 36 37 fetchRegistryGeneration = new AtomicLong(0); 38 39 // 和AWS有关,跳过 40 remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions()); 41 remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(",")); 42 43 if (config.shouldFetchRegistry()) { 44 this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L}); 45 } else { 46 this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC; 47 } 48 49 if (config.shouldRegisterWithEureka()) { 50 this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L}); 51 } else { 52 this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC; 53 } 54 55 logger.info("Initializing Eureka in region {}", clientConfig.getRegion()); 56 57 if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) { 58 logger.info("Client configured to neither register nor query for data."); 59 scheduler = null; 60 heartbeatExecutor = null; 61 cacheRefreshExecutor = null; 62 eurekaTransport = null; 63 instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion()); 64 65 // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance() 66 // to work with DI'd DiscoveryClient 67 DiscoveryManager.getInstance().setDiscoveryClient(this); 68 DiscoveryManager.getInstance().setEurekaClientConfig(config); 69 70 initTimestampMs = System.currentTimeMillis(); 71 logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}", 72 initTimestampMs, this.getApplications().size()); 73 74 return; // no need to setup up an network tasks and we are done 75 } 76 77 try { 78 // default size of 2 - 1 each for heartbeat and cacheRefresh 79 // 设置两个线程执行以下两个heartbeatExecutor 80 scheduler = Executors.newScheduledThreadPool(2, 81 new ThreadFactoryBuilder() 82 .setNameFormat("DiscoveryClient-%d") 83 .setDaemon(true) 84 .build()); 85 86 // 1.初始化心跳线程池 87 heartbeatExecutor = new ThreadPoolExecutor( 88 1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS, 89 new SynchronousQueue<Runnable>(), 90 new ThreadFactoryBuilder() 91 .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d") 92 .setDaemon(true) 93 .build() 94 ); // use direct handoff 95 96 // 2.初始化缓存刷新线程池 97 cacheRefreshExecutor = new ThreadPoolExecutor( 98 1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS, 99 new SynchronousQueue<Runnable>(), 100 new ThreadFactoryBuilder() 101 .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d") 102 .setDaemon(true) 103 .build() 104 ); // use direct handoff 105 106 // 初始化HttpClient,包括registerHttpClient和queryHttpClient 107 eurekaTransport = new EurekaTransport(); 108 scheduleServerEndpointTask(eurekaTransport, args); 109 110 // AWS相关,跳过 111 AzToRegionMapper azToRegionMapper; 112 if (clientConfig.shouldUseDnsForFetchingServiceUrls()) { 113 azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig); 114 } else { 115 azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig); 116 } 117 if (null != remoteRegionsToFetch.get()) { 118 azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(",")); 119 } 120 instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion()); 121 } catch (Throwable e) { 122 throw new RuntimeException("Failed to initialize DiscoveryClient!", e); 123 } 124 125 // 从eureka-server拉取注册信息,fetchRegistry(false)->有全量和增量拉取,初始化时为全量拉取 126 if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) { 127 // 从备份注册中心拉取注册信息,目前暂时没有实现 128 fetchRegistryFromBackup(); 129 } 130 131 // call and execute the pre registration handler before all background tasks (inc registration) is started 132 if (this.preRegistrationHandler != null) { 133 // 注册前的操作,目前暂时没有实现 134 this.preRegistrationHandler.beforeRegistration(); 135 } 136 137 if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) { 138 try { 139 // 注册到eureka-server,enforceRegistrationAtInit默认为false,所以不会马上注册 140 // 后台开启线程注册 141 if (!register() ) { 142 throw new IllegalStateException("Registration error at startup. Invalid server response."); 143 } 144 } catch (Throwable th) { 145 logger.error("Registration error at startup: {}", th.getMessage()); 146 throw new IllegalStateException(th); 147 } 148 } 149 150 // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch 151 // 开启定时任务 152 initScheduledTasks(); 153 154 try { 155 Monitors.registerObject(this); 156 } catch (Throwable e) { 157 logger.warn("Cannot register timers", e); 158 } 159 160 // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance() 161 // to work with DI'd DiscoveryClient 162 DiscoveryManager.getInstance().setDiscoveryClient(this); 163 DiscoveryManager.getInstance().setEurekaClientConfig(config); 164 165 initTimestampMs = System.currentTimeMillis(); 166 logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}", 167 initTimestampMs, this.getApplications().size()); 168 }
这么长的构造函数我也不常见,现在大体下梳理整个流程:
5~53行:属性初始化
77~104行:初始化3个线程池,其中一个为主线程池,另外两个为子线程池,分别用于执行两个定时任务。
110~123行:和AWS(亚马逊云服务)相关,国内基本不会涉及,跳过。
126~129行:从eureka-server拉取服务,如果拉取服务失败,则从备份注册中心拉取(目前备份中心没有具体实现,可能以后版本才会有),其中拉取服务的逻辑在fetchRegistry(false)中。
132~134行:preRegistrationHandler.beforeRegistration(),进行eureka-client初始化前的操作,目前也没有实现,应该是留给用户扩展的方法。
137~148行:向eureka-client注册中心注册服务,因为clientConfig.shouldEnforceRegistrationAtInit()默认为false,再没修改该配置,是不会执行逻辑内的方法,也就是此时不会注册服务,服务注册会在定时任务那里后台注册。
152行:initScheduledTasks(),在这里开始了3个定时任务,分别为服务续约,缓存更新(拉取服务),实例复制(向注册中心注册或更新自身实例)。
拉取服务
1 private boolean fetchRegistry(boolean forceFullRegistryFetch) { 2 // TODO 监控相关 3 Stopwatch tracer = FETCH_REGISTRY_TIMER.start(); 4 5 try { 6 // If the delta is disabled or if it is the first time, get all 7 // applications 8 Applications applications = getApplications(); 9 10 // 【全量】拉取 /apps 11 if (clientConfig.shouldDisableDelta() // 禁用增量 default false 12 || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress())) // default null 13 || forceFullRegistryFetch 14 || (applications == null) 15 || (applications.getRegisteredApplications().size() == 0) // 初始化 size=0 16 || (applications.getVersion() == -1)) //Client application does not have latest library supporting delta 17 { 18 logger.info("Disable delta property : {}", clientConfig.shouldDisableDelta()); 19 logger.info("Single vip registry refresh property : {}", clientConfig.getRegistryRefreshSingleVipAddress()); 20 logger.info("Force full registry fetch : {}", forceFullRegistryFetch); 21 logger.info("Application is null : {}", (applications == null)); 22 logger.info("Registered Applications size is zero : {}", 23 (applications.getRegisteredApplications().size() == 0)); 24 logger.info("Application version is -1: {}", (applications.getVersion() == -1)); 25 26 // 全量从eureka-server拉取注册信息 27 getAndStoreFullRegistry(); 28 } else { 29 // 【增量】拉取 /apps/delta 30 getAndUpdateDelta(applications); 31 } 32 applications.setAppsHashCode(applications.getReconcileHashCode()); 33 logTotalInstances(); 34 } catch (Throwable e) { 35 logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e); 36 return false; 37 } finally { 38 if (tracer != null) { 39 tracer.stop(); 40 } 41 } 42 43 // Notify about cache refresh before updating the instance remote status 44 onCacheRefreshed(); 45 46 // Update remote status based on refreshed data held in the cache 47 updateInstanceRemoteStatus(); 48 49 // registry was fetched successfully, so return true 50 return true; 51 }
拉取实例信息有两种方式,分别为【全量拉取】和【增量拉取】。全量拉取即为把注册中心的所有实例信息一次性拉取到本地,这种方式网络占用大,故有第二种方式增量拉取,顾名思义就是拉取注册中心最近增加的实例信息,这种方式可以减少网络占用。在eureka-client初始化时(第一次)采用的是全量拉取,如果没有禁用增量的话,后续定时任务会进行增量拉取。
27行,全量拉取:getAndStoreFullRegistry(),调用了注册中心的 /apps接口。
30行,增量拉取:getAndUpdateDelta(applications),调用了注册中心的 /apps/delta接口。
44~47行:主要做一些缓存刷新等监听操作。
注册服务
调用了regitry()方法进行服务注册,但事实上按照默认设置的话,由于clientConfig.shouldEnforceRegistrationAtInit()默认为false,所以不会进入这个方法,注册由后台线程(定时任务)发起。
从下面的方法可以看到,注册信息就是InstanceInfo实例。
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()); } return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode(); }
进入register(instanceInfo)方法:
@Override public EurekaHttpResponse<Void> register(InstanceInfo info) { String urlPath = "apps/" + info.getAppName(); ClientResponse response = null; try { Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder(); addExtraHeaders(resourceBuilder); response = resourceBuilder .header("Accept-Encoding", "gzip") .type(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON) .post(ClientResponse.class, info); return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build(); } finally { if (logger.isDebugEnabled()) { logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(), response == null ? "N/A" : response.getStatus()); } if (response != null) { response.close(); } } }
正是通过httpClient调用了注册中心 apps/AppName的接口,请求参数是InstanceInfo。
定时任务
前面说到定时任务initScheduledTasks()分别开启了服务续约、服务注册、服务拉取三个定时任务(可以理解为服务的增改查),下面来看看是怎么实现的:
private void initScheduledTasks() { if (clientConfig.shouldFetchRegistry()) { // registry cache refresh timer -> 服务缓存刷新定时器 // 从server更新本地服务的频率,默认30s // 更新超时延迟重试时间,默认10s int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds(); int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound(); scheduler.schedule( new TimedSupervisorTask( "cacheRefresh", scheduler, cacheRefreshExecutor, registryFetchIntervalSeconds, TimeUnit.SECONDS, expBackOffBound, // 【增量拉取任务】 -> registryFetch() new CacheRefreshThread() ), registryFetchIntervalSeconds, TimeUnit.SECONDS); } // 允许客户端注册到eureka-server if (clientConfig.shouldRegisterWithEureka()) { // 心跳续约周期 int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); // 心跳执行超时后的延迟重试的时间 int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound(); logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs); //1.(服务续租) Heartbeat timer 心跳定时任务 scheduler.schedule( new TimedSupervisorTask( "heartbeat", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread() ), renewalIntervalInSecs, TimeUnit.SECONDS); //2.(服务注册) 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"); } }
这三个定时任务核心是具体的任务实现,事实上大家也可以猜想到最后也就是调用了注册中心的某个接口,进行信息的更新交互,由于笔者水平以及时间有限(主要是前者),就不一一深入。
值得一提的时这些任务调度以一个很巧妙的方式try-catch-finally去实现了任务循环和取消,大家有兴趣的可以深入去看看。
三、总结
1.Eureka-Client是什么?我们可以理解为它就是向注册中心(相当于一个容器)进行服务实例的增删改查的一个实体。
2.注册中心经常会有变化(其他实例的增删改查),那么本地需要通过一系列的定时任务去和注册中心定时通信,维护和更新本地缓存的实例信息。
3.这么看来,注册中心其实就是一个Web应用(实时上确实是WEB应用),提供了各种Http接口来被其他eureka-client调用进行各种增删改查操作。
以上,都是笔者比较浅显的一个理解,事实上源码很多都没有去深入理解,毕竟深入每一个细节需要花费太多时间,笔者暂且先理一下大概脉络和大家分享。
由于水平有限,如果纰漏以及错误之处,希望大家不吝指点。

浙公网安备 33010602011771号