下面是官方提供的 Eureka 架构图:
1、Eureka Client 服务下线
当 Eureka Client 服务关闭之前会调用 DiscoveryClient#shutdown
方法。因为这个方法上面标注了 @PreDestroy
在对象销毁之前这个方法就会被调用。
@PreDestroy
@Override
public synchronized void shutdown() {
if (isShutdown.compareAndSet(false, true)) {
if (statusChangeListener != null && applicationInfoManager != null) {
applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
}
cancelScheduledTasks();
// If APPINFO was registered
if (applicationInfoManager != null
&& clientConfig.shouldRegisterWithEureka()
&& clientConfig.shouldUnregisterOnShutdown()) {
applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
unregister();
}
if (eurekaTransport != null) {
eurekaTransport.shutdown();
}
heartbeatStalenessMonitor.shutdown();
registryStalenessMonitor.shutdown();
Monitors.unregisterObject(this);
}
}
- 首先把 AtomicBoolean 类型的
isShutdown
设置成 true。 - 接着会解除掉服务应用管理器(
ApplicationInfoManager
) 中注册的状态监听器(StatusChangeListener
) - 然后会调用
cancelScheduledTasks
释放线程池资源,包括:Eureka Server 间的数据同步线程与线程池、心跳处理的线程池与线程以及注册表定时刷新的线程池与线程。 - 设置当前服务应用的状态为下线,并且在
DiscoveryClient#unregister
方法当中调用EurekaHttpClient#cancel
进行服务下线 - 释放
EurekaTransport
远程调用里面的 HTTP 连接资源 - 最后释放一些监控资源
在调用服务下线的时候请求最终会调用到 AbstractJersey2EurekaHttpClient#cancel发送类似于:http://localhost:8080/v2/apps/APPLICATION0/i-00000000
这样的 PUT 请求路径到注册中心。
http://localhost:8080
:Eureka Server 的请求地址/v2/apps
:Restful 处理类 ApplicationsResource 上面的路径:@Path("/{version}/apps")
APPLICATION0
:微服务应用的名称,也可以是:user-service
,order-service
等i-00000000
:微服务应用的名称下具体的服务实例 ID,比如user-service
下面有 8 台机制实例。i-00000000
就是其中一台唯一的 ID 值
2、Eureka Server 服务下线
Eureka Server 处理 Eureka Client 服务下线时序图:
首先 ApplicationsResource#getApplicationResource
会来接收 Eureka Client 发送过来的服务下线请求,这个接口里面会返回 ApplicationResource
对象。然后会调用 ApplicationResource#getInstanceInfo
。因为 Eureka Client 发送的是 DELETE
请求,所以请求会转发到 InstanceResource
标注了 @DELETE
的方法 cancelLease 进行处理。
InstanceResource#cancelLease
@DELETE
public Response cancelLease(
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
try {
boolean isSuccess = registry.cancel(app.getName(), id,
"true".equals(isReplication));
if (isSuccess) {
return Response.ok().build();
} else {
return Response.status(Status.NOT_FOUND).build();
}
} catch (Throwable e) {
return Response.serverError().build();
}
}
这个方法里面的逻辑非常简单:
- 调用注册中心的服务下线功能
- 然后把服务下线的响应结果返回给 Eureka Client
3、注册中心下线服务
下面我们来看一下注册中心服务在服务下线做了哪些事情。下面是 Eureka Server 服务下线的时序图:
上面的时序图有两点比较核心:
- 调用当前 Eureka Server 的
AbstractInstanceRegistry#internalCancel
方法进行服务下线 - 如果是集群环境调用
PeerAwareInstanceRegistryImpl#replicateToPeers
把当前的服务实例的下线同步到其它 Eureka Servier 当中去(后续分析)。
那我们就来分析一下 AbstractInstanceRegistry#internalCancel
方法是如何进行服务下线的。
AbstractInstanceRegistry#internalCancel
protected boolean internalCancel(String appName, String id, boolean isReplication) {
read.lock();
try {
CANCEL.increment(isReplication);
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToCancel = null;
if (gMap != null) {
leaseToCancel = gMap.remove(id);
}
recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
if (instanceStatus != null) {
logger.debug("Removed instance id {} from the overridden map which has value {}", id, instanceStatus.name());
}
if (leaseToCancel == null) {
CANCEL_NOT_FOUND.increment(isReplication);
logger.warn("DS: Registry: cancel failed because Lease is not registered for: {}/{}", appName, id);
return false;
} else {
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();
}
invalidateCache(appName, vip, svip);
logger.info("Cancelled instance {}/{} (replication={})", appName, id, isReplication);
}
} finally {
read.unlock();
}
synchronized (lock) {
if (this.expectedNumberOfClientsSendingRenews > 0) {
// Since the client wants to cancel it, reduce the number of clients to send renews.
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;
updateRenewsPerMinThreshold();
}
}
return true;
}
EurekaMonitors#CANCEL
服务取消监控进行 +1- 根据微服务的名称获取到当前服务的注册列表
- 如果当前服务的注册列表不为空,根据传入的服务实例 ID 把它从注册列表当中移除
- 向
CircularQueue<Pair<Long, String>> recentCanceledQueue
列表当中添加当前服务实例信息( 注:这个队列信息暂时没有用到 ) - 如果移除的服务实例为空,
EurekaMonitors#CANCEL_NOT_FOUND
+1 并返回 false. - 调用
Lease<InstanceInfo>#cancel
进行下线,其实就是修改 Lease 的 registrationTimestamp 属性为当前系统时间 - 设置当前信息为删除状态,并添加到
ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue
当中。( 注:这个队列会在 Eureka Client 增量拉取注册表时使用 )
其实 Eureka Client 服务下线就是在注册服务中的 Map 当中把当前服务实例的信息删除就行了。
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。