三、Eureka源码解析:客户端服务列表的获取与更新

1、Eureka client从注册中心更新服务列表,然后自身会做缓存;

2、作为服务消费者,就是从这些缓存信息中获取的服务提供者的信息;

3、增量更新的服务以30秒为周期循环调用;

4、增量更新数据在服务端保存时间为3分钟,因此Eureka client取得的数据虽然被称为"增量更新",仍然可能和30秒前取的数据一样,所以Eureka client要自己来处理重复信息;

5、由3、4两点可以推断出,Eureka client的增量更新,其实获取的是Eureka server最近三分钟内的变更,因此,如果Eureka client有超过三分钟没有做增量更新的话(例如网络问题),那么再调用增量更新接口时,那三分钟内Eureka server的变更就可能获取不到了,这就造成了Eureka server和Eureka client之间的数据不一致,需要有个方案来及时发现这个问题;

6、正常情况下,Eureka client多次增量更新后,最终的服务列表数据应该Eureka server保持一致,但如果期间发生异常,可能导致和Eureka server的数据不一致,为了暴露这个问题,Eureka server每次返回的增量更新数据中,会带有一致性哈希码,Eureka client用本地服务列表数据算出的一致性哈希码应该和Eureka server返回的一致,若不一致就证明增量更新出了问题导致Eureka client和Eureka server上的服务列表信息不一致了,此时需要全量更新;

7、Eureka server上的服务列表信息对外提供JSON/XML两种格式下载;

8、Eureka client使用jersey的SDK,去下载JSON格式的服务列表信息;

服务列表获取更新:

三、Eureka源码解析:客户端服务列表的获取与更新_第1张图片

注释消息: 这可以确保对要获取的远程区域进行动态更改。

三、Eureka源码解析:客户端服务列表的获取与更新_第2张图片 注释消息: 这两块区域都需要进行同步

注释消息:  只刷新映射以反映任何DNS/属性更改

 三、Eureka源码解析:客户端服务列表的获取与更新_第3张图片

 

更新服务列表,根据入参判断进行全量更新还是增量更新

private boolean fetchRegistry(boolean forceFullRegistryFetch) {

        //用Stopwatch做耗时分析

        Stopwatch tracer = FETCH_REGISTRY_TIMER.start();

 

        try {

            // 取出本地缓存的,之气获取的服务列表信息

            Applications applications = getApplications();

 

            //判断多个条件,确定是否触发全量更新,如下任一个满足都会全量更新:

            //1. 是否禁用增量更新;

            //2. 是否对某个region特别关注;

            //3. 外部调用时是否通过入参指定全量更新;

            //4. 本地还未缓存有效的服务列表信息;

            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);

            }

            //重新计算和设置一致性hash码

            applications.setAppsHashCode(applications.getReconcileHashCode());

            //日志打印所有应用的所有实例数之和

            logTotalInstances();

        } catch (Throwable e) {

            logger.error(PREFIX + appPathIdentifier + " - was unable to refresh its cache! status = " + e.getMessage(), e);

            return false;

        } finally {

            if (tracer != null) {

                tracer.stop();

            }

        }

 

        //将本地缓存更新的事件广播给所有已注册的监听器,注意该方法已被CloudEurekaClient类重写

        onCacheRefreshed();

 

        //检查刚刚更新的缓存中,有来自Eureka server的服务列表,其中包含了当前应用的状态,

        //当前实例的成员变量lastRemoteInstanceStatus,记录的是最后一次更新的当前应用状态,

        //上述两种状态在updateInstanceRemoteStatus方法中作比较 ,如果不一致,就更新lastRemoteInstanceStatus,并且广播对应的事件

        updateInstanceRemoteStatus();

 

        return true;

    }

全量更新:

private void getAndStoreFullRegistry() throws Throwable {

        long currentUpdateGeneration = fetchRegistryGeneration.get();

 

        logger.info("Getting all instance registry info from the eureka server");

 

        Applications apps = null;

        //由于并没有配置特别关注的region信息,因此会调用eurekaTransport.queryClient.getApplications方法从服务端获取服务列表

        EurekaHttpResponse 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();

        }

        logger.info("The response status is {}", httpResponse.getStatusCode());

 

        if (apps == null) {

            logger.error("The application is null for some reason. Not storing this information");

        }

    //考虑到多线程同步,只有CAS成功的线程,才会把自己从Eureka server获取的数据来替换本地缓存

        else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {

            //localRegionApps就是本地缓存,是个AtomicReference实例,过滤只保留状态为开启( UP )的应用实例,并随机打乱应用实例顺序。打乱后,实现调用应用服务的随机性。

            localRegionApps.set(this.filterAndShuffle(apps));

            logger.debug("Got full registry with apps hashcode {}", apps.getAppsHashCode());

        } else {

            logger.warn("Not updating applications as another thread is updating it already");

        }

    }

@Override  EurekaHttpClientDecorator

    public EurekaHttpResponse getApplications(final String... regions) {

        return execute(new RequestExecutor() {

            @Override

            public EurekaHttpResponse execute(EurekaHttpClient delegate) {

                return delegate.getApplications(regions);

            }

 

            @Override

            public RequestType getRequestType() {

                //本次向Eureka server请求的类型:获取服务列表

                return RequestType.GetApplications;

            }

        });

    }

        再继续追踪 delegate.register(info),进入了AbstractJerseyEurekaHttpClient类,这里面是各种网络请求的具体实现,EurekaHttpClientDecorator类中的getApplications、register、sendHeartBeat等方法对应的网络请求响应逻辑在AbstractJerseyEurekaHttpClient中都有具体实现,篇幅所限我们只关注getApplications:

@Override

public EurekaHttpResponse getApplications(String... regions) {

    //取全量数据的path是""apps"

    return getApplicationsInternal("apps/", regions);

}

 

@Override

public EurekaHttpResponse getDelta(String... regions) {

    //取增量数据的path是""apps/delta"

    return getApplicationsInternal("apps/delta", regions);

}

//具体的请求响应处理都在此方法中

private EurekaHttpResponse getApplicationsInternal(String urlPath, String[] regions) {

        ClientResponse response = null;

        String regionsParamValue = null;

        try {

            //jersey、resource这些关键词都预示着这是个restful请求    

            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);

            //发起网络请求,将响应封装成ClientResponse实例

            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 {

            if (logger.isDebugEnabled()) {

                logger.debug("Jersey HTTP GET {}/{}?{}; statusCode={}",

                        serviceUrl, urlPath,

                        regionsParamValue == null ? "" : "regions=" + regionsParamValue,

                        response == null ? "N/A" : response.getStatus()

                );

            }

            if (response != null) {

                response.close();

            }

        }

    }

 

获取服务列表信息的增量更新

private void getAndUpdateDelta(Applications applications) throws Throwable {

        long currentUpdateGeneration = fetchRegistryGeneration.get();

 

        Applications delta = null;

        //增量信息是通过eurekaTransport.queryClient.getDelta方法完成的

        EurekaHttpResponse httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());

        if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {

            //delta中保存了Eureka server返回的增量更新

            delta = httpResponse.getEntity();

        }

        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();

        }

        //考虑到多线程同步问题,这里通过CAS来确保请求发起到现在是线程安全的,

        //如果这期间fetchRegistryGeneration变了,就表示其他线程也做了类似操作,因此放弃本次响应的数据

        else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {

            logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());

            String reconcileHashCode = "";

            if (fetchRegistryUpdateLock.tryLock()) {

                try {

                    //用Eureka返回的增量数据和本地数据做合并操作,这个方法稍后会细说

                    updateDelta(delta);

                    //用合并了增量数据之后的本地数据来生成一致性哈希码

                    reconcileHashCode = getReconcileHashCode(applications);

                } finally {

                    fetchRegistryUpdateLock.unlock();

                }

            } else {

                logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");

            }

            //Eureka server在返回增量更新数据时,将增量的数据合并到本地数据,之后进行一致性Hash,也会返回服务端的一致性哈希码,

            //理论上每次本地缓存数据经历了多次增量更新后,计算出的一致性哈希码应该是和服务端一致的,

            //如果发现不一致,就证明本地缓存的服务列表信息和Eureka server不一致了,需要做一次全量更新

            if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {

                //一致性哈希码不同,就在reconcileAndLogDifference方法中做全量更新

                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());

        }

    }

上述代码中有几处需要注意:

a. 获取增量更新数据使用的方法是:eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());

b. 将增量更新的数据和本地缓存合并的方法是: updateDelta(delta);

c. 通过检查一致性哈希码可以确定历经每一次增量更新后,本地的服务列表信息和Eureka server上的是否还保持一致,若不一致就要做一次全量更新,通过调用reconcileAndLogDifference方法来完成;

上述a、b、c三点,接下来依次展开:

 

向Eureka server发起网络请求的逻辑和前面全量更新的差不多,也是EurekaHttpClientDecorator和AbstractJerseyEurekaHttpClient这两个类合作实现的,先看EurekaHttpClientDecorator部分:

@Override

    public EurekaHttpResponse getDelta(final String... regions) {

        return execute(new RequestExecutor() {

            @Override

            public EurekaHttpResponse execute(EurekaHttpClient delegate) {

                return delegate.getDelta(regions);

            }

 

            @Override

            public RequestType getRequestType() {

                return RequestType.GetDelta;

            }

        });

    }

2、再看AbstractJerseyEurekaHttpClient类中的getDelta方法,居然和全量获取服务列表数据调用了相同的方法getApplicationsInternal,只是ur参数不一样而已;

@Override

    public EurekaHttpResponse getDelta(String... regions) {

        return getApplicationsInternal("apps/delta", regions);

    }

由上述代码可见,从Eureka server的获取增量更新,和一些常见的方式略有区别:

a. 一般的增量更新是在请求中增加一个时间戳或者上次更新的tag号等参数,由服务端根据参数来判断哪些数据是客户端没有的;

b. 而这里的Eureka client却没有这类参数,联想到前面官方文档中提到的“Eureka会把更新数据保留三分钟”,就可以理解了:Eureka把最近的变更数据保留三分钟,这三分钟内每个Eureka client来请求增量更新是,server都返回同样的缓存数据,只要client能保证三分钟之内有一次请求,就能保证自己的数据和Eureka server端的保持一致;

c. 那么如果client有问题,导致超过三分钟才来获取增量更新数据,那就有可能client和server数据不一致了,此时就要有一种方式来判断是否不一致,如果不一致,client就会做一次全量更新,这种判断就是一致性哈希码;

 

3、Eureka client获取到增量更新后,通过updateDelta方法将增量更新数据和本地数据做合并:

private void updateDelta(Applications delta) {

        int deltaCount = 0;

        //遍历所有服务

        for (Application app : delta.getRegisteredApplications()) {

            //遍历当前服务的所有实例    

            for (InstanceInfo instance : app.getInstances()) {

                //取出缓存的所有服务列表,用于合并

                Applications applications = getApplications();

                String instanceRegion = instanceRegionChecker.getInstanceRegion(instance);

                //判断正在处理的实例和当前应用是否在同一个region

                if (!instanceRegionChecker.isLocalRegion(instanceRegion)) {

                    //如果不是同一个region,接下来合并的数据就换成专门为其他region准备的缓存

                    Applications remoteApps = remoteRegionVsApps.get(instanceRegion);

                    if (null == remoteApps) {

                        remoteApps = new Applications();

                        remoteRegionVsApps.put(instanceRegion, remoteApps);

                    }

                    applications = remoteApps;

                }

 

                ++deltaCount;

                

                if (ActionType.ADDED.equals(instance.getActionType())) {  //对新增的实例的处理

                    Application existingApp = applications.getRegisteredApplications(instance.getAppName());

                    if (existingApp == null) {

                        applications.addApplication(app);

                    }

                    logger.debug("Added instance {} to the existing apps in region {}", instance.getId(), instanceRegion);

                    applications.getRegisteredApplications(instance.getAppName()).addInstance(instance);

                } else if (ActionType.MODIFIED.equals(instance.getActionType())) {  //对修改实例的处理

                    Application existingApp = applications.getRegisteredApplications(instance.getAppName());

                    if (existingApp == null) {

                        applications.addApplication(app);

                    }

                    logger.debug("Modified instance {} to the existing apps ", instance.getId());

 

                    applications.getRegisteredApplications(instance.getAppName()).addInstance(instance);

 

                } else if (ActionType.DELETED.equals(instance.getActionType())) { //对删除实例的处理

                    Application existingApp = applications.getRegisteredApplications(instance.getAppName());

                    if (existingApp == null) {

                        applications.addApplication(app);

                    }

                    logger.debug("Deleted instance {} to the existing apps ", instance.getId());

                    applications.getRegisteredApplications(instance.getAppName()).removeInstance(instance);

                }

            }

        }

        logger.debug("The total number of instances fetched by the delta processor : {}", deltaCount);

 

        getApplications().setVersion(delta.getVersion());

        //整理数据,使得后续使用过程中,这些应用的实例总是以相同顺序返回

        getApplications().shuffleInstances(clientConfig.shouldFilterOnlyUpInstances());

    

    //和当前应用不在同一个region的应用,其实例数据也要整理

        for (Applications applications : remoteRegionVsApps.values()) {

            applications.setVersion(delta.getVersion());

            applications.shuffleInstances(clientConfig.shouldFilterOnlyUpInstances());

        }

    }

 

服务端:

GET获取缓存(获取readOnlyCacheMap(数据列表

如果readOnlyCacheMap不存在则从readWriteCacheMap获取。

@VisibleForTesting

String get(final Key key, boolean useReadOnlyCache) {

    // 主要看这个getValue

   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) {

            // 从只读缓存里面获取数据

           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);

       }

   } catch (Throwable t) {

       logger.error("Cannot get value for key :" + key, t);

   }

   return payload;

}

@VisibleForTesting

String get(final Key key, boolean useReadOnlyCache) {

    // 主要看这个getValue

   Value payload = getValue(key, useReadOnlyCache);

   if (payload == null || payload.getPayload().equals(EMPTY_PAYLOAD)) {

       return null;

   } else {

       return payload.getPayload();

   }

}

useReadOnlyCache : shouldUseReadOnlyResponseCache ,可以配置是否使用只读缓存,默认是true

 

readWriteCacheMap.get(key) : 这个使用的是gauva 的缓存机制,如果当前的缓存里面这个key没有,那么

 

会直接调用CacheLoader.load()方法,从最上面的代码可以看到, load方法,主要是执行了generatePayload()

的方法。

generatePayload

 

根据不同的Application来判断是全量更新还是增量更新。

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();

                        //调用registry.getApplications() 获取应用信息。同时调用getPayLoad进行编码

                       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();

                        // 调用registry.getApplicationDeltas() 获取增量注册信息

                       payload = getPayLoad(key, registry.getApplicationDeltas());

                   }

               } else {

                    // 根据key直接获取注册信息

                   tracer = serializeOneApptimer.start();

                   payload = getPayLoad(key, registry.getApplication(key.getName()));

               }

               break;

            // 根据VIP获取

           case VIP:

           case SVIP:

               tracer = serializeViptimer.start();

               payload = getPayLoad(key, getApplicationsForVip(key, registry));

               break;

           default:

               logger.error("Unidentified entity type: " + key.getEntityType() + " found in the cache key.");

               payload = "";

               break;

       }

       return new Value(payload);

   } finally {

       if (tracer != null) {

           tracer.stop();

       }

   }

}

// 编码类

private String getPayLoad(Key key, Applications apps) {

   EncoderWrapper encoderWrapper = serverCodecs.getEncoder(key.getType(), key.getEurekaAccept());

   String result;

   try {

       result = encoderWrapper.encode(apps);

   } catch (Exception e) {

       logger.error("Failed to encode the payload for all apps", e);

       return "";

   }

   if(logger.isDebugEnabled()) {

       logger.debug("New application cache entry {} with apps hashcode {}", key.toStringCompact(), apps.getAppsHashCode());

   }

   return result;

}

entityType : 分为三种,Application, VIP, SVIP , 客户端获取注册信息的话,传入的主要是Application类型的,另外两种类型此处不做考虑 。

根据KEY_NAME 的不同,判断是全量获取信息,还是增量获取信息。

 

Eureka Client缓存机制

Eureka Client缓存机制很简单,设置了一个每30秒执行一次的定时任务,定时去服务端获取注册信息。获取之后,存入本地内存。

你可能感兴趣的:(Eureka源码解析,SpringCloud,Eureka)