Eureka客户端解析
下图是我们Eureka客户端的主要流程分析:
根据上图分析源码
1. 查看我们的pom文件,发现我们导入了maven依赖
<!--Eureka的客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2. 根据我们导入的maven依赖查看导入的源码,查看其下面的spring.factories文件,利用SpringBoot的原理查看自动导入的类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration
3. 我们观察上面自动导入的类,有点多,这时候就靠猜了,看看和哪一个相关,这里我们查看的是EurekaClientAutoConfiguration类
4. 查看EurekaClientAutoConfiguration类
(4.1)这里我们关注下@AutoConfigureAfter注解。该注解的意思是当前类EurekaClientAutoConfiguration会在该注解中的类加载完成之后再加载。
(4.2)我们的@AutoConfigureAfter中导入了org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration
该类的作用就是创建一个Marker对象
(4.3)再回头看EurekaClientAutoConfiguration类的@ConditionalOnBean(EurekaDiscoveryClientConfiguration.Marker.class)注解,发现该注解的意思就是当我们类中有Marker对象的时候进行加载,这样我们就已将其联系起来。
5. EurekaClientAutoConfiguration类的Bean加载太多,我们只看关键的。这里我们看下DiscoveryClient类。
(5.1)这里我们自动注入了EurekaClient,该类也是在EurekaClientAutoConfiguration加载到Spring中的。
6. 观察EurekaClient类
(6.1)我们一步一步点进去,最终是来到了com.netflix.discovery.DiscoveryClient 中的构造函数
7. 观察该构造函数
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider) {
// .... 省略关键代码
logger.info("Initializing Eureka in region {}", clientConfig.getRegion());
// .... 省略关键代码
try {
// 定义一个线程池,设置其为守护线程,该线程池是一个延迟或者定期执行的线程池
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
eurekaTransport = new EurekaTransport();
scheduleServerEndpointTask(eurekaTransport, args);
AzToRegionMapper azToRegionMapper;
if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {
azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);
} else {
azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);
}
if (null != remoteRegionsToFetch.get()) {
azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));
}
instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());
} catch (Throwable e) {
throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
}
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
fetchRegistryFromBackup();
}
// call and execute the pre registration handler before all background tasks (inc registration) is started
if (this.preRegistrationHandler != null) {
this.preRegistrationHandler.beforeRegistration();
}
if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
try {
if (!register() ) {
throw new IllegalStateException("Registration error at startup. Invalid server response.");
}
} catch (Throwable th) {
logger.error("Registration error at startup: {}", th.getMessage());
throw new IllegalStateException(th);
}
}
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
// 最最最核心的代码 初始化线程任务 集群解析,心跳,实例信息赋值,拉取服务,
initScheduledTasks();
try {
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register timers", e);
}
// 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());
}
(7.1)@Inject注解的作用是其参数在运行时由配置好的IoC容器提供
8. 初始化时启动核心功能的定时任务:initScheduledTasks()
/**
* Initializes all scheduled tasks.
*/
private void initScheduledTasks() {
// 服务是否开启拉取,是否开启了fetch-register
if (clientConfig.shouldFetchRegistry()) {
// 服务注册列表的更新的周期时间
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
// 定时更新服务的注册列表
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
// 具体的更新服务的方法的具体逻辑
new CacheRefreshThread()
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}
// 如果我们允许当前服务向Eureka注册
if (clientConfig.shouldRegisterWithEureka()) {
// 服务续约的周期时间
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
// 从日志看,如果我们没有配置续约的时间,默认是30s
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);
// InstanceInfo replicator
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);
}
instanceInfoReplicator.onDemandUpdate();
}
};
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
// 调用start方法,该方法中会启动线程,该线程的作用是执行服务注册的逻辑
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
9. initScheduledTasks方法中的TimedSupervisorTask任务
- 我们在启动服务续约和定时更新服务注册表的线程都是通过new TimedSupervisorTask来实现的。
- 这里我们详细看下这个类的作用,TimedSupervisorTask类是一个Runnable接口,主要这里看其run方法
@Override
public void run() {
Future<?> future = null;
try {
// 该task是我们新建该类的对象的时候传进来的一个对象
// 如果我们是用来进行服务续约则该task就是new HeartbeatThread()
// 如果我们是用来更新服务注册表的话,该task就是new CacheRefreshThread()
// future模式执行线程
future = executor.submit(task);
threadPoolLevelGauge.set((long) executor.getActiveCount());
// 指定等待子线程的最长的时间
future.get(timeoutMillis, TimeUnit.MILLISECONDS); // block until done or timeout
// 这一步很关键,这里记得我们每次都会重新赋值这个值时间,delay变量在这里每次都被重置
delay.set(timeoutMillis);
threadPoolLevelGauge.set((long) executor.getActiveCount());
} catch (TimeoutException e) {
// 进入到这里表示的是执行超时,在规定的时间内线程还没有执行完成
logger.warn("task supervisor timed out", e);
timeoutCounter.increment();
// 得到当前delay的值
long currentDelay = delay.get();
// 任务线程超时的时候,就把delay变量翻倍,但是不会超过外部调用的时候设定的最大的延误时间
long newDelay = Math.min(maxDelay, currentDelay * 2);
// 将delay设置为最新的值,注意,这里因为有并发的情况。使用了CAS的机制,进行赋值
// CAS就是将内存中的值和期望值做对比,若一致,将其改为更新值
// 这里就是用cureentDelay的值和内存的值做对比,若相等,将其值改为newDelay
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) {
future.cancel(true);
}
if (!scheduler.isShutdown()) {
// 亮点啊 这里是只要调度器没有停止,就等待一段时间后,再执行一次任务
// 不管我们是服务续约还是服务注册表的更新,都不应该只执行一遍,应该不停的执行
// 这里我们重新调用其定时任务将其执行
// 这里的delay的时间已经变为原来设置值的2倍
// 假设外部调用的时候传入的超时时间是30s,构造方法中的timeout,最大的时间间隔为50s,构造方法中的expBackOffBound
// 如果最近一次的任务没有超时,那么再30s后重新开始新的任务
// 如果最近一次的任务超时了,那么就在50s后开始新的任务(这里的50s因为我们最大的间隔是50s,30*2大于了50,所以这里取的是50s)
scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
}
}
}
- scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);从代码的注释上可以看出这个方法是一次性调用的方法,但是实际上这个方法执行的任务是会反复执行的(服务续约,服务更新注册表),这里的关键就是类TimedSupervisorTask的run方法里,run方法任务执行完成之后,会再次调用schedule方法,在指定的时间之后执行一次相同的任务,这个时间间隔和最近一次任务是否超时有关,如果超时了则下一次执行任务的间隔时间就会变大。
- 代码精髓
从整体上来看,TimedSupervisorTask是固定间隔的周期性任务,一旦遇到了超时就会将下一个周期的间隔时间调大,如果连续超时的话,那么每次间隔时间都会增大一倍,一直到达外部参数设定的上限为止,因为只要超时就会进catch方法中,2倍扩充间隔时间,一旦新任务不再超时,间隔时间又会恢复初始值,不进入catch中,将delay重新赋值为初始。另外还有CAS来控制多线程的同步
- 定时更新服务注册列表线程CacheRefreshThread
- 代码解析
/**
* The task that fetches the registry information at specified intervals.
*
*/
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逻辑
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