2023-09-17  阅读(0)
原文作者:carl.zhao 原文地址: https://carlzone.blog.csdn.net/article/details/112254151

下面是官方提供的 Eureka 架构图:

202309172251303571.png

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-serviceorder-service
  • i-00000000 :微服务应用的名称下具体的服务实例 ID,比如 user-service 下面有 8 台机制实例。i-00000000 就是其中一台唯一的 ID 值

2、Eureka Server 服务下线

Eureka Server 处理 Eureka Client 服务下线时序图:

202309172251307542.png
首先 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 服务下线的时序图:

202309172251312193.png
上面的时序图有两点比较核心:

  • 调用当前 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] ,回复【面试题】 即可免费领取。

阅读全文