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

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

本文主要基於 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 發起註冊應用實例需要符合如下條件:

    • 配置 eureka.registration.enabled = true,Eureka-Client 向 Eureka-Server 發起註冊應用實例的開關
    • InstanceInfo 在 Eureka-Client 和 Eureka-Server 數據不一致。

    每次 InstanceInfo 發生屬性變化時,標記 isInstanceInfoDirty 屬性爲 true,表示 InstanceInfo 在 Eureka-Client 和 Eureka-Server 數據不一致,需要註冊。另外,InstanceInfo 剛被創建時,在 Eureka-Server 不存在,也會被註冊。

    當符合條件時,InstanceInfo 不會立即向 Eureka-Server 註冊,而是後臺線程定時註冊。

    當 InstanceInfo 的狀態( status ) 屬性發生變化時,並且配置 eureka.shouldOnDemandUpdateStatusChange = true 時,立即向 Eureka-Server 註冊。因爲狀態屬性非常重要,一般情況下建議開啓,當然默認情況也是開啓的

    Let’s Go。讓我們看看代碼的實現。

    2.1 應用實例信息複製器

    // DiscoveryClient.java
    public class DiscoveryClient implements EurekaClient {
    /
    * 應用實例狀態變更監聽器
    /
    private ApplicationInfoManager.StatusChangeListener statusChangeListener;
    /
    應用實例信息複製器
    */
    private InstanceInfoReplicator instanceInfoReplicator;
    private void initScheduledTasks() {
    // ... 省略無關代碼
    if (clientConfig.shouldRegisterWithEureka()) {
    // ... 省略無關代碼
    // 創建 應用實例信息複製器
    // InstanceInfo replicator
    instanceInfoReplicator = new InstanceInfoReplicator(
    this,
    instanceInfo,
    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
    2); // burstSize
    // 創建 應用實例狀態變更監聽器
    statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
    @Override
    public String getId() {
    return "statusChangeListener";
    }
    @Override
    public void notify(StatusChangeEvent statusChangeEvent) {
    if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
    InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
    // log at warn level if DOWN was involved
    logger.warn("Saw local status change event {}", statusChangeEvent);
    } else {
    logger.info("Saw local status change event {}", statusChangeEvent);
    }
    instanceInfoReplicator.onDemandUpdate();
    }
    };
    // 註冊 應用實例狀態變更監聽器
    if (clientConfig.shouldOnDemandUpdateStatusChange()) {
    applicationInfoManager.registerStatusChangeListener(statusChangeListener);
    }
    // 開啓 應用實例信息複製器
    instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    }
    }
    }

    • com.netflix.discovery.InstanceInfoReplicator,應用實例信息複製器。

      • 調用 InstanceInfoReplicator#start(...) 方法,開啓應用實例信息複製器。實現代碼如下:

        // InstanceInfoReplicator.java
        class InstanceInfoReplicator implements Runnable {
        private static final Logger logger = LoggerFactory.getLogger(InstanceInfoReplicator.class);
        private final DiscoveryClient discoveryClient;
        /**
        * 應用實例信息
        */
        private final InstanceInfo instanceInfo;
        /**
        * 定時執行頻率,單位:秒
        */
        private final int replicationIntervalSeconds;
        /**
        * 定時執行器
        */
        private final ScheduledExecutorService scheduler;
        /**
        * 定時執行任務的 Future
        */
        private final AtomicReference<Future> scheduledPeriodicRef;
        /**
        * 是否開啓調度
        */
        private final AtomicBoolean started;
        private final RateLimiter rateLimiter; // 限流相關,跳過
        private final int burstSize; // 限流相關,跳過
        private final int allowedRatePerMinute; // 限流相關,跳過
        InstanceInfoReplicator(DiscoveryClient discoveryClient, InstanceInfo instanceInfo, int replicationIntervalSeconds, int burstSize) {
        this.discoveryClient = discoveryClient;
        this.instanceInfo = instanceInfo;
        this.scheduler = Executors.newScheduledThreadPool(1,
        new ThreadFactoryBuilder()
        .setNameFormat("DiscoveryClient-InstanceInfoReplicator-%d")
        .setDaemon(true)
        .build());
        this.scheduledPeriodicRef = new AtomicReference<Future>();
        this.started = new AtomicBoolean(false);
        this.rateLimiter = new RateLimiter(TimeUnit.MINUTES);
        this.replicationIntervalSeconds = replicationIntervalSeconds;
        this.burstSize = burstSize;
        this.allowedRatePerMinute = 60 * this.burstSize / this.replicationIntervalSeconds;
        logger.info("InstanceInfoReplicator onDemand update allowed rate per min is {}", allowedRatePerMinute);
        }
        public void start(int initialDelayMs) {
        if (started.compareAndSet(false, true)) {
        // 設置 應用實例信息 數據不一致
        instanceInfo.setIsDirty(); // for initial register
        // 提交任務,並設置該任務的 Future
        Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
        }
        }
        // ... 省略無關方法
        }
        // InstanceInfo.java
        private volatile boolean isInstanceInfoDirty = false;
        private volatile Long lastDirtyTimestamp;
        public synchronized void setIsDirty() {
        isInstanceInfoDirty = true;
        lastDirtyTimestamp = System.currentTimeMillis();
        }
        • 執行 instanceInfo.setIsDirty() 代碼塊,因爲 InstanceInfo 剛被創建時,在 Eureka-Server 不存在,也會被註冊
        • 調用 ScheduledExecutorService#schedule(...) 方法,延遲 initialDelayMs 毫秒執行一次任務。爲什麼此處設置 scheduledPeriodicRef ?在 InstanceInfoReplicator#onDemandUpdate() 方法會看到具體用途。

      • 定時檢查 InstanceInfo 的狀態( status ) 屬性是否發生變化。若是,發起註冊。實現代碼如下:


        // InstanceInfoReplicator.java
        @Override
        public void run() {
        try {
        // 刷新 應用實例信息
        discoveryClient.refreshInstanceInfo();
        // 判斷 應用實例信息 是否數據不一致
        Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
        if (dirtyTimestamp != null) {
        // 發起註冊
        discoveryClient.register();
        // 設置 應用實例信息 數據一致
        instanceInfo.unsetIsDirty(dirtyTimestamp);
        }
        } catch (Throwable t) {
        logger.warn("There was a problem with the instance info replicator", t);
        } finally {
        // 提交任務,並設置該任務的 Future
        Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
        }
        }
        // InstanceInfo.java
        public synchronized long setIsDirtyWithTime() {
        setIsDirty();
        return lastDirtyTimestamp;
        }
        public synchronized void unsetIsDirty(long unsetDirtyTimestamp) {
        if (lastDirtyTimestamp <= unsetDirtyTimestamp) {
        isInstanceInfoDirty = false;
        } else {
        }
        }
        • 調用 DiscoveryClient#refreshInstanceInfo() 方法,刷新應用實例信息。此處可能導致應用實例信息數據不一致,在「2.2」刷新應用實例信息 詳細解析。
        • 調用 DiscoveryClient#register() 方法,Eureka-Client 向 Eureka-Server 註冊應用實例
        • 調用 ScheduledExecutorService#schedule(...) 方法,再次延遲執行任務,並設置 scheduledPeriodicRef。通過這樣的方式,不斷循環定時執行任務。


    • com.netflix.appinfo.ApplicationInfoManager.StatusChangeListener 內部類,監聽應用實例信息狀態的變更。

      • 調用 ApplicationInfoManager#registerStatusChangeListener(...) 方法,註冊應用實例狀態變更監聽器。實現代碼如下:

        public class ApplicationInfoManager {
        /**
        * 狀態變更監聽器
        */
        protected final Map<String, StatusChangeListener> listeners;
        public void registerStatusChangeListener(StatusChangeListener listener) {
        listeners.put(listener.getId(), listener);
        }
        }
      • 業務裏,調用 ApplicationInfoManager#setInstanceStatus(...) 方法,設置應用實例信息的狀態,從而通知 InstanceInfoReplicator#onDemandUpdate() 方法的調用。實現代碼如下:

        // ApplicationInfoManager.java
        public synchronized void setInstanceStatus(InstanceStatus status) {
        InstanceStatus next = instanceStatusMapper.map(status);
        if (next == null) {
        return;
        }
        InstanceStatus prev = instanceInfo.setStatus(next);
        if (prev != null) {
        for (StatusChangeListener listener : listeners.values()) {
        try {
        listener.notify(new StatusChangeEvent(prev, next));
        } catch (Exception e) {
        logger.warn("failed to notify listener: {}", listener.getId(), e);
        }
        }
        }
        }
        // InstanceInfo.java
        public synchronized InstanceStatus setStatus(InstanceStatus status) {
        if (this.status != status) {
        InstanceStatus prev = this.status;
        this.status = status;
        // 設置 應用實例信息 數據一致
        setIsDirty();
        return prev;
        }
        return null;
        }
      • InstanceInfoReplicator#onDemandUpdate(),實現代碼如下:

        // InstanceInfoReplicator.java
        public boolean onDemandUpdate() {
        if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) { // 限流相關,跳過
        scheduler.submit(new Runnable() {
        @Override
        public void run() {
        logger.debug("Executing on-demand update of local InstanceInfo");
        // 取消任務
        Future latestPeriodic = scheduledPeriodicRef.get();
        if (latestPeriodic != null && !latestPeriodic.isDone()) {
        logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update");
        latestPeriodic.cancel(false);
        }
        // 再次調用
        InstanceInfoReplicator.this.run();
        }
        });
        return true;
        } else {
        logger.warn("Ignoring onDemand update due to rate limiter");
        return false;
        }
        }
        • 調用 Future#cancel(false) 方法,取消定時任務,避免無用的註冊
        • 調用 InstanceInfoReplicator#run() 方法,發起註冊。



    2.2 刷新應用實例信息

    調用 DiscoveryClient#refreshInstanceInfo() 方法,刷新應用實例信息。此處可能導致應用實例信息數據不一致,實現代碼如下:


    void refreshInstanceInfo() {
    // 刷新 數據中心信息
    applicationInfoManager.refreshDataCenterInfoIfRequired();
    // 刷新 租約信息
    applicationInfoManager.refreshLeaseInfoIfRequired();
    // 健康檢查
    InstanceStatus status;
    try {
    status = getHealthCheckHandler().getStatus(instanceInfo.getStatus());
    } catch (Exception e) {
    logger.warn("Exception from healthcheckHandler.getStatus, setting status to DOWN", e);
    status = InstanceStatus.DOWN;
    }
    if (null != status) {
    applicationInfoManager.setInstanceStatus(status);
    }
    }
    • 調用 ApplicationInfoManager#refreshDataCenterInfoIfRequired() 方法,刷新數據中心相關信息,實現代碼如下:

      // ApplicationInfoManager.java
      public void refreshDataCenterInfoIfRequired() {
      // hostname
      String existingAddress = instanceInfo.getHostName();
      String newAddress;
      if (config instanceof RefreshableInstanceConfig) {
      // Refresh data center info, and return up to date address
      newAddress = ((RefreshableInstanceConfig) config).resolveDefaultAddress(true);
      } else {
      newAddress = config.getHostName(true);
      }
      // ip
      String newIp = config.getIpAddress();
      if (newAddress != null && !newAddress.equals(existingAddress)) {
      logger.warn("The address changed from : {} => {}", existingAddress, newAddress);
      // :( in the legacy code here the builder is acting as a mutator.
      // This is hard to fix as this same instanceInfo instance is referenced elsewhere.
      // We will most likely re-write the client at sometime so not fixing for now.
      InstanceInfo.Builder builder = new InstanceInfo.Builder(instanceInfo);
      builder.setHostName(newAddress) // hostname
      .setIPAddr(newIp) // ip
      .setDataCenterInfo(config.getDataCenterInfo()); // dataCenterInfo
      instanceInfo.setIsDirty();
      }
      }
      public abstract class AbstractInstanceConfig implements EurekaInstanceConfig {
      private static final Pair<String, String> hostInfo = getHostInfo();
      @Override
      public String getHostName(boolean refresh) {
      return hostInfo.second();
      }
      @Override
      public String getIpAddress() {
      return hostInfo.first();
      }
      private static Pair<String, String> getHostInfo() {
      Pair<String, String> pair;
      try {
      InetAddress localHost = InetAddress.getLocalHost();
      pair = new Pair<String, String>(localHost.getHostAddress(), localHost.getHostName());
      } catch (UnknownHostException e) {
      logger.error("Cannot get host info", e);
      pair = new Pair<String, String>("", "");
      }
      return pair;
      }
      }
      • 關注應用實例信息的 hostNameipAddrdataCenterInfo 屬性的變化。
      • 一般情況下,我們使用的是非 RefreshableInstanceConfig 實現的配置類( 一般是 MyDataCenterInstanceConfig ),因爲 AbstractInstanceConfig.hostInfo靜態屬性即使本機修改了 IP 等信息,Eureka-Client 進程也不會感知到。TODO[0022]:看下springcloud 的實現

    • 調用 ApplicationInfoManager#refreshLeaseInfoIfRequired() 方法,刷新租約相關信息,實現代碼如下:


      public void refreshLeaseInfoIfRequired() {
      LeaseInfo leaseInfo = instanceInfo.getLeaseInfo();
      if (leaseInfo == null) {
      return;
      }
      int currentLeaseDuration = config.getLeaseExpirationDurationInSeconds();
      int currentLeaseRenewal = config.getLeaseRenewalIntervalInSeconds();
      if (leaseInfo.getDurationInSecs() != currentLeaseDuration // 租約過期時間 改變
      || leaseInfo.getRenewalIntervalInSecs() != currentLeaseRenewal) { // 租約續約頻率 改變
      LeaseInfo newLeaseInfo = LeaseInfo.Builder.newBuilder()
      .setRenewalIntervalInSecs(currentLeaseRenewal)
      .setDurationInSecs(currentLeaseDuration)
      .build();
      instanceInfo.setLeaseInfo(newLeaseInfo);
      instanceInfo.setIsDirty();
      }
      }
      • 關注應用實例信息的 renewalIntervalInSecsdurationInSecs 屬性的變化。

    • 調用 HealthCheckHandler#getStatus() 方法,健康檢查。這裏先暫時跳過,我們在TODO[0004]:健康檢查 詳細解析。



    2.3 發起註冊應用實例

    調用 DiscoveryClient#register() 方法,Eureka-Client 向 Eureka-Server 註冊應用實例,實現代碼如下:


    // DiscoveryClient.java
    boolean register() throws Throwable {
    logger.info(PREFIX + appPathIdentifier + ": registering service...");
    EurekaHttpResponse<Void> httpResponse;
    try {
    httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
    } catch (Exception e) {
    logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
    throw e;
    }
    if (logger.isInfoEnabled()) {
    logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
    }
    return httpResponse.getStatusCode() == 204;
    }
    // AbstractJerseyEurekaHttpClient.java
    @Override
    public EurekaHttpResponse<Void> register(InstanceInfo info) {
    String urlPath = "apps/" + info.getAppName();
    ClientResponse response = null;
    try {
    Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
    addExtraHeaders(resourceBuilder);
    response = resourceBuilder
    .header("Accept-Encoding", "gzip")
    .type(MediaType.APPLICATION_JSON_TYPE)
    .accept(MediaType.APPLICATION_JSON)
    .post(ClientResponse.class, info);
    return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
    } finally {
    if (logger.isDebugEnabled()) {
    logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
    response == null ? "N/A" : response.getStatus());
    }
    if (response != null) {
    response.close();
    }
    }
    }
    • 調用 AbstractJerseyEurekaHttpClient#register(...) 方法,POST 請求 Eureka-Server 的 apps/${APP_NAME} 接口,參數爲 InstanceInfo ,實現註冊實例信息的註冊。

    3. Eureka-Server 接收註冊

    3.1 接收註冊請求

    com.netflix.eureka.resources.ApplicationResource,處理單個應用的請求操作的 Resource ( Controller )。

    註冊應用實例信息的請求,映射 ApplicationResource#addInstance() 方法,實現代碼如下:

    @Produces({"application/xml", "application/json"})
    public class ApplicationResource {
    @POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info,
    @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
    // 校驗參數是否合法
    logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
    // validate that the instanceinfo contains all the necessary required fields
    if (isBlank(info.getId())) {
    return Response.status(400).entity("Missing instanceId").build();
    } else if (isBlank(info.getHostName())) {
    return Response.status(400).entity("Missing hostname").build();
    } else if (isBlank(info.getIPAddr())) {
    return Response.status(400).entity("Missing ip address").build();
    } else if (isBlank(info.getAppName())) {
    return Response.status(400).entity("Missing appName").build();
    } else if (!appName.equals(info.getAppName())) {
    return Response.status(400).entity("Mismatched appName, expecting " + appName + " but was " + info.getAppName()).build();
    } else if (info.getDataCenterInfo() == null) {
    return Response.status(400).entity("Missing dataCenterInfo").build();
    } else if (info.getDataCenterInfo().getName() == null) {
    return Response.status(400).entity("Missing dataCenterInfo Name").build();
    }
    // AWS 相關,跳過
    // handle cases where clients may be registering with bad DataCenterInfo with missing data
    DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
    if (dataCenterInfo instanceof UniqueIdentifier) {
    String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId();
    if (isBlank(dataCenterInfoId)) {
    boolean experimental = "true".equalsIgnoreCase(serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
    if (experimental) {
    String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
    return Response.status(400).entity(entity).build();
    } else if (dataCenterInfo instanceof AmazonInfo) {
    AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo;
    String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId);
    if (effectiveId == null) {
    amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId());
    }
    } else {
    logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
    }
    }
    }
    // 註冊應用實例信息
    registry.register(info, "true".equals(isReplication));
    // 返回 204 成功
    return Response.status(204).build(); // 204 to be backwards compatible
    }
    }

    • 請求頭 isReplication 參數,和 Eureka-Server 集羣複製相關,暫時跳過。
    • 調用 PeerAwareInstanceRegistryImpl#register(...) 方法,註冊應用實例信息。實現代碼如下:

      @Override
      public void register(final InstanceInfo info, final boolean isReplication) {
      // 租約過期時間
      int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
      if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
      leaseDuration = info.getLeaseInfo().getDurationInSecs();
      }
      // 註冊應用實例信息
      super.register(info, leaseDuration, isReplication);
      // Eureka-Server 複製
      replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
      }
      • 調用父類 AbstractInstanceRegistry#register(...) 方法,註冊應用實例信息。


    3.2 Lease

    在看具體的註冊應用實例信息的邏輯之前,我們先來看下 com.netflix.eureka.lease.Lease,租約。實現代碼如下:


    public class Lease<T> {
    /
    * 實體
    /
    private T holder;
    /
    註冊時間戳
    /
    private long registrationTimestamp;
    /
    開始服務時間戳
    /
    private long serviceUpTimestamp;
    /
    取消註冊時間戳
    /
    private long evictionTimestamp;
    /
    最後更新時間戳
    /
    // Make it volatile so that the expiration task would see this quicker
    private volatile long lastUpdateTimestamp;
    /
    租約持續時長,單位:毫秒
    /
    private long duration;
    public Lease(T r, int durationInSecs) {
    holder = r;
    registrationTimestamp = System.currentTimeMillis();
    lastUpdateTimestamp = registrationTimestamp;
    duration = (durationInSecs 1000);
    }
    }
    • holder 屬性,租約的持有者。在 Eureka-Server 裏,暫時只有 InstanceInfo 使用。
    • registrationTimestamp 屬性,註冊( 創建 )租約時間戳。在構造方法裏可以看租約對象的創建時間戳即爲註冊租約時間戳。
    • serviceUpTimestamp 屬性,開始服務時間戳。註冊應用實例信息會使用到它如下兩個方法,實現代碼如下:

      public void serviceUp() {
      if (serviceUpTimestamp == 0) { // 第一次有效
      serviceUpTimestamp = System.currentTimeMillis();
      }
      }
      public void setServiceUpTimestamp(long serviceUpTimestamp) {
      this.serviceUpTimestamp = serviceUpTimestamp;
      }
    • lastUpdatedTimestamp 屬性,最後更新租約時間戳。每次續租時,更新該時間戳。註冊應用實例信息會使用到它如下方法,實現代碼如下:

      public void setLastUpdatedTimestamp() {
      this.lastUpdatedTimestamp = System.currentTimeMillis();
      }
    • duration 屬性,租約持續時間,單位:毫秒。當租約過久未續租,即當前時間 - lastUpdatedTimestamp > duration 時,租約過期。

    • evictionTimestamp 屬性,租約過期時間戳。

    3.3 註冊應用實例信息

    調用 AbstractInstanceRegistry#register(...) 方法,註冊應用實例信息,實現代碼如下:


    1: public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    2: try {
    3: // 獲取讀鎖
    4: read.lock();
    5: Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
    6: // 增加 註冊次數 到 監控
    7: REGISTER.increment(isReplication);
    8: // 獲得 應用實例信息 對應的 租約
    9: if (gMap == null) {
    10: final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
    11: gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap); // 添加 應用
    12: if (gMap == null) { // 添加 應用 成功
    13: gMap = gNewMap;
    14: }
    15: }
    16: Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
    17: // Retain the last dirty timestamp without overwriting it, if there is already a lease
    18: if (existingLease != null && (existingLease.getHolder() != null)) { // 已存在時,使用數據不一致的時間大的應用註冊信息爲有效的
    19: Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp(); // Server 註冊的 InstanceInfo
    20: Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp(); // Client 請求的 InstanceInfo
    21: logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
    22:
    23: // this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
    24: // InstanceInfo instead of the server local copy.
    25: if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
    26: logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
    27: " than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
    28: logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
    29: registrant = existingLease.getHolder();
    30: }
    31: } else {
    32: // The lease does not exist and hence it is a new registration
    33: // 【自我保護機制】增加 numberOfRenewsPerMinThresholdexpectedNumberOfRenewsPerMin
    34: synchronized (lock) {
    35: if (this.expectedNumberOfRenewsPerMin > 0) {
    36: // Since the client wants to cancel it, reduce the threshold
    37: // (1
    38: // for 30 seconds, 2 for a minute)
    39: this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
    40: this.numberOfRenewsPerMinThreshold =
    41: (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
    42: }
    43: }
    44: logger.debug("No previous lease information found; it is new registration");
    45: }
    46: // 創建 租約
    47: Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
    48: if (existingLease != null) { // 若租約已存在,設置 租約的開始服務的時間戳
    49: lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
    50: }
    51: // 添加到 租約映射
    52: gMap.put(registrant.getId(), lease);
    53: // 添加到 最近註冊的調試隊列
    54: synchronized (recentRegisteredQueue) {
    55: recentRegisteredQueue.add(new Pair<Long, String>(
    56: System.currentTimeMillis(),
    57: registrant.getAppName() + "(" + registrant.getId() + ")"));
    58: }
    59: // 添加到 應用實例覆蓋狀態映射(Eureka-Server 初始化使用)
    60: // This is where the initial state transfer of overridden status happens
    61: if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
    62: logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
    63: + "overrides", registrant.getOverriddenStatus(), registrant.getId());
    64: if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
    65: logger.info("Not found overridden id {} and hence adding it", registrant.getId());
    66: overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
    67: }
    68: }
    69: InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
    70: if (overriddenStatusFromMap != null) {
    71: logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
    72: registrant.setOverriddenStatus(overriddenStatusFromMap);
    73: }
    74:
    75: // 獲得應用實例最終狀態,並設置應用實例的狀態
    76: // Set the status based on the overridden status rules
    77: InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
    78: registrant.setStatusWithoutDirty(overriddenInstanceStatus);
    79:
    80: // 設置 租約的開始服務的時間戳(只有第一次有效)
    81: // If the lease is registered with UP status, set lease service up timestamp
    82: if (InstanceStatus.UP.equals(registrant.getStatus())) {
    83: lease.serviceUp();
    84: }
    85: // 設置 應用實例信息的操作類型 爲 添加
    86: registrant.setActionType(ActionType.ADDED);
    87: // 添加到 最近租約變更記錄隊列
    88: recentlyChangedQueue.add(new RecentlyChangedItem(lease));
    89: // 設置 租約的最後更新時間戳
    90: registrant.setLastUpdatedTimestamp();
    91: // 設置 響應緩存 過期
    92: invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
    93: logger.info("Registered instance {}/{} with status {} (replication={})",
    94: registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
    95: } finally {
    96: // 釋放鎖
    97: read.unlock();
    98: }
    99: }
    • 第 3 行 :添加到應用實例覆蓋狀態映射,在 《Eureka 源碼解析 —— Eureka-Server 集羣同步》 詳細解析。
    • 第 6 至 7 行 :增加註冊次數到監控。配合 Netflix Servo 實現監控信息採集。
    • 第 5 至 16 行 :獲得應用實例信息對應的租約registry 實現代碼如下:

      /**
      * 租約映射
      * key1 :應用名 {@link InstanceInfo#appName}
      * key2 :應用實例信息編號 {@link InstanceInfo#instanceId}
      * value :租約
      */
      private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
    • 第 17 至 30 行 :當租約已存在,判斷 Server 已存在的 InstanceInfo 的 lastDirtyTimestamp 是否大於( 不包括等於 ) Client 請求的 InstanceInfo ,若是,使用 Server 的 InstanceInfo 進行替代

    • 第 31 至 44 行 :增加 numberOfRenewsPerMinThresholdexpectedNumberOfRenewsPerMin,自我保護機制相關,在 《Eureka 源碼解析 —— 應用實例註冊發現(四)之自我保護機制》 有詳細解析。
    • 第 45 至 52 行 :創建租約,並添加到租約映射( registry )。
    • 第 53 至 58 行 :添加到最近註冊的調試隊列( recentRegisteredQueue ),用於 Eureka-Server 運維界面的顯示,無實際業務邏輯使用。實現代碼如下:

      /**
      * 最近註冊的調試隊列
      * key :添加時的時間戳
      * value :字符串 = 應用名(應用實例信息編號)
      */
      private final CircularQueue<Pair<Long, String>> recentRegisteredQueue;
      /**
      * 循環隊列
      *
      * @param <E> 泛型
      */
      private class CircularQueue<E> extends ConcurrentLinkedQueue<E> {
      /**
      * 隊列大小
      */
      private int size = 0;
      public CircularQueue(int size) {
      this.size = size;
      }
      @Override
      public boolean add(E e) {
      this.makeSpaceIfNotAvailable();
      return super.add(e);
      }
      /**
      * 保證空間足夠
      *
      * 當空間不夠時,移除首元素
      */
      private void makeSpaceIfNotAvailable() {
      if (this.size() == size) {
      this.remove();
      }
      }
      public boolean offer(E e) {
      this.makeSpaceIfNotAvailable();
      return super.offer(e);
      }
      }
    • 第 59 至 68 行 :添加到應用實例覆蓋狀態映射,在 《Eureka 源碼解析 —— Eureka-Server 集羣同步》 詳細解析。

    • 第 69 至 73 行 :設置應用實例的覆蓋狀態( overridestatus ),避免註冊應用實例後,丟失覆蓋狀態。在《應用實例註冊發現 (八)之覆蓋狀態》詳細解析。
    • 第 75 至 78 行 : 獲得應用實例最終狀態,並設置應用實例的狀態。在《應用實例註冊發現 (八)之覆蓋狀態》詳細解析。
    • 第 80 至 84 行 :設置租約的開始服務的時間戳( 只有第一次有效 )。
    • 第 85 至 88 行 :設置應用實例信息的操作類型爲添加,並添加到最近租約變更記錄隊列( recentlyChangedQueue )。recentlyChangedQueue 用於註冊信息的增量獲取,在《應用實例註冊發現 (七)之增量獲取》詳細解析。實現代碼如下:

      /**
      * 最近租約變更記錄隊列
      */
      private ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue = new ConcurrentLinkedQueue<RecentlyChangedItem>();
    • 第 89 至 90 行 :設置租約的最後更新時間戳。

    • 第 91 至 92 行 :設置響應緩存( ResponseCache )過期,在《Eureka 源碼解析 —— 應用實例註冊發現 (六)之全量獲取》詳細解析。
    • 第 96 至 97 行 :釋放鎖。
發佈了22 篇原創文章 · 獲贊 2 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章