【源码】Spring Cloud —— Eureka Client 2 DiscoveryClient
前言
上一章节对 Spring Cloud Netflix Eureka Client 提供的 核心组件类 做了大致的了解,本章节结合部分源码解读核心类 DiscoveryClient
该系列章节需要相关的内容做铺垫,传送门:
关于延时、周期任务调度 —— ScheduledExecutorService ScheduledThreadPoolExecutor
版本
Spring Cloud Netflix 版本:2.2.3.RELEASE
对应 Netflix-Eureka 版本:1.9.21
DiscoveryClient
DiscoveryClient 的核心业务逻辑发生在 构造方法 中,也就是说,在启动 Spring 应用注册对应的 Bean 时同时注册对应的 服务实例
if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
// 略
return;
}
属性 shouldRegisterWithEureka
和 shouldFetchRegistry
为 false
时,不进行 服务注册、发现,即对应我们的配置项 eureka.client.register-with-eureka
和 eureka.client.fetch-register
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()
);
cacheRefreshExecutor = new ThreadPoolExecutor(
1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
.setDaemon(true)
.build()
);
如果允许 服务注册、发现,则实例化用于 发送心跳、刷新缓存 相关的 线程池
eurekaTransport = new EurekaTransport();
// 构建
scheduleServerEndpointTask(eurekaTransport, args);
EurekaTransport 封装了 DiscoveryClient 与 Eureka Server 进行 HTTP 调用的 Jersey 客户端
注册表信息拉取
// 允许拉取注册表信息,则进行拉取
// 如果拉取失败,则调用 fetchRegistryFromBackup
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
fetchRegistryFromBackup();
}
如果允许注册表信息拉取,则调用 fetchRegistry
方法拉取,拉取失败则调用交由成员属性 backupRegistryProvider
获取,其默认实现为 NotImplementedRegisteryImpl,即没有实现
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
{
// 全量拉取
getAndStoreFullRegistry();
} else {
// 增量拉取
getAndUpdateDelta(applications);
}
// hash 码计算
applications.setAppsHashCode(applications.getReconcileHashCode());
// 打印实例数
logTotalInstances();
} catch (Throwable e) {
// ...
}
// ...
return true;
}
- 全量拉取:第一次拉取时,必然是 全量拉取 并缓存到本地,请求
url
为eureka/apps
,拉取的实例经过过滤、调整后缓存在localRegionApps
属性 - 增量拉取:之后的拉取则只针对一段时间内发生的变更信息,并根据变更的状态进行 增量式更新,请求
url
为/eureka/app/delta
服务注册
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);
}
}
注册方法为 register
,PS:这种在判断条件中执行逻辑的写法,在 JUC 源码中十分常用
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
请求 url
为 /eureka/${APP_NAME}
,传递参数为 InstanceInfo,如果服务器返回 204
状态码,则表明注册成功
定时任务
// 初始化定时任务
initScheduledTasks();
定时任务包括
- 定时拉取注册列表信息,因为第一次全量拉取注册列表之后缓存在本地,因此需要定时获取 Eureka Server 的注册列表信息,进行更新
- 心跳续约,需要定时向 Eureka Server 发送心跳以保证 服务实例 的健康
- 按需注册,需要定时或者在 实例状态 发生改变时,重新注册 对应的实例,以保证服务的 可用性
定时拉取注册列表信息
if (clientConfig.shouldFetchRegistry()) {
// 拉取时间间隔,可由属性
// eureka.client.registry-fetch-interval-seconds 设置
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
cacheRefreshTask = new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
);
scheduler.schedule(
cacheRefreshTask,
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}
定时拉取注册列表信息,初始化对应的 cacheRefreshTask
后交由 scheduler
延时执行,对于 周期 执行并未由 scheduler
实现,而是委托给了 TimedSupervisorTask#run
@Override
public void run() {
Future<?> future = null;
try {
// 任务执行
future = executor.submit(task);
threadPoolLevelGauge.set((long) executor.getActiveCount());
// 结果获取
future.get(timeoutMillis, TimeUnit.MILLISECONDS); // block until done or timeout
delay.set(timeoutMillis);
threadPoolLevelGauge.set((long) executor.getActiveCount());
// 成功计数
successCounter.increment();
} catch (...) {
// 略
} finally {
if (future != null) {
future.cancel(true);
}
// 此处再次将任务交给 scheduler,实现周期执行
if (!scheduler.isShutdown()) {
scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
}
}
}
拉取注册表信息的业务逻辑由 CacheRefreshThread 定义
class CacheRefreshThread implements Runnable {
public void run() {
refreshRegistry();
}
}
refreshRegistry 略
心跳续约
// 续约周期,默认 30s
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);
heartbeatTask = new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
);
scheduler.schedule(
heartbeatTask,
renewalIntervalInSecs, TimeUnit.SECONDS);
续约逻辑由 HeartbeatThread 提供
private class HeartbeatThread implements Runnable {
public void run() {
if (renew()) {
lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
}
---------------------- renew ----------------------
boolean renew() {
EurekaHttpResponse<InstanceInfo> httpResponse;
try {
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()) {
REREGISTER_COUNTER.increment();
logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());
long timestamp = instanceInfo.setIsDirtyWithTime();
// 对于 404 的实例,重新进行注册
boolean success = register();
if (success) {
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;
}
}
请求的 url
为 apps/${APP_NAME}/${INSTANCE_INFO_ID}
,HTTP 方法为 put
,续约成功返回 200
状态码
按需注册
// 负责定时刷新实例,检测状态按需注册
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2);
// 监听器,负责监听实例的状态,按需注册
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}
@Override
public void notify(StatusChangeEvent statusChangeEvent) {
// ...
// 按需注册
instanceInfoReplicator.onDemandUpdate();
}
};
// 注册监听器 statusChangeListener
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
// 定时任务启动
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
按需注册 主要分两部分
- 启动一个定时任务
instanceInfoReplicator
,定时刷新 服务实例 的信息和检查 应用状态 的变化,在 服务实例 信息发生变化的情况下向 Eureka Server 重新发起注册 - 注册一个监听器
statusChangeListener
,在 应用状态 发生变化向 Eureka Server 重新发起注册
定时任务的执行逻辑由 InstanceInfoReplicator#run
提供
public void run() {
try {
// 刷新实例信息
discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
// 如果发生更改,则重新注册
discoveryClient.register();
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
// 周期执行
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
监听器 statusChangeListener
监听到事件 StatusChangeEvent,调用 instanceInfoReplicator#onDemandUpdate
public boolean onDemandUpdate() {
if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
if (!scheduler.isShutdown()) {
scheduler.submit(new Runnable() {
@Override
public void run() {
// ...
InstanceInfoReplicator.this.run();
}
});
return true;
} else {
// ...
}
} else {
// ...
}
}
最终也是委托给了 InstanceInfoReplicator#run
服务下线
服务下线 交由 Spring 生命周期管理,DiscoveryClient 对象销毁时会调用 unregister
方法进行服务下线,调用 url
为 apps/${APP_NAME}/${INSTANCE_INFO_ID}
,传递参数为服务名和服务实例 id
,HTTP 方法为 delete
总结
本章节结合部分源码重点解读了 DiscoveryClient 类,该类主要的业务逻辑:注册表信息拉取、服务注册、心跳续约、按需注册 等都在 构造方法 中实现,因此当我们引入对应的依赖并指定对应的 Eureka Server 时,就会注册对应的服务到 注册中心
上一篇:【源码】Spring Cloud —— Eureka Client 1 核心组件
参考
《Spring Cloud 微服务架构进阶》 —— 朱荣鑫 张天 黄迪璇