Eureka 源碼解析 —— 應用實例註冊發現(二)之續租

摘要: 原創出處 http://www.iocoder.cn/Eureka/instance-registry-renew/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!

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

  • 1. 概述
  • 2. Eureka-Client 發起續租

  • 3. Eureka-Server 接收續租
  • 1. 概述

    本文主要分享 Eureka-Client 向 Eureka-Server 續租應用實例的過程

    FROM 《深度剖析服務發現組件Netflix Eureka》 二次編輯

    • 藍框部分,爲本文重點。
    • 藍框部分,Eureka-Server 集羣間複製註冊的應用實例信息,不在本文內容範疇。

    推薦 Spring Cloud 書籍

    2. Eureka-Client 發起續租

    Eureka-Client 向 Eureka-Server 發起註冊應用實例成功後獲得租約 ( Lease )。
    Eureka-Client 固定間隔向 Eureka-Server 發起續租( renew ),避免租約過期。

    默認情況下,租約有效期爲 90 秒,續租頻率爲 30 秒。兩者比例爲 1 : 3 ,保證在網絡異常等情況下,有三次重試的機會。

    2.1 初始化定時任務

    Eureka-Client 在初始化過程中,創建心跳線程,固定間隔向 Eureka-Server 發起續租( renew )。實現代碼如下:


    // DiscoveryClient.java
    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
    Provider<BackupRegistry> backupRegistryProvider) {
    // ... 省略無關代碼
    scheduler = Executors.newScheduledThreadPool(2,
    new ThreadFactoryBuilder()
    .setNameFormat("DiscoveryClient-%d")
    .setDaemon(true)
    .build());
    heartbeatExecutor = new ThreadPoolExecutor(
    1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>(),
    new ThreadFactoryBuilder()
    .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
    .setDaemon(true)
    .build()
    ); // use direct handoff
    // ... 省略無關代碼
    // 【3.2.14】初始化定時任務
    initScheduledTasks();
    }
    private void initScheduledTasks() {
    // 向 Eureka-Server 心跳(續租)執行器
    if (clientConfig.shouldRegisterWithEureka()) {
    int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); // 續租頻率
    int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound(); //
    logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);
    // Heartbeat timer
    scheduler.schedule(
    new TimedSupervisorTask(
    "heartbeat",
    scheduler,
    heartbeatExecutor,
    renewalIntervalInSecs,
    TimeUnit.SECONDS,
    expBackOffBound,
    new HeartbeatThread()
    ),
    renewalIntervalInSecs, TimeUnit.SECONDS);
    // ... 省略無關代碼
    }
    // ... 省略無關代碼
    }
    • scheduler,定時任務服務,用於定時觸發心跳( 續租 )。細心如你,會發現任務提交的方式是 ScheduledExecutorService#schedule(...) 方法,只延遲執行一次心跳,說好的固定頻率執行心跳呢!!!答案在 「2.3 TimedSupervisorTask」 揭曉。
    • heartbeatExecutor,心跳任務執行線程池。爲什麼有 scheduler 的情況下,還有 heartbeatExecutor ???答案也在 「2.3 TimedSupervisorTask」 揭曉。
    • HeartbeatThread,心跳線程,在「2.2 TimedSupervisorTask」 詳細解析。

    2.2 HeartbeatThread

    com.netflix.discovery.DiscoveryClient.HeartbeatThread,心跳線程,實現執行 Eureka-Client 向 Eureka-Server 發起續租( renew )請求。實現代碼如下:


    // DiscoveryClient.java
    /*
    最後成功向 Eureka-Server 心跳時間戳
    */
    private volatile long lastSuccessfulHeartbeatTimestamp = -1;
    private class HeartbeatThread implements Runnable {
    public void run() {
    if (renew()) {
    lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
    }
    }
    }
    • 調用 #renew 方法,執行續租邏輯。實現代碼如下:

      // DiscoveryClient.java
      boolean renew() {
      EurekaHttpResponse<InstanceInfo> httpResponse;
      try {
      httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
      logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
      if (httpResponse.getStatusCode() == 404) {
      REREGISTER_COUNTER.increment();
      logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName());
      long timestamp = instanceInfo.setIsDirtyWithTime();
      // 發起註冊
      boolean success = register();
      if (success) {
      instanceInfo.unsetIsDirty(timestamp);
      }
      return success;
      }
      return httpResponse.getStatusCode() == 200;
      } catch (Throwable e) {
      logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e);
      return false;
      }
      }
      • 調用 AbstractJerseyEurekaHttpClient#sendHeartBeat(...) 方法,發起續租請求,實現代碼如下:

        // AbstractJerseyEurekaHttpClient.java
        @Override
        public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
        String urlPath = "apps/" + appName + '/' + id;
        ClientResponse response = null;
        try {
        WebResource webResource = jerseyClient.resource(serviceUrl)
        .path(urlPath)
        .queryParam("status", info.getStatus().toString())
        .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());
        if (overriddenStatus != null) {
        webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name());
        }
        Builder requestBuilder = webResource.getRequestBuilder();
        addExtraHeaders(requestBuilder);
        response = requestBuilder.put(ClientResponse.class);
        EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response));
        if (response.hasEntity()) {
        eurekaResponseBuilder.entity(response.getEntity(InstanceInfo.class));
        }
        return eurekaResponseBuilder.build();
        } finally {
        if (logger.isDebugEnabled()) {
        logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
        }
        if (response != null) {
        response.close();
        }
        }
        }
        • PUT 請求 Eureka-Server 的 apps/${APP_NAME}/${INSTANCE_INFO_ID} 接口,參數爲 statuslastDirtyTimestampoverriddenstatus,實現續租。

      • 調用 AbstractJerseyEurekaHttpClient#register(...) 方法,當 Eureka-Server 不存在租約時,重新發起註冊,在《Eureka 源碼解析 —— 應用實例註冊發現 (一)之註冊》有詳細解析。




    2.3 TimedSupervisorTask

    com.netflix.discovery.TimedSupervisorTask,監管定時任務的任務。

    A supervisor task that schedules subtasks while enforce a timeout.

    創建 TimedSupervisorTask 代碼如下:

    public class TimedSupervisorTask extends TimerTask {
    private final Counter timeoutCounter;
    private final Counter rejectedCounter;
    private final Counter throwableCounter;
    private final LongGauge threadPoolLevelGauge;
    /
    * 定時任務服務
    /
    private final ScheduledExecutorService scheduler;
    /
    執行子任務線程池
    /
    private final ThreadPoolExecutor executor;
    /
    子任務執行超時時間
    /
    private final long timeoutMillis;
    /
    子任務
    /
    private final Runnable task;
    /
    當前任子務執行頻率
    /
    private final AtomicLong delay;
    /
    最大子任務執行頻率
    子任務執行超時情況下使用
    /
    private final long maxDelay;
    public TimedSupervisorTask(String name, ScheduledExecutorService scheduler, ThreadPoolExecutor executor,
    int timeout, TimeUnit timeUnit, int expBackOffBound, Runnable task) {
    this.scheduler = scheduler;
    this.executor = executor;
    this.timeoutMillis = timeUnit.toMillis(timeout);
    this.task = task;
    this.delay = new AtomicLong(timeoutMillis);
    this.maxDelay = timeoutMillis expBackOffBound;
    // Initialize the counters and register.
    timeoutCounter = Monitors.newCounter("timeouts");
    rejectedCounter = Monitors.newCounter("rejectedExecutions");
    throwableCounter = Monitors.newCounter("throwables");
    threadPoolLevelGauge = new LongGauge(MonitorConfig.builder("threadPoolUsed").build());
    Monitors.registerObject(name, this);
    }
    }

    • scheduler ,定時任務服務,用於定時【發起】子任務。
    • executor ,執行子任務線程池,用於【提交】子任務執行。
    • task ,子任務。
    • timeoutMillis ,子任務執行超時時間,單位:毫秒。
    • delay ,當前子任務執行頻率,單位:毫秒。值等於 timeout 參數。
    • maxDelay最大子任務執行頻率,單位:毫秒。值等於 timeout * expBackOffBound 參數。

    • scheduler 初始化延遲執行 TimedSupervisorTask 。
    • TimedSupervisorTask 執行時,提交 taskexecutor 執行任務。
      • task 執行正常,TimedSupervisorTask 再次提交自己scheduler 延遲 timeoutMillis 執行。
      • task 執行超時,重新計算延遲時間( 不允許超過 maxDelay ),再次提交自己scheduler 延遲執行。

    實現代碼如下:

    // TimedSupervisorTask.java
    1: @Override
    2: public void run() {
    3: Future<?> future = null;
    4: try {
    5: // 提交 任務
    6: future = executor.submit(task);
    7: //
    8: threadPoolLevelGauge.set((long) executor.getActiveCount());
    9: // 等待任務 執行完成 或 超時
    10: future.get(timeoutMillis, TimeUnit.MILLISECONDS); // block until done or timeout
    11: // 設置 下一次任務執行頻率
    12: delay.set(timeoutMillis);
    13: //
    14: threadPoolLevelGauge.set((long) executor.getActiveCount());
    15: } catch (TimeoutException e) {
    16: logger.error("task supervisor timed out", e);
    17: timeoutCounter.increment(); //
    18:
    19: // 設置 下一次任務執行頻率
    20: long currentDelay = delay.get();
    21: long newDelay = Math.min(maxDelay, currentDelay * 2);
    22: delay.compareAndSet(currentDelay, newDelay);
    23:
    24: } catch (RejectedExecutionException e) {
    25: if (executor.isShutdown() || scheduler.isShutdown()) {
    26: logger.warn("task supervisor shutting down, reject the task", e);
    27: } else {
    28: logger.error("task supervisor rejected the task", e);
    29: }
    30:
    31: rejectedCounter.increment(); //
    32: } catch (Throwable e) {
    33: if (executor.isShutdown() || scheduler.isShutdown()) {
    34: logger.warn("task supervisor shutting down, can't accept the task");
    35: } else {
    36: logger.error("task supervisor threw an exception", e);
    37: }
    38:
    39: throwableCounter.increment(); //
    40: } finally {
    41: // 取消 未完成的任務
    42: if (future != null) {
    43: future.cancel(true);
    44: }
    45:
    46: // 調度 下次任務
    47: if (!scheduler.isShutdown()) {
    48: scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
    49: }
    50: }
    51: }

    • 第 5 至 6 行 :提交子任務 task 到執行子任務線程池 executor
    • 第 9 至 10 行 :等待子任務 task 執行完成或執行超時。
    • 第 11 至 12 行 :子任務 task 執行完成,設置下一次執行延遲 delay
    • 第 19 至 22 行 :子任務 task 執行超時,重新計算下一次執行延遲 delay 。計算公式爲 Math.min(maxDelay, currentDelay * 2) 。如果多次超時,超時時間不斷乘以 2 ,不允許超過最大延遲時間( maxDelay )。
    • 第 41 至 44 行 :強制取消未完成的子任務。
    • 第 46 至 49 行 :調度下一次 TimedSupervisorTask 。

    3. Eureka-Server 接收續租

    3.1 接收續租請求

    com.netflix.eureka.resources.InstanceResource,處理單個應用實例信息的請求操作的 Resource ( Controller )。

    續租應用實例信息的請求,映射 InstanceResource#renewLease() 方法,實現代碼如下:

    1: @PUT
    2: public Response renewLease(
    3: @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
    4: @QueryParam("overriddenstatus") String overriddenStatus,
    5: @QueryParam("status") String status,
    6: @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
    7: boolean isFromReplicaNode = "true".equals(isReplication);
    8: // 續租
    9: boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
    10:
    11: // 續租失敗
    12: // Not found in the registry, immediately ask for a register
    13: if (!isSuccess) {
    14: logger.warn("Not Found (Renew): {} - {}", app.getName(), id);
    15: return Response.status(Status.NOT_FOUND).build();
    16: }
    17:
    18: // 比較 InstanceInfo 的 lastDirtyTimestamp 屬性
    19: // Check if we need to sync based on dirty time stamp, the client
    20: // instance might have changed some value
    21: Response response = null;
    22: if (lastDirtyTimestamp != null && serverConfig.shouldSyncWhenTimestampDiffers()) {
    23: response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode);
    24: // Store the overridden status since the validation found out the node that replicates wins
    25: if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()
    26: && (overriddenStatus != null)
    27: && !(InstanceStatus.UNKNOWN.name().equals(overriddenStatus))
    28: && isFromReplicaNode) {
    29: registry.storeOverriddenStatusIfRequired(app.getAppName(), id, InstanceStatus.valueOf(overriddenStatus));
    30: }
    31: } else { // 成功
    32: response = Response.ok().build();
    33: }
    34: logger.debug("Found (Renew): {} - {}; reply status={}" + app.getName(), id, response.getStatus());
    35: return response;
    36: }

    • 第 8 至 9 行 :調用 PeerAwareInstanceRegistryImpl#renew(...) 方法,續租。實現代碼如下:

      // PeerAwareInstanceRegistryImpl.java
      public boolean renew(final String appName, final String id, final boolean isReplication) {
      if (super.renew(appName, id, isReplication)) { // 續租
      // Eureka-Server 複製
      replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
      return true;
      }
      return false;
      }
      • 調用父類 AbstractInstanceRegistry#renew(...) 方法,註冊應用實例信息。

    • 第 11 至 16 行 :續租失敗,返回 404 響應。當 Eureka-Client 收到 404 響應後,會重新發起 InstanceInfo 的註冊。


    • 第 18 至 30 行 :比較請求的 lastDirtyTimestamp 和 Server 的 InstanceInfo 的 lastDirtyTimestamp 屬性差異,需要配置 eureka.syncWhenTimestampDiffers = true ( 默認開啓 )。

      • 第 23 行 :調用 #validateDirtyTimestamp(...) 方法,比較 lastDirtyTimestamp 的差異。實現代碼如下:

        // InstanceResource.java
        1: private Response validateDirtyTimestamp(Long lastDirtyTimestamp, boolean isReplication) {
        2: // 獲取 InstanceInfo
        3: InstanceInfo appInfo = registry.getInstanceByAppAndId(app.getName(), id, false);
        4: if (appInfo != null) {
        5: if ((lastDirtyTimestamp != null) && (!lastDirtyTimestamp.equals(appInfo.getLastDirtyTimestamp()))) {
        6: Object[] args = {id, appInfo.getLastDirtyTimestamp(), lastDirtyTimestamp, isReplication};
        7: // 請求 的 較大
        8: if (lastDirtyTimestamp > appInfo.getLastDirtyTimestamp()) {
        9: logger.debug("Time to sync, since the last dirty timestamp differs -"
        10: + " ReplicationInstance id : {},Registry : {} Incoming: {} Replication: {}", args);
        11: return Response.status(Status.NOT_FOUND).build();
        12: // Server 的 較大
        13: } else if (appInfo.getLastDirtyTimestamp() > lastDirtyTimestamp) {
        14: // In the case of replication, send the current instance info in the registry for the
        15: // replicating node to sync itself with this one.
        16: if (isReplication) {
        17: logger.debug(
        18: "Time to sync, since the last dirty timestamp differs -"
        19: + " ReplicationInstance id : {},Registry : {} Incoming: {} Replication: {}",
        20: args);
        21: return Response.status(Status.CONFLICT).entity(appInfo).build();
        22: } else {
        23: return Response.ok().build();
        24: }
        25: }
        26: }
        27:
        28: }
        29: return Response.ok().build();
        30: }
        • 第 7 至 11 行 :請求的 lastDirtyTimestamp 較大,意味着請求方( 可能是 Eureka-Client ,也可能是 Eureka-Server 集羣內的其他 Server )存在 InstanceInfo 和 Eureka-Server 的 InstanceInfo 的數據不一致,返回 404 響應。請求方收到 404 響應後重新發起註冊
        • 第 16 至 21 行 :《Eureka 源碼解析 —— Eureka-Server 集羣同步》 有詳細解析。
        • 第 22 至 24 行 :Server 的 lastDirtyTimestamp 較大,並且請求方爲 Eureka-Client,續租成功,返回 200 成功響應。
        • 第 29 行 :lastDirtyTimestamp 一致,返回 200 成功響應。


    • 第 31 至 33 行 :續租成功,返回 200 成功響應。



    3.2 續租應用實例信息

    調用 AbstractInstanceRegistry#renew(...) 方法,續租應用實例信息,實現代碼如下:


    1: public boolean renew(String appName, String id, boolean isReplication) {
    2: // 增加 續租次數 到 監控
    3: RENEW.increment(isReplication);
    4: // 獲得 租約
    5: Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
    6: Lease<InstanceInfo> leaseToRenew = null;
    7: if (gMap != null) {
    8: leaseToRenew = gMap.get(id);
    9: }
    10: // 租約不存在
    11: if (leaseToRenew == null) {
    12: RENEW_NOT_FOUND.increment(isReplication);
    13: logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
    14: return false;
    15: } else {
    16: InstanceInfo instanceInfo = leaseToRenew.getHolder();
    17: if (instanceInfo != null) {
    18: // touchASGCache(instanceInfo.getASGName());
    19: // override status
    20: InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
    21: instanceInfo, leaseToRenew, isReplication);
    22: if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
    23: logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}"
    24: + "; re-register required", instanceInfo.getId());
    25: RENEW_NOT_FOUND.increment(isReplication);
    26: return false;
    27: }
    28: if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
    29: Object[] args = {
    30: instanceInfo.getStatus().name(),
    31: instanceInfo.getOverriddenStatus().name(),
    32: instanceInfo.getId()
    33: };
    34: logger.info(
    35: "The instance status {} is different from overridden instance status {} for instance {}. "
    36: + "Hence setting the status to overridden status", args);
    37: instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
    38: }
    39: }
    40: // 新增 續租每分鐘次數
    41: renewsLastMin.increment();
    42: // 設置 租約最後更新時間(續租)
    43: leaseToRenew.renew();
    44: return true;
    45: }
    46: }
    • 第 2 至 3 行 :增加續租次數到監控。配合 Netflix Servo 實現監控信息採集。
    • 第 4 至 9 行 :獲得租約( Lease )。
    • 第 10 至 14 行 :租約不存在,返回續租失敗( false )。
    • 第 19 至 21 行 :獲得應用實例的最終狀態。在《應用實例註冊發現 (八)之覆蓋狀態》詳細解析。
    • 第 22 至 27 行 :應用實例的最終狀態UNKNOWN,無法續約,返回 false 。在《應用實例註冊發現 (八)之覆蓋狀態》詳細解析。
    • 第 28 至 37 行 :應用實例的狀態與最終狀態不相等,使用最終狀態覆蓋應用實例的狀態。在《應用實例註冊發現 (八)之覆蓋狀態》詳細解析。
    • 第 40 至 41 行 :新增續租每分鐘次數( renewsLastMin )。com.netflix.eureka.util.MeasuredRate,速度測量類,實現代碼如下:

      // AbstractInstanceRegistry.java
      /**
      * 續租每分鐘次數
      */
      private final MeasuredRate renewsLastMin;
      // MeasuredRate.java
      public class MeasuredRate {
      /**
      * 上一個間隔次數
      */
      private final AtomicLong lastBucket = new AtomicLong(0);
      /**
      * 當前間隔次數
      */
      private final AtomicLong currentBucket = new AtomicLong(0);
      /**
      * 間隔
      */
      private final long sampleInterval;
      /**
      * 定時器
      */
      private final Timer timer;
      private volatile boolean isActive;
      public MeasuredRate(long sampleInterval) {
      this.sampleInterval = sampleInterval;
      this.timer = new Timer("Eureka-MeasureRateTimer", true);
      this.isActive = false;
      }
      public synchronized void start() {
      if (!isActive) {
      timer.schedule(new TimerTask() {
      @Override
      public void run() {
      try {
      // Zero out the current bucket.
      lastBucket.set(currentBucket.getAndSet(0));
      } catch (Throwable e) {
      logger.error("Cannot reset the Measured Rate", e);
      }
      }
      }, sampleInterval, sampleInterval);
      isActive = true;
      }
      }
      public synchronized void stop() {
      if (isActive) {
      timer.cancel();
      isActive = false;
      }
      }
      /**
      * Returns the count in the last sample interval.
      */
      public long getCount() {
      return lastBucket.get();
      }
      /**
      * Increments the count in the current sample interval.
      */
      public void increment() {
      currentBucket.incrementAndGet();
      }
      }
      • timer ,定時器,負責每個 sampleInterval 間隔重置當前次數( currentBucket ),並將原當前次數設置到上一個次數( lastBucket )。
      • #increment() 方法,返回當前次數( currentBucket )。
      • #getCount() 方法,返回上一個次數( lastBucket )。
      • renewsLastMin 有如下用途:


    • 第 42 至 43 行 :調用 Lease#renew() 方法,設置租約最後更新時間( 續租 ),實現代碼如下:


      public void renew() {
      lastUpdateTimestamp = System.currentTimeMillis() + duration;
      }
      • x

    • 第 44 行 :返回續租成功( true )。


    • 整個過程修改的租約的過期時間,即使併發請求,也不會對數據的一致性產生不一致的影響,因此像註冊操作一樣加鎖
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章