注册中心 Eureka 源码解析 —— 应用实例注册发现 (三)之下线

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 注册中心 Eureka 源码解析 —— 应用实例注册发现 (三)之下线

本文主要基于 Eureka 1.8.X 版本

    1. 概述
    1. Eureka-Client 发起下线
    1. Eureka-Server 接收下线
  • 3.1 接收下线请求
  • 3.2 下线应用实例信息
    1. 彩蛋

1. 概述

本文主要分享 Eureka-Client 向 Eureka-Server 下线应用实例的过程

FROM 《深度剖析服务发现组件Netflix Eureka》 二次编辑
注册中心 Eureka 源码解析 —— 应用实例注册发现 (三)之下线

  • 蓝框部分,为本文重点。
  • 蓝框部分,Eureka-Server 集群间复制注册的应用实例信息,不在本文内容范畴。

推荐 Spring Cloud 书籍

  • 请支持正版。下载盗版,等于主动编写低级 BUG 。
  • 程序猿DD —— 《Spring Cloud微服务实战》
  • 周立 —— 《Spring Cloud与Docker微服务架构实战》

推荐 Spring Cloud 视频

  • Java 微服务实践 - Spring Boot
  • Java 微服务实践 - Spring Cloud
  • Java 微服务实践 - Spring Boot / Spring Cloud

2. Eureka-Client 发起下线

应用实例关闭时,Eureka-Client 向 Eureka-Server 发起下线应用实例。需要满足如下条件才可发起:

  • 配置  eureka.registration.enabled = true ,应用实例开启注册开关。默认为  false 。
  • 配置  eureka.shouldUnregisterOnShutdown = true ,应用实例开启关闭时下线开关。默认为  true 。

实现代码如下:


// DiscoveryClient.java
public synchronized void shutdown() {

    // ... 省略无关代码

    // If APPINFO was registered
    if (applicationInfoManager != null
         && clientConfig.shouldRegisterWithEureka() // eureka.registration.enabled = true
         && clientConfig.shouldUnregisterOnShutdown()) { // eureka.shouldUnregisterOnShutdown = true
        applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
        unregister();
    }
}
  • 调用 `ApplicationInfoManager#setInstanceStatus(...)` 方法,设置应用实例为关闭( DOWN )。
  • 调用 `#unregister()` 方法,实现代码如下:
    // DiscoveryClient.java
    void unregister() {
       // It can be null if shouldRegisterWithEureka == false
       if(eurekaTransport != null && eurekaTransport.registrationClient != null) {
           try {
               logger.info("Unregistering ...");
               EurekaHttpResponseVoid httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());
               logger.info(PREFIX + appPathIdentifier + " - deregister  status: " + httpResponse.getStatusCode());
           } catch (Exception e) {
               logger.error(PREFIX + appPathIdentifier + " - de-registration failed" + e.getMessage(), e);
           }
       }
    }
    
    

    // AbstractJerseyEurekaHttpClient.java
    @Override
    public EurekaHttpResponseVoid cancel(String appName, String id) {
       String urlPath = “apps/“ + appName + ‘/‘ + id;
       ClientResponse response = null;
       try {
           Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
           addExtraHeaders(resourceBuilder);
           response = resourceBuilder.delete(ClientResponse.class);
           return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
       } finally {
           if (logger.isDebugEnabled()) {
               logger.debug(“Jersey HTTP DELETE {}/{}; statusCode={}”, serviceUrl, urlPath, response == null ? “N/A” : response.getStatus());
           }
           if (response != null) {
               response.close();
           }
       }
    }

      - 调用 `AbstractJerseyEurekaHttpClient#cancel(...)` 方法,`DELETE` 请求 Eureka-Server 的 `apps/${APP_NAME}/${INSTANCE_INFO_ID}` 接口,实现应用实例信息的下线。

      调用  #unregister() 方法,实现代码如下:

      3. Eureka-Server 接收下线

      3.1 接收下线请求

      com.netflix.eureka.resources.InstanceResource,处理单个应用实例信息的请求操作的 Resource ( Controller )。

      下线应用实例信息的请求,映射  InstanceResource#cancelLease() 方法,实现代码如下:

      
      @DELETE
      public Response cancelLease(
             @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
         // 下线
         boolean isSuccess = registry.cancel(app.getName(), id, "true".equals(isReplication));
      
         if (isSuccess) { // 下线成功
             logger.debug("Found (Cancel): " + app.getName() + " - " + id);
             return Response.ok().build();
         } else { // 下线成功
             logger.info("Not Found (Cancel): " + app.getName() + " - " + id);
             return Response.status(Status.NOT_FOUND).build();
         }
      }
      
    • 调用 `PeerAwareInstanceRegistryImpl#cancel(...)` 方法,下线应用实例。实现代码如下:
        1: @Override
        2: public boolean cancel(final String appName, final String id,
        3:                       final boolean isReplication) {
        4:     if (super.cancel(appName, id, isReplication)) { // 下线
        5:         // Eureka-Server 复制
        6:         replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
        7:         // 减少 `numberOfRenewsPerMinThreshold` 、`expectedNumberOfRenewsPerMin`
        8:         synchronized (lock) {
        9:             if (this.expectedNumberOfRenewsPerMin  0) {
       10:                 // Since the client wants to cancel it, reduce the threshold (1 for 30 seconds, 2 for a minute)
       11:                 this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin - 2;
       12:                 this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
       13:             }
       14:         }
       15:         return true;
       16:     }
       17:     return false;
       18: }
      
        - 第 4 行 :调用父类 `AbstractInstanceRegistry#cancel(...)` 方法,下线应用实例信息。 - 第 6 行 :Eureka-Server 复制下线操作,在 《Eureka 源码解析 —— Eureka-Server 集群同步》 有详细解析。 - 第 7 至 14 行 :减少 `numberOfRenewsPerMinThreshold` 、`expectedNumberOfRenewsPerMin`,自我保护机制相关,在 《Eureka 源码解析 —— 应用实例注册发现(四)之自我保护机制》 有详细解析。

        3.2 下线应用实例信息

        调用  AbstractInstanceRegistry#cancel(...) 方法,下线应用实例信息,实现代码如下:

        
          1: @Override
          2: public boolean cancel(String appName, String id, boolean isReplication) {
          3:     return internalCancel(appName, id, isReplication);
          4: }
          5: 
          6: protected boolean internalCancel(String appName, String id, boolean isReplication) {
          7:     try {
          8:         // 获得读锁
          9:         read.lock();
         10:         // 增加 取消注册次数 到 监控
         11:         CANCEL.increment(isReplication);
         12:         // 移除 租约映射
         13:         MapString, LeaseInstanceInfo gMap = registry.get(appName);
         14:         LeaseInstanceInfo leaseToCancel = null;
         15:         if (gMap != null) {
         16:             leaseToCancel = gMap.remove(id);
         17:         }
         18:         // 添加到 最近取消注册的调试队列
         19:         synchronized (recentCanceledQueue) {
         20:             recentCanceledQueue.add(new PairLong, String(System.currentTimeMillis(), appName + "(" + id + ")"));
         21:         }
         22:         // 移除 应用实例覆盖状态映射
         23:         InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
         24:         if (instanceStatus != null) {
         25:             logger.debug("Removed instance id {} from the overridden map which has value {}", id, instanceStatus.name());
         26:         }
         27:         // 租约不存在
         28:         if (leaseToCancel == null) {
         29:             CANCEL_NOT_FOUND.increment(isReplication); // 添加 取消注册不存在 到 监控
         30:             logger.warn("DS: Registry: cancel failed because Lease is not registered for: {}/{}", appName, id);
         31:             return false; // 失败
         32:         } else {
         33:             // 设置 租约的取消注册时间戳
         34:             leaseToCancel.cancel();
         35:             // 添加到 最近租约变更记录队列
         36:             InstanceInfo instanceInfo = leaseToCancel.getHolder();
         37:             String vip = null;
         38:             String svip = null;
         39:             if (instanceInfo != null) {
         40:                 instanceInfo.setActionType(ActionType.DELETED);
         41:                 recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
         42:                 instanceInfo.setLastUpdatedTimestamp();
         43:                 vip = instanceInfo.getVIPAddress();
         44:                 svip = instanceInfo.getSecureVipAddress();
         45:             }
         46:             // 设置 响应缓存 过期
         47:             invalidateCache(appName, vip, svip);
         48:             logger.info("Cancelled instance {}/{} (replication={})", appName, id, isReplication);
         49:             return true; // 成功
         50:         }
         51:     } finally {
         52:         // 释放锁
         53:         read.unlock();
         54:     }
         55: }
        
      • 第 9 行 :获取读锁。在 《Eureka源码解析 —— 应用实例注册发现 (九)之岁月是把萌萌的读写锁》 详细解析。
      • 第 10 至 11 行 :增加下线次数到监控。配合 Netflix Servo 实现监控信息采集。
      • 第 12 至 17 行 :移除租约映射( `registry` )。
      • 第 18 至 21 行 :添加到最近下线的**调试**队列( `recentCanceledQueue` ),用于 Eureka-Server 运维界面的显示,无实际业务逻辑使用。实现代码如下:
        /**
        * 最近取消注册的调试队列
        * key :添加时的时间戳
        * value :字符串 = 应用名(应用实例信息编号)
        */
        private final CircularQueuePairLong, String recentCanceledQueue;
        
      • 第 22 至 26 行 :移除应用实例覆盖状态映射。在《应用实例注册发现 (八)之覆盖状态》详细解析。
      • 第 27 至 31 行 :租约不存在,返回下线失败( `false` )。
      • 第 34 行 :调用 `Lease#cancel()` 方法,取消租约。实现代码如下:
        // Lease.java
        public void cancel() {
           if (evictionTimestamp = 0) {
               evictionTimestamp = System.currentTimeMillis();
           }
        }
        
      • 第 35 至 45 行 :设置应用实例信息的**操作类型为添加**,并添加到最近租约变更记录队列( `recentlyChangedQueue` )。`recentlyChangedQueue` 用于注册信息的**增量**获取,在《应用实例注册发现 (七)之增量获取》详细解析。实现代码如下:
        /**
        * 最近租约变更记录队列
        */
        private ConcurrentLinkedQueueRecentlyChangedItem recentlyChangedQueue = new ConcurrentLinkedQueueRecentlyChangedItem();
        
      • 第 47 行 :设置响应缓存( ResponseCache )过期,在《Eureka 源码解析 —— 应用实例注册发现 (六)之全量获取》详细解析。
      • 第 49 行 :返回下线失败( `false` )。
      • 第 53 行 :释放锁。
      • 第 10 至 11 行 :增加下线次数到监控。配合 Netflix Servo 实现监控信息采集。

        第 18 至 21 行 :添加到最近下线的调试队列(  recentCanceledQueue ),用于 Eureka-Server 运维界面的显示,无实际业务逻辑使用。实现代码如下:

        第 27 至 31 行 :租约不存在,返回下线失败(  false )。

        
        // Lease.java
        public void cancel() {
           if (evictionTimestamp = 0) {
               evictionTimestamp = System.currentTimeMillis();
           }
        }
        

        第 35 至 45 行 :设置应用实例信息的操作类型为添加,并添加到最近租约变更记录队列(  recentlyChangedQueue )。 recentlyChangedQueue 用于注册信息的增量获取,在《应用实例注册发现 (七)之增量获取》详细解析。实现代码如下:

        第 49 行 :返回下线失败(  false )。

        666. 彩蛋

        水更一篇,下一篇租约过期!走起。

        胖友,分享我的公众号( 芋道源码 ) 给你的胖友可好?

  • 本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> 注册中心 Eureka 源码解析 —— 应用实例注册发现 (三)之下线


     上一篇
    注册中心 Eureka 源码解析 —— 应用实例注册发现 (四)之自我保护机制 注册中心 Eureka 源码解析 —— 应用实例注册发现 (四)之自我保护机制
    中文详细注释的开源项目 Java 并发源码合集 RocketMQ 源码合集 Sharding-JDBC 源码解析合集 Spring MVC 和 Security 源码合集 MyCAT 源码解析合集 本文主要基于 Eureka 1.8.X
    2021-04-05
    下一篇 
    注册中心 Eureka 源码解析 —— 应用实例注册发现 (二)之续租 注册中心 Eureka 源码解析 —— 应用实例注册发现 (二)之续租
    Java 并发源码合集 RocketMQ 源码合集 Sharding-JDBC 源码解析合集 Spring MVC 和 Security 源码合集 MyCAT 源码解析合集 本文主要基于 Eureka 1.8.X 版本 概述
    2021-04-05