下方是Eureka Client从Eureka Server获取实例信息的总体流程图,后面会详细介绍每个步骤。
Eureka Client在刚启动的时候会从Eureka Server全量获取一次注册信息,同时初始化Eureka Client本地实例信息缓存定时更新任务,默认30s一次 registryFetchIntervalSeconds = 30。
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider) {
//略去部分无用代码.....
//此时正在创建Client实例对象,也就是说Client刚启动的时候,全量获取一次实例信息保存到本地
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
//重点方法fetchRegistry(boolean forceFullRegistryFetch)
//没有获取成功,即fetchRegistry方法返回false,那么从备份中获取注册信息
//获取实例信息异常,可以实现BackupRegistry接口,让eureka client获取一些其他的实例信息
fetchRegistryFromBackup();
}
//略去部分无用代码.....
// finally, init the schedule tasks
//初始化调度任务,刷新Client本地缓存、心跳、实例状态信息复制
initScheduledTasks();
//略去部分无用代码.....
}
重点看fetchRegistry(boolean forceFullRegistryFetch)这个方法,参数forceFullRegistryFetch表示是否全量获取实例信息,可以通过这个参数配置eureka.client.disable-delta,默认是false,即默认采取增量获取模式,后面会讲增量与全量的区别,以及为什么默认采取增量模式。
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
try {
// If the delta is disabled or if it is the first time, get all applications
Applications applications = getApplications();
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
{
//略去log.....
//全量获取,并保存到本地缓存
getAndStoreFullRegistry();
} else {
//增量获取,并更新本地缓存,如果增量获取失败,进行一次全量获取
getAndUpdateDelta(applications);
}
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();
return true;
}
主要看一下增量获取方法的实现,getAndUpdateDelta(Applications applications),delta的意思是数学阿拉伯字符,代表着增量。
注意下面delta == null的条件判断,如果增量获取没有获取到实例信息返回的是new Applications(),而不是null,返回null说明发生了异常。只有增量获取发生了异常,才会再进行一次全量获取。
private void getAndUpdateDelta(Applications applications) throws Throwable {
long currentUpdateGeneration = fetchRegistryGeneration.get();
Applications delta = null;
//增量获取,getDelta的实现
EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
delta = httpResponse.getEntity();
}
//如果增量获取失败,进行一次全量获取
//注意这个地方,是获取失败了,如果没有获取到,那么返回的是个new Applications();
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);
//根据所有实例的信息,生成一个HashCode
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
//比较增量的AppsHashCode和更新之后缓存applications的HashCode,如果不一致,进行一次全量获取。
if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {
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());
}
}
//负责读动作的http请求,全量获取以及增量获取
private EurekaHttpClient queryClient;
EurekaHttpClient的实现类很多,最终发起http调用的是AbstractJerseyEurekaHttpClient这个类,获取实例信息的主要两个方法
注意下面getApplicationsInternal方法,里面的try代码块,并没有catch(并不是我给删了),甚至连log都不打。也就是说,在获取实例信息的时候,即便发生异常也不管,如果发生异常就返回null的Applications。
@Override
public EurekaHttpResponse<Applications> getApplications(String... regions) {
return getApplicationsInternal("apps/", regions);
}
@Override
public EurekaHttpResponse<Applications> getDelta(String... regions) {
return getApplicationsInternal("apps/delta", regions);
}
private EurekaHttpResponse<Applications> getApplicationsInternal(String urlPath, String[] regions) {
ClientResponse response = null;
String regionsParamValue = null;
try {
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 {
//省略...
}
}
我们可以在eureka-core工程下的resources包下面找到Eureka Server暴露的REST接口,getDelta对应的接口在ApplicationsResource里面
@Path("delta")
@GET
public Response getContainerDifferential(
@PathParam("version") String version,
@HeaderParam(HEADER_ACCEPT) String acceptHeader,
@HeaderParam(HEADER_ACCEPT_ENCODING) String acceptEncoding,
@HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept,
@Context UriInfo uriInfo, @Nullable @QueryParam("regions") String regionsStr) {
//省略部分代码...
//缓存的KEY包含实体的类型、增量或全量获取/ApplicationName、region信息、keytype(JSON, XML)
Key cacheKey = new Key(Key.EntityType.Application,
ResponseCacheImpl.ALL_APPS_DELTA,
keyType, CurrentRequestVersion.get(), EurekaAccept.fromString(eurekaAccept), regions
);
if (acceptEncoding != null
&& acceptEncoding.contains(HEADER_GZIP_VALUE)) {
return Response.ok(responseCache.getGZIP(cacheKey))
.header(HEADER_CONTENT_ENCODING, HEADER_GZIP_VALUE)
.header(HEADER_CONTENT_TYPE, returnMediaType)
.build();
} else {
//responseCache为ResponseCacheImpl对象,是Eureka Server缓存
//包含两级缓存,一级缓存为Google Guava Cache,二级缓存为ConcurrentMap<Key, Value>
return Response.ok(responseCache.get(cacheKey))
.build();
}
}
再来看这个responseCache.get(cacheKey)方法,直接到ResponseCacheImpl类里面找get方法
public String get(final Key key) {
return get(key, shouldUseReadOnlyResponseCache);
}
String get(final Key key, boolean useReadOnlyCache) {
Value payload = getValue(key, useReadOnlyCache);
if (payload == null || payload.getPayload().equals(EMPTY_PAYLOAD)) {
return null;
} else {
return payload.getPayload();
}
}
Value getValue(final Key key, boolean useReadOnlyCache) {
Value payload = null;
try {
if (useReadOnlyCache) {
//开启二级缓存
//从二级缓存readOnlyCacheMap中读取
final Value currentPayload = readOnlyCacheMap.get(key);
if (currentPayload != null) {
payload = currentPayload;
} else {
//如果没读到,那么从一级缓存中读,然后再保存到二级缓存
payload = readWriteCacheMap.get(key);
readOnlyCacheMap.put(key, payload);
}
} else {
//不开启二级缓存,直接从一级缓存readWriteCacheMap中读取
payload = readWriteCacheMap.get(key);
}
} catch (Throwable t) {
logger.error("Cannot get value for key : {}", key, t);
}
return payload;
}
默认先从二级缓存中读取,如果二级缓存没有命中,那么接着从再去一级缓存中读取,那么如果一级缓存也没有命中呢,我们再来看看这个readWriteCacheMap,这个采用了Google Guava Cache,在remove的时候可以自定义回调,在get方法没有返回值的时候,去调用load(Key key)方法将返回值返回并保存。
//谷歌Guava缓存,有自动过期功能,这里是默认3分钟
this.readWriteCacheMap =
CacheBuilder.newBuilder().initialCapacity(serverConfig.getInitialCapacityOfResponseCache())
.expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS)
.removalListener(new RemovalListener<Key, Value>() {
@Override
public void onRemoval(RemovalNotification<Key, Value> notification) {
Key removedKey = notification.getKey();
if (removedKey.hasRegions()) {
Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
}
}
})
.build(new CacheLoader<Key, Value>() {
@Override
public Value load(Key key) throws Exception {
if (key.hasRegions()) {
Key cloneWithNoRegions = key.cloneWithoutRegions();
regionSpecificKeys.put(cloneWithNoRegions, key);
}
//此处从Eureka Server底层的双层map去取实例信息
Value value = generatePayload(key);
return value;
}
});
全量获取和增量获取的区别就在这两个方法里getApplicationsFromMultipleRegions,getApplicationDeltasFromMultipleRegions,这两个方法在AbstractInstanceRegistry类里面,这个类可以看做是Eureka Server大部分功能的实现类
private Value generatePayload(Key key) {
Stopwatch tracer = null;
try {
String payload;
switch (key.getEntityType()) {
case Application:
boolean isRemoteRegionRequested = key.hasRegions();
//全量获取
if (ALL_APPS.equals(key.getName())) {
if (isRemoteRegionRequested) {
tracer = serializeAllAppsWithRemoteRegionTimer.start();
payload = getPayLoad(key, registry.getApplicationsFromMultipleRegions(key.getRegions()));
} else {
tracer = serializeAllAppsTimer.start();
payload = getPayLoad(key, registry.getApplications());
}
} //增量获取
else if (ALL_APPS_DELTA.equals(key.getName())) {
if (isRemoteRegionRequested) {
tracer = serializeDeltaAppsWithRemoteRegionTimer.start();
versionDeltaWithRegions.incrementAndGet();
versionDeltaWithRegionsLegacy.incrementAndGet();
payload = getPayLoad(key,
registry.getApplicationDeltasFromMultipleRegions(key.getRegions()));
} else {
tracer = serializeDeltaAppsTimer.start();
versionDelta.incrementAndGet();
versionDeltaLegacy.incrementAndGet();
payload = getPayLoad(key, registry.getApplicationDeltas());
}
} //按appName获取
else {
tracer = serializeOneApptimer.start();
payload = getPayLoad(key, registry.getApplication(key.getName()));
}
break;
case VIP:
case SVIP:
tracer = serializeViptimer.start();
payload = getPayLoad(key, getApplicationsForVip(key, registry));
break;
default:
logger.error("Unidentified entity type: {} found in the cache key.", key.getEntityType());
payload = "";
break;
}
return new Value(payload);
} finally {
if (tracer != null) {
tracer.stop();
}
}
}
看看增量获取的方法getApplicationDeltasFromMultipleRegions内部是怎么实现的。里面有一个recentlyChangedQueue,先从本地最近改变的队列里面获取,再从远程区域的最近改变的队列里面获取。
public Applications getApplicationDeltasFromMultipleRegions(String[] remoteRegions) {
if (null == remoteRegions) {
//allKnownRemoteRegions从Eureka Server 配置文件中配置
remoteRegions = allKnownRemoteRegions; // null means all remote regions.
}
boolean includeRemoteRegion = remoteRegions.length != 0;
Applications apps = new Applications();
apps.setVersion(responseCache.getVersionDeltaWithRegions().get());
Map<String, Application> applicationInstancesMap = new HashMap<String, Application>();
try {
write.lock();
Iterator<RecentlyChangedItem> iter = this.recentlyChangedQueue.iterator();
logger.debug("The number of elements in the delta queue is :{}", this.recentlyChangedQueue.size());
//从最近改变的队列里面获取最近改变的实例信息
while (iter.hasNext()) {
Lease<InstanceInfo> lease = iter.next().getLeaseInfo();
InstanceInfo instanceInfo = lease.getHolder();
logger.debug("The instance id {} is found with status {} and actiontype {}",
instanceInfo.getId(), instanceInfo.getStatus().name(), instanceInfo.getActionType().name());
Application app = applicationInstancesMap.get(instanceInfo.getAppName());
if (app == null) {
app = new Application(instanceInfo.getAppName());
applicationInstancesMap.put(instanceInfo.getAppName(), app);
apps.addApplication(app);
}
app.addInstance(new InstanceInfo(decorateInstanceInfo(lease)));
}
//遍历所有的remoteRegions
if (includeRemoteRegion) {
for (String remoteRegion : remoteRegions) {
//获取注册到其他Region的实例信息
RemoteRegionRegistry remoteRegistry = regionNameVSRemoteRegistry.get(remoteRegion);
if (null != remoteRegistry) {
Applications remoteAppsDelta = remoteRegistry.getApplicationDeltas();
if (null != remoteAppsDelta) {
for (Application application : remoteAppsDelta.getRegisteredApplications()) {
if (shouldFetchFromRemoteRegistry(application.getName(), remoteRegion)) {
Application appInstanceTillNow =
apps.getRegisteredApplications(application.getName());
if (appInstanceTillNow == null) {
appInstanceTillNow = new Application(application.getName());
apps.addApplication(appInstanceTillNow);
}
for (InstanceInfo instanceInfo : application.getInstances()) {
appInstanceTillNow.addInstance(new InstanceInfo(instanceInfo));
}
}
}
}
}
}
}
Applications allApps = getApplicationsFromMultipleRegions(remoteRegions);
apps.setAppsHashCode(allApps.getReconcileHashCode());
return apps;
} finally {
write.unlock();
}
}
那么这个recentlyChangedQueue里面都放着什么样的数据呢,或者说什么时候会向这个队列里面添加数据呢?
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
read.lock();
//省略部分代码...
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
//省略部分代码...
registrant.setActionType(ActionType.ADDED);
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
//发生了新的注册,失效readWriteCacheMap部分缓存,且不是清空全部缓存
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
} finally {
read.unlock();
}
}
这里只列出了注册方法,调用internalCancel、statusUpdate、deleteStatusOverride方法也会向这个队列里面添加数据,当Client发起增量获取请求的时候,就会从这个队列里面消费。
细心考虑的话,会发现register、internalCancel、statusUpdate、deleteStatusOverride这几个方法,在一个平稳运行的生产环境,这几个方法都不会经常被调用(相信生产环境不会经常有实例发生状态改变,也不会经常有实例上下线),换句话说,这个队列里面基本上会一直保持空的状态。
回到最初的问题,为什么Eureka Client默认设置的是开启增量获取。
正是因为,正常运行的环境,各个实例的状态不会经常发生改变,Client不需要经常去做遍历覆盖等操作,client端保存的实例信息就是最新的,也是没有改变过的。client默认每30s向Eureka Server 发送一次获取实例信息请求,这30s内发生实例状态改变的概率还是非常小的。即便服务实例的数量很大,那么也不会每30s就会发生一次实例状态改变。
当然,如果服务实例的数量很大(1000个实例),如果关闭了增量获取,那么Client每30秒就要从Server端获取包含这1000个实例的response,也是一件很消耗流量的事情。
另外,这个recentlyChangedQueue队列会定时更新,有个定时任务会默认每30s执行一次清除操作,会清除30s(默认)前加进来的实例信息,也就是说Client从这个队列里面拿到的都是最多30s*2内发生改变的实例信息。
private TimerTask getDeltaRetentionTask() {
return new TimerTask() {
@Override
public void run() {
Iterator<RecentlyChangedItem> it = recentlyChangedQueue.iterator();
while (it.hasNext()) {
//默认只保留30s内加入到这个队列面的item,超过30秒的,全部移除
if (it.next().getLastUpdateTime() <
System.currentTimeMillis() - serverConfig.getRetentionTimeInMSInDeltaQueue()) {
it.remove();
} else {
break;
}
}
}
};
}
这里只讲了一个大概的流程,有些地方没有很详细的讲,有兴趣的同学可以对照最上面的流程图去看源码,也可以对照着去debug。如果有任何疑问或者讲的不够准确的地方,感谢提出!