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

1. 概述

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

FROM 《深度剖析服务发现组件Netflix 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 ..."); EurekaHttpResponse<Void> 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 EurekaHttpResponse<Void> 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} 接口,实现应用实例信息的下线。

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 行 :减少 numberOfRenewsPerMinThresholdexpectedNumberOfRenewsPerMin,自我保护机制相关,在 《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:         Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
 14:         Lease<InstanceInfo> leaseToCancel = null;
 15:         if (gMap != null) {
 16:             leaseToCancel = gMap.remove(id);
 17:         }
 18:         // 添加到 最近取消注册的调试队列
 19:         synchronized (recentCanceledQueue) {
 20:             recentCanceledQueue.add(new Pair<Long, 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 CircularQueue<Pair<Long, 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 ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue = new ConcurrentLinkedQueue<RecentlyChangedItem>();
  • 第 47 行 :设置响应缓存( ResponseCache )过期,在《Eureka 源码解析 —— 应用实例注册发现 (六)之全量获取》详细解析。
  • 第 49 行 :返回下线失败( false )。
  • 第 53 行 :释放锁。

原文发布于微信公众号 - 芋道源码(YunaiV)

原文发表时间:2018-05-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏代码拾遗

深入理解Spring MVC

使用Spring Boot和web,thymeleaf的starter来设置初始工程。xml配置如下:

12220
来自专栏蔡鹏的专栏

Dubbo API形式 简单入门

56330
来自专栏Kubernetes

Kubernetes GC in V1.3 源码分析

本文是对Kubernetes V1.3发布的新Garbage Collector模块的源码解读。实际上本文的是基于kubernetes v1.4的代码进行分析的...

422110
来自专栏玩转JavaEE

Spring Cloud自定义Hystrix请求命令

在上篇文章中,我们介绍了断路器Hystrix的一个简单使用,主要是通过注解来实现断路器的功能的,不过对于Hystrix的使用,除了注解,我们也可以使用继承类的方...

41230
来自专栏C/C++基础

CMake简介及使用实例

CMake是一个跨平台的建构系统的工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的构建文档makefile或者project文件,描...

20320
来自专栏coolblog.xyz技术专栏

Spring MVC 原理探秘 - 容器的创建过程

14730
来自专栏IT笔记

SpringBoot开发案例之整合Dubbo提供者(一)

既然是开发案例,显然不会扯那么多老婆舌,有不清楚这两个东东的请自行百度。 ? 0.jpg 开发环境 JDK1.7、Maven、Eclipse、SpringBoo...

451130
来自专栏Ryan Miao

使用dropwizard(4)-加入测试-jacoco代码覆盖率

前言 dropwizard提供了一个简单的测试框架。这里简单集成并加入jacoco测试。 Demo source https://github.com/Rya...

42580
来自专栏小灰灰

Spring之RestTemplate中级使用篇

前面一篇介绍了如何使用RestTemplate发起post和get请求,然而也只能满足一些基本的场景,对于一些特殊的如需要设置请求头,添加认证信息等场景,却没有...

30210
来自专栏Java 源码分析

SpringBoot 笔记 ( 四 ):web 开发

SpringBoot 笔记 (四): web 开发 1、SpringBoot对静态资源的映射规则 @ConfigurationProperties(prefix...

71260

扫码关注云+社区

领取腾讯云代金券