歡迎光臨
每天分享高質量文章

註冊中心 Eureka 原始碼解析 —— 應用實體註冊發現(一)之註冊

點選上方“芋道原始碼”,選擇“置頂公眾號”

技術文章第一時間送達!

原始碼精品專欄

 

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

  • 1. 概述

  • 2. Eureka-Client 發起註冊

  • 2.1 應用實體資訊複製器

  • 2.2 掃清應用實體資訊

  • 2.3 發起註冊應用實體

  • 3. Eureka-Server 接收註冊

  • 3.1 接收註冊請求

  • 3.2 Lease

  • 3.3 註冊應用實體資訊

  • 666. 彩蛋


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,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,應用實體資訊複製器。

    • 呼叫 DiscoveryClient#refreshInstanceInfo() 方法,掃清應用實體資訊。此處可能導致應用實體資訊資料不一致,在「2.2」掃清應用實體資訊 詳細解析。

    • 呼叫 DiscoveryClient#register() 方法,Eureka-Client 向 Eureka-Server 註冊應用實體

    • 呼叫 ScheduledExecutorService#schedule(...) 方法,再次延遲執行任務,並設定 scheduledPeriodicRef。透過這樣的方式,不斷迴圈定時執行任務。

    • 執行 instanceInfo.setIsDirty() 程式碼塊,因為 InstanceInfo 剛被建立時,在 Eureka-Server 不存在,也會被註冊

    • 呼叫 ScheduledExecutorService#schedule(...) 方法,延遲 initialDelayMs 毫秒執行一次任務。為什麼此處設定 scheduledPeriodicRef ?在 InstanceInfoReplicator#onDemandUpdate() 方法會看到具體用途。

    • 呼叫 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 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();

      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 的狀態( 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 {
        }
      }
  • com.netflix.appinfo.ApplicationInfoManager.StatusChangeListener 內部類,監聽應用實體資訊狀態的變更。

    • 呼叫 Future#cancel(false) 方法,取消定時任務,避免無用的註冊

    • 呼叫 InstanceInfoReplicator#run() 方法,發起註冊。

    • 呼叫 ApplicationInfoManager#registerStatusChangeListener(...) 方法,註冊應用實體狀態變更監聽器。實現程式碼如下:

      public class ApplicationInfoManager {
      /**
      * 狀態變更監聽器
      */

      protected final Map 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;
        }
      }    

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 hostInfo = getHostInfo();

    @Override
    public String getHostName(boolean refresh) {
       return hostInfo.second();
    }

    @Override
    public String getIpAddress() {
       return hostInfo.first();
    }

    private static Pair getHostInfo() {
       Pair pair;
       try {
           InetAddress localHost = InetAddress.getLocalHost();
           pair = new Pair(localHost.getHostAddress(), localHost.getHostName());
       } catch (UnknownHostException e) {
           logger.error("Cannot get host info", e);
           pair = new Pair("", "");
       }
       return pair;
    }

    }

    • 關註應用實體資訊的 hostName 、 ipAddr 、 dataCenterInfo 屬性的變化。

    • 一般情況下,我們使用的是非 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();
      }
    }
    • 關註應用實體資訊的 renewalIntervalInSecs 、 durationInSecs 屬性的變化。

  • 呼叫 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 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 register(InstanceInfo info) {
  String urlPath = "apps/" + info.getAppName();
  ClientResponse response = null;
  try {
      Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
      addExtraHeaders(resourceBuilder);
      response = resourceBuilder
              .essay-header("Accept-Encoding", "gzip")
              .type(MediaType.APPLICATION_JSON_TYPE)
              .accept(MediaType.APPLICATION_JSON)
              .post(ClientResponse.class, info);
      return anEurekaHttpResponse(response.getStatus()).essay-headers(essay-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> gMap = registry.get(registrant.getAppName());
 6:         // 增加 註冊次數 到 監控
 7:         REGISTER.increment(isReplication);
 8:         // 獲得 應用實體資訊 對應的 租約
 9:         if (gMap == null) {
10:             final ConcurrentHashMap> gNewMap = new ConcurrentHashMap>();
11:             gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap); // 新增 應用
12:             if (gMap == null) { // 新增 應用 成功
13:                 gMap = gNewMap;
14:             }
15:         }
16:         Lease 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:             // 【自我保護機制】增加 `numberOfRenewsPerMinThreshold` 、`expectedNumberOfRenewsPerMin`
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 lease = new Lease(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(
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>> registry = new ConcurrentHashMap>>();
  • 第 17 至 30 行 :當租約已存在,判斷 Server 已存在的 InstanceInfo 的 lastDirtyTimestamp 是否大於( 不包括等於 ) Client 請求的 InstanceInfo ,若是,使用 Server 的 InstanceInfo 進行替代

  • 第 31 至 44 行 :增加 numberOfRenewsPerMinThreshold 、expectedNumberOfRenewsPerMin,自我保護機制相關,在 《Eureka 原始碼解析 —— 應用實體註冊發現(四)之自我保護機制》 有詳細解析。

  • 第 45 至 52 行 :建立租約,並新增到租約對映( registry )。

  • 第 53 至 58 行 :新增到最近註冊的除錯佇列( recentRegisteredQueue ),用於 Eureka-Server 運維介面的顯示,無實際業務邏輯使用。實現程式碼如下:

    /**
    * 最近註冊的除錯佇列
    * key :新增時的時間戳
    * value :字串 = 應用名(應用實體資訊編號)
    */

    private final CircularQueue> recentRegisteredQueue;

    /**
    * 迴圈佇列
    *
    * @param 泛型
    */

    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 recentlyChangedQueue = new ConcurrentLinkedQueue();
  • 第 89 至 90 行 :設定租約的最後更新時間戳。

  • 第 91 至 92 行 :設定響應快取( ResponseCache )過期,在《Eureka 原始碼解析 —— 應用實體註冊發現 (六)之全量獲取》詳細解析。

  • 第 96 至 97 行 :釋放鎖。

666. 彩蛋

知識星球

嘿嘿,蠻嗨的,比起前面幾篇寫配置相關的文章來說。

胖友,分享我的公眾號( 芋道原始碼 ) 給你的胖友可好?

贊(0)

分享創造快樂