(四)EurekaClient-启动时初始化的定时任务

0. 前言

  • springboot版本:2.1.9.RELEASE
  • springcloud版本:Greenwich.SR4

1. 初始化定时任务

客户端启动时,会初始化定时任务,方法入口在《EurekaClient-拉取注册表》提到过

// DiscoveryClient.class
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
    // ......
    try {
        // default size of 2 - 1 each for heartbeat and cacheRefresh
        // 初始化任务线程池,大小为2
        scheduler = Executors.newScheduledThreadPool(2,
                new ThreadFactoryBuilder()
                        .setNameFormat("DiscoveryClient-%d")
                        .setDaemon(true)
                        .build());
        // 初始化心跳续租定时器
        heartbeatExecutor = new ThreadPoolExecutor(
                1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(),
                new ThreadFactoryBuilder()
                        .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
                        .setDaemon(true)
                        .build()
        );  // use direct handoff
	// 初始化刷新缓存定时器
        cacheRefreshExecutor = new ThreadPoolExecutor(
                1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(),
                new ThreadFactoryBuilder()
                        .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                        .setDaemon(true)
                        .build()
        );  // use direct handoff
        // ......
    } catch (Throwable e) {
        throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
    }
    // ......
    // 2 初始化定时任务
    initScheduledTasks();
    // ......
}

2. initScheduledTasks()

// DiscoveryClient.class
private void initScheduledTasks() {
    if (clientConfig.shouldFetchRegistry()) {
        // registry cache refresh timer
        int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
        int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
        // 开启定时刷新注册表任务,默认每30s执行一次
        // 任务执行时间间隔不一定都是30s
        // 当某次执行在指定的时间间隔内没完成导致超时,那么下一次时间间隔是上一次2倍
        // 以此类推,但是最大时间间隔不超过 registryFetchIntervalSeconds * expBackOffBound ,默认30*10
        // 2.1 TimedSupervisorTask 类中实现了上述任务执行机制
        scheduler.schedule(
                new TimedSupervisorTask(
                        "cacheRefresh",
                        scheduler,
                        cacheRefreshExecutor,
                        registryFetchIntervalSeconds,
                        TimeUnit.SECONDS,
                        expBackOffBound,
                        // 3 刷新注册表线程
                        new CacheRefreshThread()
                ),
                registryFetchIntervalSeconds, TimeUnit.SECONDS);
    }

    if (clientConfig.shouldRegisterWithEureka()) {
        int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

        // Heartbeat timer
        // 开启定时心跳续租任务,默认每30s执行一次
        // 任务执行机制和定时刷新注册表任务类似
        scheduler.schedule(
                new TimedSupervisorTask(
                        "heartbeat",
                        scheduler,
                        heartbeatExecutor,
                        renewalIntervalInSecs,
                        TimeUnit.SECONDS,
                        expBackOffBound,
                        // 4 心跳续租线程
                        new HeartbeatThread()
                ),
                renewalIntervalInSecs, TimeUnit.SECONDS);

        // InstanceInfo replicator
        // 5 定时检测客户端任务
        instanceInfoReplicator = new InstanceInfoReplicator(
                this,
                instanceInfo,
                clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                2); // burstSize

        // 状态变更监听器
        statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
            @Override
            public String getId() {
                return "statusChangeListener";
            }

            @Override
            public void notify(StatusChangeEvent statusChangeEvent) {
                if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                        InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                    // log at warn level if DOWN was involved
                    logger.warn("Saw local status change event {}", statusChangeEvent);
                } else {
                    logger.info("Saw local status change event {}", statusChangeEvent);
                }
                // 发布状态变更事件时,触发 onDemandUpdate() 方法,下面分析
                instanceInfoReplicator.onDemandUpdate();
            }
        };

        if (clientConfig.shouldOnDemandUpdateStatusChange()) {
            applicationInfoManager.registerStatusChangeListener(statusChangeListener);
        }

        // 启动定时检测客户端任务
        instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        logger.info("Not registering with Eureka server per configuration");
    }
}
2.1 TimedSupervisorTask.class

TimedSupervisorTask 执行任务机制,可以根据具体情况灵活控制下次任务执行的时间

public class TimedSupervisorTask extends TimerTask {
    // ......
    @Override
    public void run() {
        Future<?> future = null;
        try {
            // 使用 Future ,提交一个子线程任务
            future = executor.submit(task);
            threadPoolLevelGauge.set((long) executor.getActiveCount());
            // future.get 是阻塞方法,只有子线程任务完成时才返回结果
            // 这里调用该方法并指定等待时间 timeoutMillis ,默认30s
            // 表示最多等待30s,超过30s抛出 TimeoutException
            future.get(timeoutMillis, TimeUnit.MILLISECONDS);  // block until done or timeout
            // 执行到这说明子线程任务未超时执行完成
            // 每次任务未超时执行完成都会重置 delay
            delay.set(timeoutMillis);
            threadPoolLevelGauge.set((long) executor.getActiveCount());
            successCounter.increment();
        } catch (TimeoutException e) {
            // 执行到这说明子线程任务超时未完成
            logger.warn("task supervisor timed out", e);
            timeoutCounter.increment();

            // 获取当前任务时间间隔
            long currentDelay = delay.get();
            // 计算下一次任务时间间隔,当前任务时间间隔*2,最大不超过 maxDelay(默认30*10)
            long newDelay = Math.min(maxDelay, currentDelay * 2);
            // 设置新的任务时间间隔
            delay.compareAndSet(currentDelay, newDelay);

        } catch (RejectedExecutionException e) {
            if (executor.isShutdown() || scheduler.isShutdown()) {
                logger.warn("task supervisor shutting down, reject the task", e);
            } else {
                logger.warn("task supervisor rejected the task", e);
            }

            rejectedCounter.increment();
        } catch (Throwable e) {
            if (executor.isShutdown() || scheduler.isShutdown()) {
                logger.warn("task supervisor shutting down, can't accept the task");
            } else {
                logger.warn("task supervisor threw an exception", e);
            }

            throwableCounter.increment();
        } finally {
            if (future != null) {
                // 2.2 取消任务
                future.cancel(true);
            }

            if (!scheduler.isShutdown()) {
                // 只要执行器没有停止,再次执行一次任务
                scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
            }
        }
    }
}
2.2 future.cancel

当使用 Future 时,任务可能有以下三种状态:

  • 等待状态:此时调用不管传入 true 还是 false 都会标记为取消,任务依然保存在任务队列中,当轮到该任务运行时会直接跳过
  • 完成状态:此时调用不会起任何作用,因为任务已经完成了
  • 运行中状态:此时传入 true 会中断正在执行的任务,传入 false 则不会中断

3. 刷新注册表线程

// DiscoveryClient.class
class CacheRefreshThread implements Runnable {
    public void run() {
        // 刷新注册表
        refreshRegistry();
    }
}

@VisibleForTesting
void refreshRegistry() {
    try {
        boolean isFetchingRemoteRegionRegistries = isFetchingRemoteRegionRegistries();

        boolean remoteRegionsModified = false;
        // This makes sure that a dynamic change to remote regions to fetch is honored.
        String latestRemoteRegions = clientConfig.fetchRegistryForRemoteRegions();
        if (null != latestRemoteRegions) {
            String currentRemoteRegions = remoteRegionsToFetch.get();
            if (!latestRemoteRegions.equals(currentRemoteRegions)) {
                // Both remoteRegionsToFetch and AzToRegionMapper.regionsToFetch need to be in sync
                synchronized (instanceRegionChecker.getAzToRegionMapper()) {
                    if (remoteRegionsToFetch.compareAndSet(currentRemoteRegions, latestRemoteRegions)) {
                        String[] remoteRegions = latestRemoteRegions.split(",");
                        remoteRegionsRef.set(remoteRegions);
                        instanceRegionChecker.getAzToRegionMapper().setRegionsToFetch(remoteRegions);
                        // 如果本地的远程 region 信息和配置文件中最新的不一样,则需要拉取的注册表包含远程 region 的
                        remoteRegionsModified = true;
                    } else {
                        logger.info("Remote regions to fetch modified concurrently," +
                                " ignoring change from {} to {}", currentRemoteRegions, latestRemoteRegions);
                    }
                }
            } else {
                // Just refresh mapping to reflect any DNS/Property change
                instanceRegionChecker.getAzToRegionMapper().refreshMapping();
            }
        }

        // 拉取注册表
        boolean success = fetchRegistry(remoteRegionsModified);
        if (success) {
            registrySize = localRegionApps.get().size();
            lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();
        }

        // ......
    } catch (Throwable e) {
        logger.error("Cannot fetch registry from server", e);
    }
}
3.1 fetchRegistry()
// DiscoveryClient.class
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
        {
            // ......
            // 3.2 全量拉取注册表
            getAndStoreFullRegistry();
        } else {
            // 3.3 增量拉取注册表
            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;
}
3.2 全量拉取注册表
// DiscoveryClient.class
private void getAndStoreFullRegistry() throws Throwable {
    // ......
    Applications apps = null;
    // 发起 Jersey 全量拉取注册表请求
    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();
    }
    // ......
    if (apps == null) {
        // ......
    } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
        // 服务端返回注册表信息过滤乱序后放入本地 localRegionApps
        localRegionApps.set(this.filterAndShuffle(apps));
        // ......
    } else {
        // ......
    }
}
3.3 增量拉取注册表
// DiscoveryClient.class
private void getAndUpdateDelta(Applications applications) throws Throwable {
    long currentUpdateGeneration = fetchRegistryGeneration.get();

    Applications delta = null;
    // 发起 Jersey 增量拉取注册表请求
    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.");
        // 如果 delta 为 null ,则进行全量拉取注册表
        // 因为可能是服务端不允许客户端增量拉取注册表
        // 如果服务端允许,则不可能为 null ,可以是 size = 0
        getAndStoreFullRegistry();
    } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
        logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
        String reconcileHashCode = "";
        if (fetchRegistryUpdateLock.tryLock()) {
            try {
                // 3.4 服务端返回的注册表信息更新到本地
                updateDelta(delta);
                // 获取本地更新后的 applications 的 reconcileHashCode
                // 用于判断数据是否丢失
                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
        // 比较本地更新后的注册表 reconcileHashCode 和服务端返回的 appsHashCode
        // 可能存在不一致,说明从服务端增量拉取过来的数据出现丢失
        //
        // 首先,增量拉取注册表的原理是从服务端的 recentlyChangedQueue 最近变更队列中,获取存在变更过的所有实例信息
        // recentlyChangedQueue 是先进先出队列,该队列在服务端有个定时任务,默认每30s执行一次
        // 定时任务处理逻辑是遍历队列中的实例变更信息,将存入 recentlyChangedQueue 后超过3分钟(默认)的移除队列
        // 假如在客户端没有拉取更新本地注册表的期间,recentlyChangedQueue 队列在这段期间内移除了没有被客户端的增量拉取的实例变更信息
        // 那么就会出现数据丢失的情况
        // 
        // 客户端确认数据丢失的方式就是比较 本地更新后注册表 和 服务端返回 的 hashCode
        // hashCode 一致说明数据未丢失,不一致说明数据丢失
        if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {
            // 3.5 数据丢失的情况需要再次全量拉取注册表
            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());
    }
}
3.4 updateDelta()

客户端本地不仅维护了本地的注册表(保存的和本地同样 region 的实例),还维护了其他远程 region 的注册表 remoteRegionVsApps

// DiscoveryClient.class
private void updateDelta(Applications delta) {
    int deltaCount = 0;
    // 遍历 delta 中的服务信息中的实例信息
    for (Application app : delta.getRegisteredApplications()) {
        for (InstanceInfo instance : app.getInstances()) {
            // 获取本地的注册表
            Applications applications = getApplications();
            // 获取当前处理实例所在 region
            String instanceRegion = instanceRegionChecker.getInstanceRegion(instance);
            if (!instanceRegionChecker.isLocalRegion(instanceRegion)) {
                // 如果不是当前客户端实例所在的 region 时
                // 则根据当前处理实例所在 region 从注册到本地的所有远程 region 注册表中获取 Applications(一个注册表)
                // 那么下面操作的就是其他远程 region 的注册表了
                Applications remoteApps = remoteRegionVsApps.get(instanceRegion);
                if (null == remoteApps) {
                    // 如果 remoteApps 为空,则新建一个放入 remoteRegionVsApps
                    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
                    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
                    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) {
                    logger.debug("Deleted instance {} to the existing apps ", instance.getId());
                    // 相应服务信息存在,则将服务信息中的实例信息删除
                    existingApp.removeInstance(instance);
                    /*
                     * We find all instance list from application(The status of instance status is not only the status is UP but also other status)
                     * if instance list is empty, we remove the application.
                     */
                    if (existingApp.getInstancesAsIsFromEureka().isEmpty()) {
                        // 如果相应服务信息删除实例信息后为空,则将服务信息也从 applications 中删除
                        applications.removeApplication(existingApp);
                    }
                }
            }
        }
    }
    logger.debug("The total number of instances fetched by the delta processor : {}", deltaCount);
   
    getApplications().setVersion(delta.getVersion());
    getApplications().shuffleInstances(clientConfig.shouldFilterOnlyUpInstances());

    for (Applications applications : remoteRegionVsApps.values()) {
        applications.setVersion(delta.getVersion());
        applications.shuffleInstances(clientConfig.shouldFilterOnlyUpInstances());
    }
}

小结:

  • 客户端本地不仅维护了本地的注册表(保存的是和本地同样 region 的实例),还维护了其他远程 region 的注册表 remoteRegionVsApps
  • 上面更新本地注册表逻辑时,遍历每个服务的每个实例时,一开始就区分实例所在分区,区分是更新本地的注册表还是其他远程 region 的注册表
3.5 reconcileAndLogDifference()
// DiscoveryClient.class
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();

    // 发起 Jersey 全量拉取注册表请求
    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");
    }
}

4. 心跳续租线程

// DiscoveryClient.class
private class HeartbeatThread implements Runnable {

    public void run() {
        // 客户端心跳续租
        if (renew()) {
            // 如果心跳续租成功,记录最后一次成功心跳续租的时间
            lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
        }
    }
}

boolean renew() {
    EurekaHttpResponse<InstanceInfo> httpResponse;
    try {
        // 发起 Jersey 心跳续租请求
        httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
        logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
        if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
            // 如果服务端返回404,则客户端再次发起注册
            // 这种情况原因是:
            //     1. 因为本地实例信息中的 lastDirtyTimestamp 大于 服务端注册表相应实例信息记录的 lastDirtyTimestamp,说明服务端中的是旧的
            //     2. 服务端注册表中没有客户端的实例信息
            REREGISTER_COUNTER.increment();
            logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());
            // 本地实例信息设置 lastDirtyTimestamp = 当前时间 和 isInstanceInfoDirty = true
            long timestamp = instanceInfo.setIsDirtyWithTime();
            // 向服务端发起注册
            boolean success = register();
            if (success) {
                // 4.1 注册成功后,更新本地 isInstanceInfoDirty
                instanceInfo.unsetIsDirty(timestamp);
            }
            return success;
        }
        return httpResponse.getStatusCode() == Status.OK.getStatusCode();
    } catch (Throwable e) {
        logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);
        return false;
    }
}
4.1 instanceInfo.unsetIsDirty()
// InstanceInfo.class
public synchronized void unsetIsDirty(long unsetDirtyTimestamp) {
    // 如果 lastDirtyTimestamp <= unsetDirtyTimestamp,才更改 isInstanceInfoDirty = false
    // 因为在客户端进行心跳续租,收到服务单返回404,再次发起注册,注册成功后的这整个过程中
    // 可能还会有其他线程操作对 lastDirtyTimestamp 进行更改
    // 如果 lastDirtyTimestamp > unsetDirtyTimestamp,则说明客户端的实例信息又一次和服务端的注册表中的不一致
    // 服务端的注册表中的客户端的实例信息还是脏的,说明这整个过程中同步到服务端的实例信息仍然不是最新的
    if (lastDirtyTimestamp <= unsetDirtyTimestamp) {
        isInstanceInfoDirty = false;
    } else {
    }
}

5. 定时检测客户端的任务

class InstanceInfoReplicator implements Runnable {
    // ......
    InstanceInfoReplicator(DiscoveryClient discoveryClient, InstanceInfo instanceInfo, int replicationIntervalSeconds, int burstSize) {
        this.discoveryClient = discoveryClient;
        this.instanceInfo = instanceInfo;
        // 初始化线程池,大小为1
        this.scheduler = Executors.newScheduledThreadPool(1,
                new ThreadFactoryBuilder()
                        .setNameFormat("DiscoveryClient-InstanceInfoReplicator-%d")
                        .setDaemon(true)
                        .build());

        this.scheduledPeriodicRef = new AtomicReference<Future>();

        this.started = new AtomicBoolean(false);
        this.rateLimiter = new RateLimiter(TimeUnit.MINUTES);
        this.replicationIntervalSeconds = replicationIntervalSeconds;
        this.burstSize = burstSize;

        // 默认 allowedRatePerMinute = 60 * 2 / 30 = 4,
        this.allowedRatePerMinute = 60 * this.burstSize / this.replicationIntervalSeconds;
        logger.info("InstanceInfoReplicator onDemand update allowed rate per min is {}", allowedRatePerMinute);
    }

    public void start(int initialDelayMs) {
        if (started.compareAndSet(false, true)) {
            // 当任务未启动时才启动
            // 启动检测客户端任务时,客户端可能还未注册到服务端(配置文件中没有配置强制启动时注册的情况),所以进行 dirty 标记
            instanceInfo.setIsDirty();  // for initial register
            // 开启 Future 线程,放入子线程任务,40s后执行
            Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
            // 预定下一次执行器需要执行的任务线程
            scheduledPeriodicRef.set(next);
        }
    }
    // ......
    public boolean onDemandUpdate() {
        // 令牌桶机制, RateLimiter(TimeUnit.MINUTES) , burstSize = 2 , allowedRatePerMinute = 4
        // 说明桶容量为2,每15s(每分钟4次)生成1个令牌,理想情况下1分钟最多获取4个令牌
        // 此处利用令牌桶机制,对本地按需执行的检测客户端任务进行限流
        if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
            if (!scheduler.isShutdown()) {
                // 任务执行器未停止,则提交一个新任务线程
                scheduler.submit(new Runnable() {
                    @Override
                    public void run() {
                        logger.debug("Executing on-demand update of local InstanceInfo");
                        // 获取最后一次任务执行的异步结果
                        Future latestPeriodic = scheduledPeriodicRef.get();
                        if (latestPeriodic != null && !latestPeriodic.isDone()) {
                            logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update");			 
                            // 如果最后一次任务未执行完成,则取消
                            // 入参 false 时,上面有分析
                            latestPeriodic.cancel(false);
                        }
                        // 重新启动一次新的检测客户端任务
                        InstanceInfoReplicator.this.run();
                    }
                });
                return true;
            } else {
                logger.warn("Ignoring onDemand update due to stopped scheduler");
                return false;
            }
        } else {
            logger.warn("Ignoring onDemand update due to rate limiter");
            return false;
        }
    }

    public void run() {
        try {
            // 5.1 刷新实例相关信息,刷新过程中如果发现有数据更新,则记录客户端的最新修改时间(脏时间戳),并进行 dirty 标记
            discoveryClient.refreshInstanceInfo();
	    // 如果 isInstanceInfoDirty = true ,表示本地实例信息和服务端注册表中的不一致
            // 那么需要向服务端发起注册
            Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
            if (dirtyTimestamp != null) {
                // 向服务端发起注册
                discoveryClient.register();
                // 更新本地 dirty 标记
                instanceInfo.unsetIsDirty(dirtyTimestamp);
            }
        } catch (Throwable t) {
            logger.warn("There was a problem with the instance info replicator", t);
        } finally {
            // 开启 Future 线程,放入子线程任务,间隔30s后执行
            Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
            // 预定下一次执行器需要执行的任务线程
            scheduledPeriodicRef.set(next);
        }
    }

}
5.1 discoveryClient.refreshInstanceInfo()
// DiscoveryClient.class
void refreshInstanceInfo() {
    // 5.1.1 刷新数据中心信息
    applicationInfoManager.refreshDataCenterInfoIfRequired();
    // 5.1.2 刷新心跳续租信息
    applicationInfoManager.refreshLeaseInfoIfRequired();

    InstanceStatus status;
    try {
        // 检查健康状态,默认不开启
        // 开启时,客户端会和相关中间件绑定
        // 绑定的中间件状态变更时可能会影响到当前客户端的状态
        status = getHealthCheckHandler().getStatus(instanceInfo.getStatus());
    } catch (Exception e) {
        logger.warn("Exception from healthcheckHandler.getStatus, setting status to DOWN", e);
      // 如果抛异常,则本地状态设置为下架 
      status = InstanceStatus.DOWN;
    }

    if (null != status) {
        applicationInfoManager.setInstanceStatus(status);
    }
}
5.1.1 刷新数据中心信息
// ApplicationInfoManager.class
public void refreshDataCenterInfoIfRequired() {
    // 获取本地已存在的主机名
    String existingAddress = instanceInfo.getHostName();

    String existingSpotInstanceAction = null;
    // 数据中心默认为 MyOwn ,表示自定义
    if (instanceInfo.getDataCenterInfo() instanceof AmazonInfo) {
        // 如果是亚马逊,设置 existingSpotInstanceAction
        existingSpotInstanceAction = ((AmazonInfo) instanceInfo.getDataCenterInfo()).get(AmazonInfo.MetaDataKey.spotInstanceAction);
    }

    String newAddress;
    if (config instanceof RefreshableInstanceConfig) {
        // Refresh data center info, and return up to date address
        // 如果是 Eureka 实例是可刷新,则刷新数据中心的相关信息,并从数据中心获取最新主机名
        newAddress = ((RefreshableInstanceConfig) config).resolveDefaultAddress(true);
    } else {
        // 如果 Eureka 实例是不可刷新,则最新主机名为本地的配置文件中配置的 ipAddress 或者 hostname
        newAddress = config.getHostName(true);
    }
    // 从本地配置文件中获取最新 ip 地址
    String newIp = config.getIpAddress();

    if (newAddress != null && !newAddress.equals(existingAddress)) {
        logger.warn("The address changed from : {} => {}", existingAddress, newAddress);
        // 5.1.1.1 如果最新主机名不为空且和已存在的主机名不一样,则更新主机名和 ip 地址
        updateInstanceInfo(newAddress, newIp);
    }

    if (config.getDataCenterInfo() instanceof AmazonInfo) {
        // 如果本地配置的数据中心是亚马逊,会比较 existingSpotInstanceAction ,不一致时会更新相应信息
        String newSpotInstanceAction = ((AmazonInfo) config.getDataCenterInfo()).get(AmazonInfo.MetaDataKey.spotInstanceAction);
        if (newSpotInstanceAction != null && !newSpotInstanceAction.equals(existingSpotInstanceAction)) {
            logger.info(String.format("The spot instance termination action changed from: %s => %s",
                    existingSpotInstanceAction,
                    newSpotInstanceAction));
            updateInstanceInfo(null , null );
        }
    }        
}
5.1.1.1 updateInstanceInfo()
// ApplicationInfoManager.class
private void updateInstanceInfo(String newAddress, String newIp) {
    // :( in the legacy code here the builder is acting as a mutator.
    // This is hard to fix as this same instanceInfo instance is referenced elsewhere.
    // We will most likely re-write the client at sometime so not fixing for now.
    // 不会马上更新实例信息,因为此时别的地方可能正在使用实例信息
    // 会创建一个 InstanceInfo.Builder,在某个时候再更新实例信息
    InstanceInfo.Builder builder = new InstanceInfo.Builder(instanceInfo);
    if (newAddress != null) {
        // 更新主机名
        builder.setHostName(newAddress);
    }
    if (newIp != null) {
        // 更新主机名
        builder.setIPAddr(newIp);
    }
    // 更新数据中心信息
    builder.setDataCenterInfo(config.getDataCenterInfo());
    // 进行 dirty 标记
    instanceInfo.setIsDirty();
}
5.1.2 刷新心跳续租信息
// ApplicationInfoManager.class
public void refreshLeaseInfoIfRequired() {
    // 获取本地缓存租约信息
    // 如果配置中更新了心跳间隔时间和租约过期间隔时间,则更新到本地缓存租约信息
    LeaseInfo leaseInfo = instanceInfo.getLeaseInfo();
    if (leaseInfo == null) {
        return;
    }
    int currentLeaseDuration = config.getLeaseExpirationDurationInSeconds();
    int currentLeaseRenewal = config.getLeaseRenewalIntervalInSeconds();
    if (leaseInfo.getDurationInSecs() != currentLeaseDuration || leaseInfo.getRenewalIntervalInSecs() != currentLeaseRenewal) {
        LeaseInfo newLeaseInfo = LeaseInfo.Builder.newBuilder()
                .setRenewalIntervalInSecs(currentLeaseRenewal)
                .setDurationInSecs(currentLeaseDuration)
                .build();
        instanceInfo.setLeaseInfo(newLeaseInfo);
        // 进行 dirty 标记
        instanceInfo.setIsDirty();
    }
}

7. 总结

  • 客户端启动时初始化定时任务有3个:

    • 刷新注册表:从服务端拉取注册表到客户端本地
      • 默认每30s执行一次
      • 当前任务执行超时(默认30s),下一次任务执行时间间隔翻倍,最多不超过300s(默认)
      • 当前任务执行未超时(默认30s),下一次任务执行时间间隔恢复正常(默认30s)
    • 心跳续租:客户端发起心跳续租到服务端,告诉服务端自身的信息状态,任务执行机制和刷新注册表一样
    • 检查客户端:相关配置变更时,刷新本地数据中心信息、刷新续租信息,以及进行健康检查
      • 默认每30s执行一次
      • 可插队
  • 所有定时任务的执行方式(时间间隔或可插队)是动态变化,满足动态变化的原因是每次执行完任务时开启下一次的任务

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值