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执行一次
- 可插队
- 刷新注册表:从服务端拉取注册表到客户端本地
-
所有定时任务的执行方式(时间间隔或可插队)是动态变化,满足动态变化的原因是每次执行完任务时开启下一次的任务