⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Eureka/instance-registry-cancel/ 「芋道源码」欢迎转载,保留摘要,谢谢!

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


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

1. 概述

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

FROM 《深度剖析服务发现组件Netflix Eureka》 二次编辑

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

推荐 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: }

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 行 :释放锁。

666. 彩蛋

知识星球

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

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

文章目录
  1. 1. 1. 概述
  2. 2. 2. Eureka-Client 发起下线
  3. 3. 3. Eureka-Server 接收下线
    1. 3.1. 3.1 接收下线请求
    2. 3.2. 3.2 下线应用实例信息
  4. 4. 666. 彩蛋