之前看了服务注册,注册完就来看看拉取注册表的逻辑吧
在eureka client启动的时候会调用fetchRegistry()这个方法
private 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();
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);
}
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;
}
getAndStoreFullRegistry():获取全量注册表
getAndUpdateDelta():获取增量注册表
eureka client启动的时候,会先从本地Applications缓存获取,获取不到会拉取全量注册表,会发送http get请求(例如:http://localhost:8080/v2/apps)从eureka server获取全量注册表,然后缓存在本地。
Applications apps = null;
EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
: eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
apps = httpResponse.getEntity();
}
下面来看看eureka server端来如何处理抓取全量注册表的请求的 http://localhost:8080/v2/apps 请求调用的是eureka-core中的ApplicationsResource的getContainers()方法,获取全量注册表的方法
Key cacheKey = new Key(Key.EntityType.Application,
ResponseCacheImpl.ALL_APPS,
keyType, CurrentRequestVersion.get(), EurekaAccept.fromString(eurekaAccept), regions
);
Response response;
if (acceptEncoding != null && acceptEncoding.contains(HEADER_GZIP_VALUE)) {
response = Response.ok(responseCache.getGZIP(cacheKey))
.header(HEADER_CONTENT_ENCODING, HEADER_GZIP_VALUE)
.header(HEADER_CONTENT_TYPE, returnMediaType)
.build();
} else {
response = Response.ok(responseCache.get(cacheKey))
.build();
}
eureka server端缓存机制是两层缓存,先从readOnlyCacheMap只读缓存中获取,获取不到再从readWriteCacheMap读写缓存中获取并写回只读缓存中,如果读写缓存也没有的话,会从eureka server的注册表中去读取
if (useReadOnlyCache) {
final Value currentPayload = readOnlyCacheMap.get(key);
if (currentPayload != null) {
payload = currentPayload;
} else {
payload = readWriteCacheMap.get(key);
readOnlyCacheMap.put(key, payload);
}
} else {
payload = readWriteCacheMap.get(key);
}
既然eureka server端用了缓存,那么缓存过期策略又是怎么样的?
1.主动过期 服务实例注册,下线,故障的时候会调用ResponseCacheImpl.invalidate(),会将readWriteMap中ALL_APPS这个key对应的缓存,给他过期掉
2.定时过期 readWriteMap在构建的以后会设一个过期时间serverConfig.getResponseCacheAutoExpirationInSeconds(),默认值180s
3.被动过期 clientConfig.getRegistryFetchIntervalSeconds()默认值30s,有个调度任务每隔30s会对readOnlyCacheMap和readWriteCacheMap中的数据进行一个比对,如果两块数据是不一致的,就将readWriteCacheMap中的数据放到readOnlyCacheMap中来。
下面来看看增量拉取注册表getAndUpdateDelta():
会走EurekaHttpClient的getDelta()方法和接口,get请求http://localhost:8080/v2/apps/delta
下面可以看到cacheKey是ALL_APPS_DELTA,之前全量的cacheKey是ALL_APPS
Key cacheKey = new Key(Key.EntityType.Application,
ResponseCacheImpl.ALL_APPS_DELTA,
keyType, CurrentRequestVersion.get(), EurekaAccept.fromString(eurekaAccept), regions
);
final Response response;
if (acceptEncoding != null && acceptEncoding.contains(HEADER_GZIP_VALUE)) {
response = Response.ok(responseCache.getGZIP(cacheKey))
.header(HEADER_CONTENT_ENCODING, HEADER_GZIP_VALUE)
.header(HEADER_CONTENT_TYPE, returnMediaType)
.build();
} else {
response = Response.ok(responseCache.get(cacheKey)).build();
}
两者的区别在于readWriteMap从注册表拉数据的时候,增量用的是registry.getApplicationDeltasFromMultipleRegions()
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());
}
}
再来看看registry.getApplicationDeltasFromMultipleRegions()
private ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue = new ConcurrentLinkedQueue<RecentlyChangedItem>();
//定时任务,相当于recentlyChangedQueue中只存放最近3分钟的leaseInfo
private TimerTask getDeltaRetentionTask() {
return new TimerTask() {
@Override
public void run() {
Iterator<RecentlyChangedItem> it = recentlyChangedQueue.iterator();
while (it.hasNext()) {
if (it.next().getLastUpdateTime() <
System.currentTimeMillis() - serverConfig.getRetentionTimeInMSInDeltaQueue()) {
it.remove();
} else {
break;
}
}
}
};
}
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)));
}
维护了一个保存最近3分钟服务实例变更记录的一个队列,eureka client每次30秒,去抓取注册表的时候,就会返回最近3分钟内发生过变化的服务实例
eureka client拿到eureka server返回的增量数据后,如果为空就拉取全量注册表,不为空就和本地注册表对比合并,合并完会计算一个hash值,与eureka server返回的全量注册表的hash值进行对比,如果不一样说明本地注册表和server端的注册表不一致就会再全量拉取
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()) {
reconcileAndLogDifference(delta, reconcileHashCode); // this makes a remoteCall
}
}
总结:
1.拉取注册表逻辑分为全量拉取和增量拉取
2.eureka server的缓存机制 readOnlyCacheMap,readWriteCacheMap 先从readOnlyCacheMap中获取,获取不到再从readWriteCacheMap中获取,获取不到最后从注册表获取
3.缓存过期策略 readOnlyCacheMap:被动过期,线程任务每隔30s对比readWriteCacheMap和readOnlyCacheMap是否一致,不一致的话就将readWriteCacheMap中的数据放到readOnlyCacheMap中来。readWriteCacheMap:主动过期,服务实例有注册、下线、故障都会将readWriteMap中ALL_APPS这个key对应的缓存,给他过期掉;定时过期:每个key创建的时候都会设哥过期时间180s
4.增量拉取 server端会维护一个队列,队列里存放着最近3分钟实例变更记录信息,client来拉取增量数据时,server会将队列里的数据和类似全量注册表的hash值返回给client。client拿到返回结果,若为空则全量拉取注册表,不为空就与本地注册表对比合并,然后计算hash值并与返回的全量hash值对比,如果不一致则全量拉取注册表
个人感悟: 两层缓存机制可以提高并发读写效率,增量数据通过linkQueue维护,再加一个线程保证只保存最近3分钟的变更数据,这个架构设计很值得学习,还有一个就是通过hash对比来判断数据是否一致,在分布式系统中比较数据一致中比较实用。但是由于缓存机制的存在,服务实例注册、下线、故障,client感知到需要很长时间(30s readOnlyCacheMap更新,30s client增量拉取注册表),这个在实际生产项目中,我们都会调整参数进行优化