1.EurekaServer内定时更新集群内其他Server节点
public class PeerEurekaNodes {
/**
* Eureka-Server 集群节点数组
*/
private volatile List<PeerEurekaNode> peerEurekaNodes = Collections.emptyList();
/**
* Eureka-Server 服务地址数组
*/
private volatile Set<String> peerEurekaNodeUrls = Collections.emptySet();
/**
* 启动 Eureka-Server 集群节点集合(复制)
*/
public void start() {
......
// 初始化 集群节点信息
updatePeerEurekaNodes(resolvePeerUrls());
// 初始化 初始化固定周期更新集群节点信息的任务
Runnable peersUpdateTask = new Runnable() {
@Override
public void run() {
try {
updatePeerEurekaNodes(resolvePeerUrls());
} catch (Throwable e) {
logger.error("Cannot update the replica Nodes", e);
}
}
};
// 每隔10分钟更新集群节点
taskExecutor.scheduleWithFixedDelay(
peersUpdateTask,
serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
TimeUnit.MILLISECONDS
);
......
}
/**
* Resolve peer URLs. 获取Server集群的所有serviceUrl,不包括自身
*
* @return peer URLs with node's own URL filtered out
*/
protected List<String> resolvePeerUrls() {
// 获得 Eureka-Server 集群服务地址数组
InstanceInfo myInfo = applicationInfoManager.getInfo();
String zone = InstanceInfo.getZone(clientConfig.getAvailabilityZones(clientConfig.getRegion()), myInfo);
// 获取相同Region下的所有serviceUrl
List<String> replicaUrls = EndpointUtils.getDiscoveryServiceUrls(clientConfig, zone, new EndpointUtils.InstanceInfoBasedUrlRandomizer(myInfo));
// 移除自己(避免向自己同步)
int idx = 0;
while (idx < replicaUrls.size()) {
if (isThisMyUrl(replicaUrls.get(idx))) {
replicaUrls.remove(idx);
} else {
idx++;
}
}
return replicaUrls;
}
}
EurekaServer在初始化时会根据配置的Server集群url地址,来实例化集群内其他Server节点的交互实例,用于集群间的数据同步。
EurekaServer在获取Server URL时用的还是EurekaClient,也就是环境里eureka.client开头的配置,客户端配置一模一样。
其实EurekaServer集成了EurekaClient,Client的配置都可以用到Server上,只不过很多可以略去,比如是否需要向Server发送心跳registerWithEureka,可以设置为false等。
Client里的serviceUrl的含义是可以向哪些Server节点注册和拉取服务信息;而Server里的serviceUrl的含义是集群里有哪些Server节点,当自身节点有服务操作是需要向哪些节点同步。
Client正常情况下只合Server集群中的一个交互,而Server在有服务操作时会同步至所有其他的节点。
应用的配置信息是可能发生变化的,所以Client和Server才需要定时的刷新集群节点信息,关闭那些不再连接的Server节点,初始化新增的节点。
2.每隔一分钟统计最近一分钟内所有Client的续约次数,也就是接收到的心跳次数,以此来作为是否触发服务信息回收的依据之一
public class MeasuredRate {
/**
* 间隔, 默认60S
*/
private final long sampleInterval;
public synchronized void start() {
if (!isActive) {
// 每隔一分钟执行一次定时任务, 更新最新的总的续约次数, 这样就能计算一分钟内续约的次数, 以此来判断续约次数是否低于阈值
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
// Zero out the current bucket. 将 currentBucket 赋值给lastBucket, 然后重置 currentBucket
lastBucket.set(currentBucket.getAndSet(0));
} catch (Throwable e) {
logger.error("Cannot reset the Measured Rate", e);
}
}
}, sampleInterval, sampleInterval);
isActive = true;
}
}
}
EurekaServer每隔1分钟执行一次服务信息回收,回收那些超过90S没有发送心跳,也就是续约的服务信息,当然前提是EurekaServer开启租约过期功能,且未触发自我保护临界值。
所谓自我保护,就是指最近一分钟内的续约总数 > 预估的续约总数 * 0.85。 近似的来讲,也就是一分钟内有超过85%的应用信息发送了心跳。如果这个条件未满足,那么不会执行服务回收操作。
比如当Server节点的网络不稳定,丢失了部分心跳信息,如果超过了15,那么就不会触发自我保护,停止服务信息的回收,而这也是我们希望服务发现组件应该具备的功能,强调可用性。
从这点上看其和Zookeeper的服务发现机制有很大不同。
3.EurekaServer每隔一分钟执行一次服务信息的回收
/**
* 租约过期任务
*/
/* visible for testing */
class EvictionTask extends TimerTask {
/**
* 上一次执行清理任务的时间
*/
private final AtomicLong lastExecutionNanosRef = new AtomicLong(0L);
@Override
public void run() {
try {
// 获取 补偿时间毫秒数, 计算这次执行距离上次执行的时间差,与60S的距离
long compensationTimeMs = getCompensationTimeMs();
logger.info("Running the evict task with compensationTime {}ms", compensationTimeMs);
// 清理过期租约逻辑
evict(compensationTimeMs);
} catch (Throwable e) {
logger.error("Could not run the evict task", e);
}
}
/**
* compute a compensation time defined as the actual time this task was executed since the prev iteration,
* vs the configured amount of time for execution. This is useful for cases where changes in time (due to
* clock skew or gc for example) causes the actual eviction task to execute later than the desired time
* according to the configured cycle.
*/
long getCompensationTimeMs() {
long currNanos = getCurrentTimeNano();
long lastNanos = lastExecutionNanosRef.getAndSet(currNanos);
if (lastNanos == 0L) {
return 0L;
}
// 此次执行与上次执行的时间差
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);
// 查看时间间隔是否比60S大
long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
// 如果未超过60S, 返回; 否则返回超过的时间差
return compensationTime <= 0L ? 0L : compensationTime;
}
long getCurrentTimeNano() { // for testing
return System.nanoTime();
}
}
EurekaServer每隔60S执行一次服务信息的回收任务,移除那些超过90S未更新租约信息的服务。
当然能够回收的前提是开启了租约回收功能,而且未触发自我保护。所谓的自我保护机制,就是最近一分钟内的实际续约次数比例超过期望总数的85%,如果未超过,那么认为是Server出现了问题,不进行服务回收。
4.定时更新续约次数的期望值和自我保护的临界值
public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry {
/**
* Schedule the task that updates renewal threshold periodically.
* The renewal threshold would be used to determine if the renewals drop
* dramatically because of network partition and to protect expiring too
* many instances at a time.
*/
private void scheduleRenewalThresholdUpdateTask() {
// 15分钟后更新续约阈值,之后每隔15分分钟执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
updateRenewalThreshold();
}
}, serverConfig.getRenewalThresholdUpdateIntervalMs(),
serverConfig.getRenewalThresholdUpdateIntervalMs());
}
/**
* 更新续约阈值,也就是每分钟期望续约的次数,以及触发自我保护的最低续约次数
* Updates the renewal threshold based on the current number of
* renewals. The threshold is a percentage as specified in
* {@link EurekaServerConfig#getRenewalPercentThreshold()} of renewals
* received per minute {@link #getNumOfRenewsInLastMin()}.
*/
private void updateRenewalThreshold() {
try {
// 计算 应用实例数
Applications apps = eurekaClient.getApplications();
int count = 0;
for (Application app : apps.getRegisteredApplications()) {
for (InstanceInfo instance : app.getInstances()) {
if (this.isRegisterable(instance)) {
++count;
}
}
}
// 计算 expectedNumberOfRenewsPerMin 、 numberOfRenewsPerMinThreshold 参数
synchronized (lock) {
// Update threshold only if the threshold is greater than the
// current expected threshold of if the self preservation is disabled.
// 不会一次性的把续约次数将至85%以下,也就是只有在存活应用信息数量超过总数的85%时才能更新,这样就不会修改续约的自我保护的临界值
if ((count * 2) > (serverConfig.getRenewalPercentThreshold() * numberOfRenewsPerMinThreshold) || (!this.isSelfPreservationModeEnabled())) {
this.expectedNumberOfRenewsPerMin = count * 2;
this.numberOfRenewsPerMinThreshold = (int)((count * 2) * serverConfig.getRenewalPercentThreshold());
}
}
logger.info("Current renewal threshold is : {}", numberOfRenewsPerMinThreshold);
} catch (Throwable e) {
logger.error("Cannot update renewal threshold", e);
}
}
}
服务自身取消,会相应的降低续约期望总数和自我保护临界值,每取消一个,数值均减2。但因为续约时间超时而被动移除的服务信息,不会相应的减少期望总值和临界值。
如果不定时的更新期望总值和临界值,那么当服务逐渐的因心跳超时而被移除时,很容易就触发保护临界值,之后就不能再移除那些心跳超时的服务信息。
但是在更新总值和临界值时,如果当前Server处于自我保护状态,那么也不能强制的改变临界值,这会强制的退出自我保护状态。所以更新总值和临界值的前提是当前Server不处于自我保护状态,也就是上一分钟的续约总数的比例超过85%。
5.服务信息增量缓存更新任务
public abstract class AbstractInstanceRegistry implements InstanceRegistry {
/**
* 最近租约变更记录队列
*/
private ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue = new ConcurrentLinkedQueue<RecentlyChangedItem>();
protected AbstractInstanceRegistry(EurekaServerConfig serverConfig, EurekaClientConfig clientConfig, ServerCodecs serverCodecs) {
......
// 30S后每隔30S执行一次, 移除3分钟前发生的续约记录
this.deltaRetentionTimer.schedule(getDeltaRetentionTask(),
serverConfig.getDeltaRetentionTimerIntervalInMs(), // 30S
serverConfig.getDeltaRetentionTimerIntervalInMs()); // 30S
}
private TimerTask getDeltaRetentionTask() {
return new TimerTask() {
@Override
public void run() {
Iterator<RecentlyChangedItem> it = recentlyChangedQueue.iterator();
while (it.hasNext()) {
RecentlyChangedItem item = it.next();
// 如果某个续约任务是3分钟前发生的,那么移除它
if (item.getLastUpdateTime() < System.currentTimeMillis() - serverConfig.getRetentionTimeInMSInDeltaQueue()) {
it.remove();
} else {
break;
}
}
}
};
}
}
EurekaClient在初始化时进行一次全量拉取,之后每隔30S执行一次增量拉取,也就是会返回recentlyChangedQueue里的记录,EurekaClient根据记录的操作类型和服务信息,相应的更新自身持有的可用服务信息。
recentlyChangedQueue 是一个有序队列,当Client向Server执行操作时,比如注册,状态变更,取消等(续约不会记录),那么会记录操作的时间,类型和相应的服务信息。
通过增量信息来保持同步,能够极大的减少Server和Client之间的数据的传输,降低IO消耗。
6.每隔30S执行一次,更新只读响应缓存
public class ResponseCacheImpl implements ResponseCache {
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
......
// 仅使用只读缓存, 因此每隔30S执行更新缓存任务
if (shouldUseReadOnlyResponseCache) {
timer.schedule(getCacheUpdateTask(),
new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs) + responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}
}
/**
* 缓存更新任务, 每隔30S执行一次
*
* @return
*/
private TimerTask getCacheUpdateTask() {
return new TimerTask() {
@Override
public void run() {
logger.debug("Updating the client cache from response cache");
for (Key key : readOnlyCacheMap.keySet()) { // 循环 readOnlyCacheMap 的缓存键
if (logger.isDebugEnabled()) {
Object[] args = {key.getEntityType(), key.getName(), key.getVersion(), key.getType()};
logger.debug("Updating the client cache from response cache for key : {} {} {} {}", args);
}
try {
CurrentRequestVersion.set(key.getVersion());
Value cacheValue = readWriteCacheMap.get(key);
Value currentCacheValue = readOnlyCacheMap.get(key);
if (cacheValue != currentCacheValue) { // 不一致时,进行替换
readOnlyCacheMap.put(key, cacheValue);
}
} catch (Throwable th) {
logger.error("Error while updating the client cache from response cache for key {}", key.toStringCompact(), th);
}
}
}
};
}
}
EurekaServer会缓存数据信息,根据Key的不同值缓存相应的结果,当Client获取信息时,优先用只读缓存的数据返回,如果只读缓存不存在,那么从读写缓存处获取,然后存入只读缓存,最后返回结果。
读写缓存借助guava的CacheBuilder来实现缓存淘汰,在写入180S后失效,这样当只读缓存定期更新时,如果发现读写缓存的值和只读缓存的不一致时,进行替换。
当Client进行相应操作,比如注册,状态变更,取消等操作时,会时对应的缓存立即失效,保证Client获取到的是有效的服务信息。