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

芋道原始碼_以德服人_不服就幹發表於2019-03-03

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

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


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

???關注**微信公眾號:【芋道原始碼】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有原始碼分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文註釋原始碼 GitHub 地址
  3. 您對於原始碼的疑問每條留言將得到認真回覆。甚至不知道如何讀原始碼也可以請教噢
  4. 新的原始碼解析文章實時收到通知。每週更新一篇左右
  5. 認真的原始碼交流微信群。

1. 概述

本文主要分享 Eureka-Client 向 Eureka-Server 註冊應用例項的過程

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

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

  • 藍框部分,為本文重點。
  • 藍框部分,Eureka-Server 叢集間複製註冊的應用例項資訊,不在本文內容範疇。

推薦 Spring Cloud 書籍

推薦 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:             // 【自我保護機制】增加 `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<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 行 :釋放鎖。

666. 彩蛋

知識星球

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

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

相關文章