Eureka源码学习之拉取注册表

1,566 阅读5分钟

之前看了服务注册,注册完就来看看拉取注册表的逻辑吧

在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增量拉取注册表),这个在实际生产项目中,我们都会调整参数进行优化