Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能。
Eureka Server:提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
eureka客户端,主要处理服务的注册和发现。客户端服务通过注册和参数配置的方式,嵌入在客户端应用程序的代码中。在应用程序启动时,Eureka客户端向服务注册中心注册自身提供的服务,并周期性的发送心跳来更新它的服务租约。同时,他也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期行的刷新服务状态。
首先需要引入pom文件
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
//spring-cloud-starter-eureka-server中会引入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
在spring-cloud-starter-netflix-eureka-server中的spring.factories文件中,加载了EurekaServerAutoConfiguration类。
我们在使用的时候,会在application应用中,添加@EnableEurekaServer注解。注解会导入EurekaServerMarkerConfiguration类。该类中会创建Marker这个Bean对象。
@Configuration
public class EurekaServerMarkerConfiguration {
@Bean
public Marker eurekaServerMarkerBean() {
return new Marker();
}
class Marker {
}
}
继续看EurekaServerAutoConfiguration类。
@Configuration
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter
装载类的条件是需要有之前创建的Marker Bean对象。也就是必须加@EnableEurekaServer的原因。EurekaServerAutoConfiguration类还会装载一些Bean
@ConditionalOnMissingBean
public EurekaServerConfig eurekaServerConfig(EurekaClientConfig clientConfig) {
EurekaServerConfigBean server = new EurekaServerConfigBean();
if (clientConfig.shouldRegisterWithEureka()) {
// Set a sensible default if we are supposed to replicate
server.setRegistrySyncRetries(5);
}
return server;
}
@Bean
public PeerAwareInstanceRegistry peerAwareInstanceRegistry(
ServerCodecs serverCodecs) {
this.eurekaClient.getApplications(); // force initialization
return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig,
serverCodecs, this.eurekaClient,
this.instanceRegistryProperties.getExpectedNumberOfRenewsPerMin(),
this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
}
@Bean
@ConditionalOnMissingBean
public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry,
ServerCodecs serverCodecs) {
return new PeerEurekaNodes(registry, this.eurekaServerConfig,
this.eurekaClientConfig, serverCodecs, this.applicationInfoManager);
}
@Bean
public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,
PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs,
registry, peerEurekaNodes, this.applicationInfoManager);
}
@Bean
public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry,
EurekaServerContext serverContext) {
return new EurekaServerBootstrap(this.applicationInfoManager,
this.eurekaClientConfig, this.eurekaServerConfig, registry,
serverContext);
}
1、EurekaServerConfig主要是Eureka服务端的一些配置属性,比如同步周期、超时时间等等。
2、peerAwareInstanceRegistry的实现类是InstanceRegistry,继承自PeerAwareInstanceRegistryImpl,主要是:PeerAwareInstanceRegistryImpl 新的Eureka Server节点加入集群后的影响 新服务注册(Register)注册时的影响 服务心跳(renew) 服务下线和剔除以及自我保护模式。每隔60S执行一次服务信息的回收任务,方法为postInit。
3、PeerEurekaNodes主要是用于同步服务集群其他节点信息。
4、EurekaServerBootstrap:Eureka服务启动加载触发类。
5、EurekaServerContext:主要负责peerEurekaNodes和PeerAwareInstanceRegistry的启动。
EurekaServerContext的类实现:
@Singleton
public class DefaultEurekaServerContext implements EurekaServerContext {
private static final Logger logger = LoggerFactory.getLogger(DefaultEurekaServerContext.class);
private final EurekaServerConfig serverConfig;
private final ServerCodecs serverCodecs;
private final PeerAwareInstanceRegistry registry;
private final PeerEurekaNodes peerEurekaNodes;
private final ApplicationInfoManager applicationInfoManager;
@Inject
public DefaultEurekaServerContext(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes, ApplicationInfoManager applicationInfoManager) {
this.serverConfig = serverConfig;
this.serverCodecs = serverCodecs;
this.registry = registry;
this.peerEurekaNodes = peerEurekaNodes;
this.applicationInfoManager = applicationInfoManager;
}
@PostConstruct
public void initialize() throws Exception {
logger.info("Initializing ...");
this.peerEurekaNodes.start();
this.registry.init(this.peerEurekaNodes);
logger.info("Initialized");
}
}
peerEurekaNodes.start()方法会定时更新集群内其他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;
}
registry.init(this.peerEurekaNodes)方法主要负责统计续约数量和续约阀值。每15分钟一次。
public void init(PeerEurekaNodes peerEurekaNodes) throws Exception {
this.numberOfReplicationsLastMin.start();
this.peerEurekaNodes = peerEurekaNodes;
this.initializedResponseCache();
this.scheduleRenewalThresholdUpdateTask();
this.initRemoteRegionRegistry();
try {
Monitors.registerObject(this);
} catch (Throwable var3) {
logger.warn("Cannot register the JMX monitor for the InstanceRegistry :", var3);
}
}
//默认每隔15分钟进行一次检查
private void scheduleRenewalThresholdUpdateTask() {
this.timer.schedule(new TimerTask() {
public void run() {
PeerAwareInstanceRegistryImpl.this.updateRenewalThreshold();
}
}, (long)this.serverConfig.getRenewalThresholdUpdateIntervalMs(), (long)this.serverConfig.getRenewalThresholdUpdateIntervalMs());
}
private void updateRenewalThreshold() {
try {
Applications apps = this.eurekaClient.getApplications();
int count = 0;
Iterator var3 = apps.getRegisteredApplications().iterator();
while(var3.hasNext()) {
Application app = (Application)var3.next();
Iterator var5 = app.getInstances().iterator();
while(var5.hasNext()) {
InstanceInfo instance = (InstanceInfo)var5.next();
if (this.isRegisterable(instance)) {
++count;
}
}
}
Object var10 = this.lock;
synchronized(this.lock) {
if ((double)(count * 2) > this.serverConfig.getRenewalPercentThreshold() * (double)this.numberOfRenewsPerMinThreshold || !this.isSelfPreservationModeEnabled()) {
//设置期望续约数
this.expectedNumberOfRenewsPerMin = count * 2;
//设置续约阀值数量 默认比例是 cout * 0.85
this.numberOfRenewsPerMinThreshold = (int)((double)(count * 2) * this.serverConfig.getRenewalPercentThreshold());
}
}
logger.info("Current renewal threshold is : {}", this.numberOfRenewsPerMinThreshold);
} catch (Throwable var9) {
logger.error("Cannot update renewal threshold", var9);
}
}
这个方法每15分钟执行一次,统计的是当前所有实例的数量,然后设置期望续约数量以及续约阀值数量。续约阀值数量为期望续约数 * 0.85。不会一次性的把续约次数降至85%以下,也就是只有在存活应用信息数量超过总数的85%时才能更新,这样就不会修改续约的自我保护的临界值。
当我们本地访问http://localhost:8761/时 会调用PeerAwareInstanceRegistryImpl.isBelowRenewThresold方法进行判断是否提示开启保护。
public int isBelowRenewThresold() {
return this.getNumOfRenewsInLastMin() <= (long)this.numberOfRenewsPerMinThreshold && this.startupTime > 0L &&
System.currentTimeMillis() > this.startupTime + (long)this.serverConfig.getWaitTimeInMsWhenSyncEmpty() ? 1 : 0;
}
就是判断上一分钟续约数如果小于等于续约阀值数量,并且当前时间 > 设置开始统计时间(默认5分钟),则在页面中会出现提示。
设置开始统计时间的参数为waitTimeInMsWhenSyncEmpty,可以直接设置为 0 观看效果。
eureka:
server:
waitTimeInMsWhenSyncEmpty: 0
前面说过EurekaServerBootstrap是Eureka服务启动加载触发类。通过EurekaServerInitializerConfiguration.start方法会执行EurekaServerBootstrap.contextInitialized方法
public void contextInitialized(ServletContext context) {
try {
initEurekaEnvironment();
initEurekaServerContext();
context.setAttribute(EurekaServerContext.class.getName(), this.serverContext);
}
catch (Throwable e) {
log.error("Cannot bootstrap eureka server :", e);
throw new RuntimeException("Cannot bootstrap eureka server :", e);
}
}
主要看initEurekaServerContext方法
protected void initEurekaServerContext() throws Exception {
// For backward compatibility
JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
XStream.PRIORITY_VERY_HIGH);
XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
XStream.PRIORITY_VERY_HIGH);
if (isAws(this.applicationInfoManager.getInfo())) {
this.awsBinder = new AwsBinderDelegate(this.eurekaServerConfig,
this.eurekaClientConfig, this.registry, this.applicationInfoManager);
this.awsBinder.start();
}
EurekaServerContextHolder.initialize(this.serverContext);
log.info("Initialized server context");
// 注册数量
int registryCount = this.registry.syncUp();
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
// Register all monitoring statistics.
EurekaMonitors.registerAllStats();
}
跟进registry.openForTraffic(this.applicationInfoManager, registryCount)
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
//设置当前期望续约数量
this.expectedNumberOfRenewsPerMin = count * 2;
//设置续约阀值
this.numberOfRenewsPerMinThreshold = (int)((double)this.expectedNumberOfRenewsPerMin * this.serverConfig.getRenewalPercentThreshold());
logger.info("Got " + count + " instances from neighboring DS node");
logger.info("Renew threshold is: " + this.numberOfRenewsPerMinThreshold);
//设置启动时间
this.startupTime = System.currentTimeMillis();
if (count > 0) {
this.peerInstancesTransferEmptyOnStartup = false;
}
Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
boolean isAws = Name.Amazon == selfName;
if (isAws && this.serverConfig.shouldPrimeAwsReplicaConnections()) {
logger.info("Priming AWS connections for all replicas..");
this.primeAwsReplicas(applicationInfoManager);
}
logger.info("Changing status to UP");
applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
super.postInit();
}
主要就是根据之前传入的count数量,计算启动时的期望续约数量(就是从其他集群同步过来的,如果单机则该值为1),以及续约阀值和启动时间,接着进入到super.postInit()
protected void postInit() {
//统计前一分钟收到的续约次数,register、renew都会修改里面的count
this.renewsLastMin.start();
if (this.evictionTaskRef.get() != null) {
((AbstractInstanceRegistry.EvictionTask)this.evictionTaskRef.get()).cancel();
}
this.evictionTaskRef.set(new AbstractInstanceRegistry.EvictionTask());
this.evictionTimer.schedule((TimerTask)this.evictionTaskRef.get(), this.serverConfig.getEvictionIntervalTimerInMs(), this.serverConfig.getEvictionIntervalTimerInMs());
}
//每隔一分钟统计最近一分钟内所有Client的续约次数,也就是接收到的心跳次数,以此来作为是否触发服务信息回收的依据之一
public synchronized void start() {
if (!this.isActive) {
this.timer.schedule(new TimerTask() {
public void run() {
try {
//每分钟清0一次
MeasuredRate.this.lastBucket.set(MeasuredRate.this.currentBucket.getAndSet(0L));
} catch (Throwable var2) {
MeasuredRate.logger.error("Cannot reset the Measured Rate", var2);
}
}
}, this.sampleInterval, this.sampleInterval);
this.isActive = true;
}
}
继续看evictionTaskRef.set(new AbstractInstanceRegistry.EvictionTask()),以及设置的每分钟执行一次evictionTimer.schedule((TimerTask)this.evictionTaskRef.get(), this.serverConfig.getEvictionIntervalTimerInMs(), this.serverConfig.getEvictionIntervalTimerInMs())方法。
EvictionTask() {
}
public void run() {
try {
long compensationTimeMs = this.getCompensationTimeMs();
AbstractInstanceRegistry.logger.info("Running the evict task with compensationTime {}ms", compensationTimeMs);
AbstractInstanceRegistry.this.evict(compensationTimeMs);
} catch (Throwable var3) {
AbstractInstanceRegistry.logger.error("Could not run the evict task", var3);
}
}
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
// 判断是否开启自我保护机制
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
// 循环遍历本地CurrentHashMap中的实例信息
for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
if (leaseMap != null) {
for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
Lease<InstanceInfo> lease = leaseEntry.getValue();
// 判断是否过期,此处为重点,里面有判断实例过期的依据
if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
expiredLeases.add(lease);
}
}
}
}
// 获取注册的实例数量
int registrySize = (int) getLocalRegistrySize();
// serverConfig.getRenewalPercentThreshold() 为0.85 , 主要是为了避免开启自动保护机制。 所以会逐步过期
int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
// 可以过期的数量
int evictionLimit = registrySize - registrySizeThreshold;
// 取最小值,在过期数量和可以过期的数量中间取最小值。
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
if (toEvict > 0) {
logger.info("Evicting {} items (expired={}, evictionLimit={})", toEvict, expiredLeases.size(), evictionLimit);
// 随机过期
Random random = new Random(System.currentTimeMillis());
for (int i = 0; i < toEvict; i++) {
// Pick a random item (Knuth shuffle algorithm)
int next = i + random.nextInt(expiredLeases.size() - i);
Collections.swap(expiredLeases, i, next);
Lease<InstanceInfo> lease = expiredLeases.get(i);
String appName = lease.getHolder().getAppName();
String id = lease.getHolder().getId();
// 写入过期监控
EXPIRED.increment();
// 服务下线
logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
internalCancel(appName, id, false);
}
}
}
EurekaServer每隔60S执行一次服务信息的回收任务,移除那些超过90S未更新租约信息的服务。当然能够回收的前提是未触发自我保护。所谓的自我保护机制,就是最近一分钟内的实际续约次数比例超过期望总数的85%,如果未超过,那么认为是Server出现了问题,不进行服务回收。
Lease.isExpire()
判断是否过期
public boolean isExpired(long additionalLeaseMs) {
return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
}
续约的时候会更新最后更新时间
public void renew() {
lastUpdateTimestamp = System.currentTimeMillis() + duration;
}
duration : 过期间隔,默认为90秒。
lastUpdateTimestamp : 为最后更新时间,当前时间 + 90s。
但是在最终做判断的时候lastUpdateTimestamp + duration + additionalLeaseMs , 这个地方还加了一遍,也就导致了,当前时间必须要大于实际最后更新时间180秒,才会认为过期。
服务端的定时分析到此完毕
=================================分割线==========================================================
EurekaClient客户端
客户端启动后主要实现类是DiscoveryClient。该类的构造方法有几处比较关键。
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider) {
.................
//如果作为EurekaServer 是无需注册和拉取的
if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
logger.info("Client configured to neither register nor query for data.");
scheduler = null;
heartbeatExecutor = null;
cacheRefreshExecutor = null;
eurekaTransport = null;
instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());
// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
// to work with DI'd DiscoveryClient
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
initTimestampMs = System.currentTimeMillis();
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
initTimestampMs, this.getApplications().size());
return; // no need to setup up an network tasks and we are done
}
//如果不是,创建三个线程池
try {
// default size of 2 - 1 each for heartbeat and cacheRefresh
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
....................
initScheduledTasks();
.....................
}
如果是作为EurekaServer,则无需注册和拉取。
如果作为EurekaClient,则创建三个线程池,一个是创建数量为2的用于定时调度的线程池,还有一个是用于执行发送心跳的线程池,还一个是用于获取列表的线程池。在initScheduledTasks方法中。
private void initScheduledTasks() {
if (clientConfig.shouldFetchRegistry()) {
// registry cache refresh timer
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
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
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
),
renewalIntervalInSecs, TimeUnit.SECONDS);
................
}
可以看到CacheRefreshThread()延时执行刷新的列表的是registryFetchIntervalSeconds参数,默认30s,即每30s刷新一次。HeartbeatThread()延时执行发送心跳的是renewalIntervalInSecs参数,默认30s。
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);
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();
}
if (logger.isDebugEnabled()) {
StringBuilder allAppsHashCodes = new StringBuilder();
allAppsHashCodes.append("Local region apps hashcode: ");
allAppsHashCodes.append(localRegionApps.get().getAppsHashCode());
allAppsHashCodes.append(", is fetching remote regions? ");
allAppsHashCodes.append(isFetchingRemoteRegionRegistries);
for (Map.Entry<String, Applications> entry : remoteRegionVsApps.entrySet()) {
allAppsHashCodes.append(", Remote region: ");
allAppsHashCodes.append(entry.getKey());
allAppsHashCodes.append(" , apps hashcode: ");
allAppsHashCodes.append(entry.getValue().getAppsHashCode());
}
logger.debug("Completed cache refresh task for discovery. All Apps hash code is {} ",
allAppsHashCodes.toString());
}
} catch (Throwable e) {
logger.error("Cannot fetch registry from server", e);
}
}
继续看拉取流程
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
try {
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
{
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();
}
}
}
全量拉取处理
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()) {
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");
}
}
那么问题来了,如果启动一个EurekaClient服务,另一个Eureka Client最长多久可以发现?
首先,Eureka对HTTP响应做了缓存。在Eureka的”控制器”类ApplicationResource的109行可以看到有一行 String payLoad = responseCache.get(cacheKey);
的调用,该代码所在的getApplication()方法的功能是响应客户端查询某个服务信息的HTTP请求:
String payLoad = responseCache.get(cacheKey); // 从cache中拿响应数据
if (payLoad != null) {
logger.debug("Found: {}", appName);
return Response.ok(payLoad).build();
} else {
logger.debug("Not Found: {}", appName);
return Response.status(Status.NOT_FOUND).build();
}
上面的代码中,responseCache引用的是ResponseCache类型,该类型是一个接口,其get()方法首先会去缓存中查询数据,如果没有则生成数据返回(即真正去查询注册列表),且缓存的有效时间为30s。也就是说,客户端拿到Eureka的响应并不一定是即时的,大部分时候只是缓存信息。
其次,Eureka Client对已经获取到的注册信息也做了30s缓存。即服务通过eureka客户端第一次查询到可用服务地址后会将结果缓存,每30s拉取一次。
再次, 负载均衡组件Ribbon也有30s缓存。Ribbon会从上面提到的Eureka Client获取服务列表,然后将结果缓存30s。
最后,如果你并不是在Spring Cloud环境下使用这些组件(Eureka, Ribbon),你的服务启动后并不会马上向Eureka注册,而是需要等到第一次发送心跳请求时才会注册。心跳请求的发送间隔也是30s。(Spring Cloud对此做了修改,服务启动后会马上注册)
所以如果除去最后一个条件,那么发现一个服务最多需要90s时间。
总结:
1、EurekaServer 每15分钟一次统计续约数量和续约阀值。
2、EurekaServer每隔60S执行一次服务信息的回收任务,移除那些超过90S未更新租约信息的服务。但实际移除的超过180s。
3、EurekaServer 每30s 更新一次responseCache,用于返回Eureka Client请求列表信息。
4、Eureka Client 每30s 向 EurekaServer 发送一次心跳续约。
5、Eureka Client 每30s 向 EurekaServer 拉取一次服务列表信息。