Eureka源码分析之Eureka Client获取实例信息流程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_36960211/article/details/85226088

下方是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,代表着增量。

注意下面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。如果有任何疑问或者讲的不够准确的地方,感谢提出!

 

 

 

 

猜你喜欢

转载自blog.csdn.net/qq_36960211/article/details/85226088