一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情
图解+源码讲解 Eureka Client 下线流程分析
如果把生活比喻为创作的意境,那么阅读就像阳光 —— 池莉 相关文章
eureka-server 项目结构分析
图解+源码讲解 Eureka Server 启动流程分析
图解+源码讲解 Eureka Client 启动流程分析
图解+源码讲解 Eureka Server 注册表缓存逻辑
图解+源码讲解 Eureka Client 拉取注册表流程
图解+源码讲解 Eureka Client 服务注册流程
图解+源码讲解 Eureka Client 心跳机制流程
图解+源码讲解 Eureka Client 下线流程分析
图解+源码讲解 Eureka Server 服务剔除逻辑
图解+源码讲解 Eureka Server 集群注册表同步机制
核心流程图
从哪里开始分析
DiscoveryClient 这个类其实就是客户端相关的类,按理说什么注册的方法啊,下线的方法啊以及心跳的方法都在这里才对,所以我们看看这个类的主要方法
核心方法逻辑
shatdown 下线方法
这个方法是实例下线的核心方法,里面包含了一些方法、比如取消监听器操作、取消调度任务操作、将实例设置为下线方法、取消注册后续的方法、处理后续不需要的资源、取消注册监控操作
public synchronized void shutdown() {
if (isShutdown.compareAndSet(false, true)) {
logger.info("Shutting down DiscoveryClient ...");
if (statusChangeListener != null && applicationInfoManager != null) {
applicationInfoManager.
unregisterStatusChangeListener(statusChangeListener.getId());
}
/**
* 取消调度任务比如心跳、注册表缓存
*/
cancelScheduledTasks();
/**
* 如果应用程序被注册过
* clientConfig.shouldRegisterWithEureka() &&
clientConfig.shouldUnregisterOnShutdown() 默认都为true
*/
if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka()
&& clientConfig.shouldUnregisterOnShutdown()) {
/**
* 设置实例状态为下线
*/
applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
/**
* 发送下线逻辑给eureka-server端提示当前实例下线处理
*/
unregister();
}
/**
* 处理后续不需要的资源
*/
if (eurekaTransport != null) {
eurekaTransport.shutdown();
}
heartbeatStalenessMonitor.shutdown();
registryStalenessMonitor.shutdown();
/**
* 取消监控注册
*/
Monitors.unregisterObject(this);
logger.info("Completed shut down of DiscoveryClient");
}
}
取消调度任务 cancelScheduledTasks
这里面取消调度任务,其实就是在客户端初始化操作的时候初始化的那些调度任务,比如心跳任务、缓存操作任务等等
private void cancelScheduledTasks() {
// 实例复制停止
if (instanceInfoReplicator != null) {
instanceInfoReplicator.stop();
}
// 心跳执行器关闭
if (heartbeatExecutor != null) {
heartbeatExecutor.shutdownNow();
}
// 缓存执行器关闭
if (cacheRefreshExecutor != null) {
cacheRefreshExecutor.shutdownNow();
}
// 调度执行器关闭
if (scheduler != null) {
scheduler.shutdownNow();
}
// 取消缓存刷新任务
if (cacheRefreshTask != null) {
cacheRefreshTask.cancel();
}
// 取消心跳任务
if (heartbeatTask != null) {
heartbeatTask.cancel();
}
}
取消注册逻辑
如果实例不为空的情况下,先将实例的状态设置为下线状态,之后进行unregister()方法操作,通过eurekaTransport.registrationClient 客户端的cancel 方法进行实例下架
void unregister() {
// It can be null if shouldRegisterWithEureka == false
if (eurekaTransport != null && eurekaTransport.registrationClient != null) {
logger.info("Unregistering ...");
EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.
cancel(instanceInfo.getAppName(), instanceInfo.getId());
}
}
通过 AbstractJersey2EurekaHttpClient 的cancel操作进行实例下线,构造一个发送请求去 eureka-core 项目里面去找InstanceResource 这个类里面的 cancelLease方法这个方法是真正的下线请求逻辑
@DELETE
public Response cancelLease(@HeaderParam(PeerEurekaNode.HEADER_REPLICATION)
String isReplication) {
registry.cancel(app.getName(), id, "true".equals(isReplication));
return Response.ok().build();
}
执行的是注册表中的PeerAwareInstanceRegistryImpl 的 cancel 方法,之后同步到其他的服务节点中,都需要下线当前的实例
@Override
public boolean cancel(final String appName, final String id,
final boolean isReplication) {
// 取消实例操作
if (super.cancel(appName, id, isReplication)) {
// 集群同步方法
replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
return true;
}
return false;
}
执行注册表中 internalCancel 方法,这里面包含了很多的核心方法
从本地的读写缓存中移除当前下线实例信息
如果本地的读写缓存不为空的情况下,也就是获取到当前实例的情况下,那么就从读写缓存中将其实例移除
public boolean cancel(String appName, String id, boolean isReplication) {
return internalCancel(appName, id, isReplication);
}
protected boolean internalCancel(String appName, String id, boolean isReplication) {
CANCEL.increment(isReplication);
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToCancel = null;
if (gMap != null) {
// 将服务实例从eureka server的map结构的注册表中移除掉
leaseToCancel = gMap.remove(id);
}
......
return true;
}
将下线的实例放入最近下线队列
将最近下线的实例放入到最近下线的队列里面
/**
* 最近有实例下线的队列
*/
recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(),
appName + "(" + id + ")"));
/**
* 调用了Lease的cancel()方法,里面保存了一个evictionTimestamp
* 就是服务实例被清理掉,服务实例下线的时间戳
*/
leaseToCancel.cancel();
添加到最近改变的队列
将当前的实例设置为下线操作,之后放入到最近改变队列里面
InstanceInfo instanceInfo = leaseToCancel.getHolder();
String vip = null;
String svip = null;
if (instanceInfo != null) {
// 设置实例的操作类型是删除操作
instanceInfo.setActionType(ActionType.DELETED);
// 实例下线进入改变队列
recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
instanceInfo.setLastUpdatedTimestamp();
vip = instanceInfo.getVIPAddress();
svip = instanceInfo.getSecureVipAddress();
}
无效本地缓存
/**
* 实例下线无效本地缓存,中清理掉
* 就是有一个定时的任务,每隔30秒,将readWriteCacheMap和readOnlyCacheMap进行一个同步
* 下次所有的eureka client来拉取增量注册表的时候,都会发现readOnlyCacheMap里没有,
* 会找readWriteCacheMap也会发现没有,
* 然后就会从注册表里抓取增量注册表,此时就会将上面那个recentCHangedQuuee中的记录返回
*/
invalidateCache(appName, vip, svip);
更新期待的心跳次数
// 下线期待的每次心跳操作
synchronized (lock) {
if (this.expectedNumberOfClientsSendingRenews > 0) {
this.expectedNumberOfClientsSendingRenews =
this.expectedNumberOfClientsSendingRenews - 1;
updateRenewsPerMinThreshold();
}
}
protected void updateRenewsPerMinThreshold() {
// serverConfig.getExpectedClientRenewalIntervalSeconds() 默认 30s
// 60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds() == 2、
// serverConfig.getRenewalPercentThreshold())
// 2 * 0.85 也就是期待一分钟内 实例数量*2*0.85个心跳
this.numberOfRenewsPerMinThreshold = (int)
(this.expectedNumberOfClientsSendingRenews *
(60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
* serverConfig.getRenewalPercentThreshold());
}
小结
- 服务下线操作
- 从本地缓存中移除当前实例
- 添加到最近的下线队列以及改变队列
- 无效本地缓存
- 服务摘除了那么就需要进行每分钟实例心跳数量的更新,下掉一个实例那么就减少两次的心跳