Eureka系列(三)获取服务Client端具体实现

获取服务Client 端流程

  我们先看下面这张图片,这张图片简单描述了下我们Client是如何获取到Server已续约实例信息的流程:Eureka获取实例信息Client端实现.jpg  从图片中我们可以知晓大致流程就是Client会自己开启一个定时任务,然后根据不同的条件去调用Server端接口得到所有已续约服务的信息,然后合并到自己的缓存数据中。下面我们详情了解下上述流程在源码中的具体实现。


获取服务Client端源码分析

  我们先来看看服务获取定时任务的初始化。那我们的服务获取定时任务什么时候会被初始化呢,那肯定是我们启用我们Eureka Client的时候呗,当我们启动Client时,Eureka会先处理相关的配置,然后初始化我们Client的相关信息,我们的定时任务也就是此时进行的初始化,更具体地说我们的服务续约定时任务就是在DiscoveryClient这个类中initScheduledTasks方法中被初始化的。具体代码如下:

private final ScheduledExecutorService scheduler;
private void initScheduledTasks() {
    …省略其他代码
    // 初始化定时拉取服务注册信息
    scheduler.schedule(
        new TimedSupervisorTask(
                "cacheRefresh",
                scheduler,
                cacheRefreshExecutor,
                registryFetchIntervalSeconds,
                TimeUnit.SECONDS,
                expBackOffBound,
                new CacheRefreshThread()
        ),
        registryFetchIntervalSeconds, TimeUnit.SECONDS);
     …省略其他代码
}

  由此可见,我们的定时任务其实是Client进行初始化完成的,并且还是使用ScheduledExecutorService线程池来完成我们的定时任务。我们下面就看看CacheRefreshThread这个类是如何实现获取服务的流程:

class CacheRefreshThread implements Runnable {
        public void run() {
            refreshRegistry();
        }
    }

  不多说,我们接着看refreshRegistry()方法:

 @VisibleForTesting
    void refreshRegistry() {
            …省略部分代码
            boolean success = fetchRegistry(remoteRegionsModified); //  获取实例信息
            if (success) {
                registrySize = localRegionApps.get().size();
                lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();
            }
            …省略部分代码
    }

  这里不做太多解释,我们接着看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();

        //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); // 增量更新
        }
        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;
}

  由此可见,fetchRegistry 方法主要是判断我们获取实例信息是进行全量更新还是增量更新。如果条件成立,则我们会进行全量更新,否则则进行批量更新。下面我们简单介绍下getAndStoreFullRegistry() 全量更新、getAndUpdateDelta(applications)批量更新方法:

// 全量更新
private void getAndStoreFullRegistry() throws Throwable {
    long currentUpdateGeneration = fetchRegistryGeneration.get();
    logger.info("Getting all instance registry info from the 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()) {
        // 将实例信息存进Applications
        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");
    } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
        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");
    }
}

   getAndStoreFullRegistry() 简单来说就是通过调用Eureka服务端提供的http接口获取到全部实例信息,然后将实例信息存进我们的Applications中。

// 批量更新
private void getAndUpdateDelta(Applications applications) throws Throwable {
    long currentUpdateGeneration = fetchRegistryGeneration.get();

    Applications delta = null;
     // 得到增量的实例信息
    EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
    if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
        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 {
                // 合并增量实例信息
                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
         //Eureka server在返回增量更新数据时,也会返回服务端的一致性哈希码,
         //理论上每次本地缓存数据经历了多次增量更新后,计算出的一致性哈希码应该是和服务端一致的,
         //如果发现不一致,就证明本地缓存的服务列表信息和Eureka server不一致了,需要做一次全量更新
        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());
    }
}

  getAndUpdateDelta 方法简单来说,就是首先调用Eureka服务端提供的增量信息接口得到增量实例信息,然后进行判断,如果增量为null,为了数据准确性,调用一下全量更新实例接口更新实例信息。如果增量信息不为空,则进行一个cas处理,如果成功,则进行增量信息的合并。最后会再进行一次判断,判断从服务端得到的批量实例信息计算得到的HashCode是否和从服务端得到的实例信息HashCode值是否相等,如果不相等则会调用reconcileAndLogDifference 方法,再次进行全量更新实例信息。下面我们就简单看下reconcileAndLogDifference这个方法:

private void reconcileAndLogDifference(Applications delta, String reconcileHashCode) throws Throwable {
    logger.debug("The Reconcile hashcodes do not match, client : {}, server : {}. Getting the full registry",
            reconcileHashCode, delta.getAppsHashCode());
    RECONCILE_HASH_CODES_MISMATCH.increment();
    long currentUpdateGeneration = fetchRegistryGeneration.get();
    EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
            // 调用服务端接口得到全量的实例信息
            ? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get()) 
            : eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
    Applications serverApps = httpResponse.getEntity();
    if (serverApps == null) {
        logger.warn("Cannot fetch full registry from the server; reconciliation failure");
        return;
    }
    if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
        localRegionApps.set(this.filterAndShuffle(serverApps));
        getApplications().setVersion(delta.getVersion());
        logger.debug(
                "The Reconcile hashcodes after complete sync up, client : {}, server : {}.",
                getApplications().getReconcileHashCode(),
                delta.getAppsHashCode());
    } else {
        logger.warn("Not setting the applications map as another thread has advanced the update generation");
    }
}

   由此可见,reconcileAndLogDifference方法和我们getAndStoreFullRegistry方法调用的接口一样,都是调用Eureka服务端提供的全量实例信息接口,然后更新我们自己的实例信息。

总的来说,获取服务Client端的执行流程就可以分为以下两步:
   1.初始化一个定时任务,默认30s
   2.定时任务中根据不同的情况调用不同的方法来更新本地实例缓存信息

在定时任务中获取实例信息我们也可以分为以下几步:
   1.判断是否需要全量更新
   2.条件成立则进行全量更新
     a.将全量更新数据更新到本地缓存中
  3.条件不成立则进行批量更新
     a.判断批量更新数据是否为空,是则进行一次全量更新
     b.将批量更新数据合并到本地缓存中
     c.判断批量更新数据计算得到的HashCode是否和服务端传过来的HashCode相等,如果不相等,说明数据有问题,需要进行一次全量更新

原文地址:https://www.cnblogs.com/liujunj/p/13401805.html