图解+源码讲解 Eureka Client 下线流程分析

932 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

图解+源码讲解 Eureka Client 下线流程分析

如果把生活比喻为创作的意境,那么阅读就像阳光 —— 池莉 相关文章
eureka-server 项目结构分析
图解+源码讲解 Eureka Server 启动流程分析
图解+源码讲解 Eureka Client 启动流程分析
图解+源码讲解 Eureka Server 注册表缓存逻辑
图解+源码讲解 Eureka Client 拉取注册表流程
图解+源码讲解 Eureka Client 服务注册流程
图解+源码讲解 Eureka Client 心跳机制流程
图解+源码讲解 Eureka Client 下线流程分析
图解+源码讲解 Eureka Server 服务剔除逻辑
图解+源码讲解 Eureka Server 集群注册表同步机制

核心流程图

image.png

从哪里开始分析

    DiscoveryClient 这个类其实就是客户端相关的类,按理说什么注册的方法啊,下线的方法啊以及心跳的方法都在这里才对,所以我们看看这个类的主要方法
image.png

核心方法逻辑

shatdown 下线方法

    这个方法是实例下线的核心方法,里面包含了一些方法、比如取消监听器操作、取消调度任务操作、将实例设置为下线方法、取消注册后续的方法、处理后续不需要的资源、取消注册监控操作

public synchronized void shutdown() {
    if (isShutdown.compareAndSet(false, true)) {
        logger.info("Shutting down DiscoveryClient ...");
    if (statusChangeListener != null && applicationInfoManager != null) {
        applicationInfoManager.
            unregisterStatusChangeListener(statusChangeListener.getId());
    }
    /**
     * 取消调度任务比如心跳、注册表缓存
     */
    cancelScheduledTasks();
    /**
     * 如果应用程序被注册过
     * clientConfig.shouldRegisterWithEureka() &&
     clientConfig.shouldUnregisterOnShutdown() 默认都为true
     */
    if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka() 
        && clientConfig.shouldUnregisterOnShutdown()) {
        /**
         * 设置实例状态为下线
         */
        applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
        /**
         * 发送下线逻辑给eureka-server端提示当前实例下线处理
         */
        unregister();
    }
    /**
     * 处理后续不需要的资源
     */
    if (eurekaTransport != null) {
        eurekaTransport.shutdown();
    }
    heartbeatStalenessMonitor.shutdown();
    registryStalenessMonitor.shutdown();
    /**
     * 取消监控注册
     */
    Monitors.unregisterObject(this);
    logger.info("Completed shut down of DiscoveryClient");
    }
}

取消调度任务 cancelScheduledTasks

    这里面取消调度任务,其实就是在客户端初始化操作的时候初始化的那些调度任务,比如心跳任务、缓存操作任务等等

private void cancelScheduledTasks() {
    // 实例复制停止
    if (instanceInfoReplicator != null) {
        instanceInfoReplicator.stop();
    }
   // 心跳执行器关闭
    if (heartbeatExecutor != null) {
        heartbeatExecutor.shutdownNow();
    }
   // 缓存执行器关闭
    if (cacheRefreshExecutor != null) {
        cacheRefreshExecutor.shutdownNow();
    }
    // 调度执行器关闭
    if (scheduler != null) {
        scheduler.shutdownNow();
    }
     // 取消缓存刷新任务
    if (cacheRefreshTask != null) {
        cacheRefreshTask.cancel();
    }
     // 取消心跳任务
    if (heartbeatTask != null) {
        heartbeatTask.cancel();
    }
}

取消注册逻辑

    如果实例不为空的情况下,先将实例的状态设置为下线状态,之后进行unregister()方法操作,通过eurekaTransport.registrationClient 客户端的cancel 方法进行实例下架

void unregister() {
    // It can be null if shouldRegisterWithEureka == false
    if (eurekaTransport != null && eurekaTransport.registrationClient != null) {
            logger.info("Unregistering ...");
    EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.
        cancel(instanceInfo.getAppName(), instanceInfo.getId());
    }
}

    通过 AbstractJersey2EurekaHttpClient 的cancel操作进行实例下线,构造一个发送请求去 eureka-core 项目里面去找InstanceResource 这个类里面的 cancelLease方法这个方法是真正的下线请求逻辑

@DELETE
public Response cancelLease(@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) 
                        String isReplication) {
  registry.cancel(app.getName(), id, "true".equals(isReplication));
  return Response.ok().build();
}

    执行的是注册表中的PeerAwareInstanceRegistryImpl 的 cancel 方法,之后同步到其他的服务节点中,都需要下线当前的实例

@Override
public boolean cancel(final String appName, final String id,
                      final boolean isReplication) {
    // 取消实例操作
    if (super.cancel(appName, id, isReplication)) {
        // 集群同步方法
        replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);

        return true;
    }
    return false;
}

    执行注册表中 internalCancel 方法,这里面包含了很多的核心方法

从本地的读写缓存中移除当前下线实例信息

    如果本地的读写缓存不为空的情况下,也就是获取到当前实例的情况下,那么就从读写缓存中将其实例移除

public boolean cancel(String appName, String id, boolean isReplication) {
    return internalCancel(appName, id, isReplication);
}

protected boolean internalCancel(String appName, String id, boolean isReplication) {
    CANCEL.increment(isReplication);
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
    Lease<InstanceInfo> leaseToCancel = null;
    if (gMap != null) {
       //  将服务实例从eureka server的map结构的注册表中移除掉
        leaseToCancel = gMap.remove(id);
    }
    ......
    return true;
}

将下线的实例放入最近下线队列

    将最近下线的实例放入到最近下线的队列里面

  /**
     * 最近有实例下线的队列
     */
    recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), 
                                                   appName + "(" + id + ")"));
    /**
     * 调用了Lease的cancel()方法,里面保存了一个evictionTimestamp
     * 就是服务实例被清理掉,服务实例下线的时间戳
     */
    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();
}

无效本地缓存

/**
 * 实例下线无效本地缓存,中清理掉
 * 就是有一个定时的任务,每隔30秒,将readWriteCacheMap和readOnlyCacheMap进行一个同步
 * 下次所有的eureka client来拉取增量注册表的时候,都会发现readOnlyCacheMap里没有,
 * 会找readWriteCacheMap也会发现没有,
 * 然后就会从注册表里抓取增量注册表,此时就会将上面那个recentCHangedQuuee中的记录返回
 */
invalidateCache(appName, vip, svip);

更新期待的心跳次数

// 下线期待的每次心跳操作
synchronized (lock) {
    if (this.expectedNumberOfClientsSendingRenews > 0) {
        this.expectedNumberOfClientsSendingRenews = 
            this.expectedNumberOfClientsSendingRenews - 1;
        updateRenewsPerMinThreshold();
    }
}
 protected void updateRenewsPerMinThreshold() {
        // serverConfig.getExpectedClientRenewalIntervalSeconds() 默认 30s
        // 60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds() == 2、
        // serverConfig.getRenewalPercentThreshold())
        // 2 * 0.85 也就是期待一分钟内 实例数量*2*0.85个心跳
        this.numberOfRenewsPerMinThreshold = (int)
                (this.expectedNumberOfClientsSendingRenews * 
                 (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
                 * serverConfig.getRenewalPercentThreshold());
 }

小结

  1. 服务下线操作
  2. 从本地缓存中移除当前实例
  3. 添加到最近的下线队列以及改变队列
  4. 无效本地缓存
  5. 服务摘除了那么就需要进行每分钟实例心跳数量的更新,下掉一个实例那么就减少两次的心跳