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

註冊中心 Eureka 原始碼解析 —— Eureka-Client 初始化(一)之 EurekaInstanceConfig

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

  • 1. 概述

  • 2. EurekaInstanceConfig

    • 2.1 類關係圖

    • 2.2 配置屬性

    • 2.3 AbstractInstanceConfig

    • 2.4 PropertiesInstanceConfig

    • 2.5 MyDataCenterInstanceConfig

  • 2.6 小結

    • 3. InstanceInfo

    • 4. ApplicationInfoManager

  • 666. 彩蛋


友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群討論技術和原始碼。

友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群討論技術和原始碼。

友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群討論技術和原始碼。


1. 概述

本文主要分享 Eureka-Client 自身初始化的過程,不包含 Eureka-Client 向 Eureka-Server 的註冊過程( ?後面會另外文章分享 )。

Eureka-Client 自身初始化過程中,涉及到主要物件如下圖:

  1. 建立 EurekaInstanceConfig物件

  2. 使用 EurekaInstanceConfig物件 建立 InstanceInfo物件

  3. 使用 EurekaInstanceConfig物件 + InstanceInfo物件 建立 ApplicationInfoManager物件

  4. 建立 EurekaClientConfig物件

  5. 使用 ApplicationInfoManager物件 + EurekaClientConfig物件 建立 EurekaClient物件

考慮到整個初始化的過程中涉及的配置特別多,拆分成三篇文章:

  1. 【本文】(一)EurekaInstanceConfig

  2. (二)EurekaClientConfig

  3. (三)EurekaClient

下麵我們來看看每個的實現。

推薦 Spring Cloud 書籍

  • 請支援正版。下載盜版,等於主動編寫低階 BUG 。

  • 程式猿DD —— 《Spring Cloud微服務實戰》

  • 周立 —— 《Spring Cloud與Docker微服務架構實戰》

  • 兩書齊買,京東包郵。

2. EurekaInstanceConfig

com.netflix.appinfo.EurekaInstanceConfig,Eureka 應用實體配置介面。在下文你會看到 EurekaClientConfig 介面,兩者的區別如下:

  • EurekaInstanceConfig,重在應用實體,例如,應用名、應用的埠等等。此處應用指的是,Application Consumer 和 Application Provider。

  • EurekaClientConfig,重在 Eureka-Client,例如, 連線的 Eureka-Server 的地址、獲取服務提供者串列的頻率、註冊自身為服務提供者的頻率等等。

2.1 類關係圖

EurekaInstanceConfig 整體類關係如下圖:

  • 本文只解析紅圈部分類。

  • EurekaArchaius2ClientConfig 基於 Netflix Archaius 2.x 實現,目前還在開發中,因此暫不解析。

  • CloudInstanceConfig、Ec2EurekaArchaius2InstanceConfig 基於亞馬遜 AWS,大多數讀者和我對 AWS 都不瞭解,因此暫不解析。

2.2 配置屬性

點選 EurekaInstanceConfig 檢視配置屬性簡介,已經新增中文註釋,可以對照著英文註釋一起理解。這裡筆者摘出部分較為重要的屬性:

  • #getLeaseRenewalIntervalInSeconds() :租約續約頻率,單位:秒。應用不斷透過按照該頻率傳送心跳給 Eureka-Server 以達到續約的作用。當 Eureka-Server 超過最大頻率未收到續約(心跳),契約失效,進行應用移除。應用移除後,其他應用無法從 Eureka-Server 獲取該應用。

  • #getLeaseExpirationDurationInSeconds() :契約過期時間,單位:秒。

  • #getDataCenterInfo() :資料中心資訊。com.netflix.appinfo.DataCenterInfo,資料中心資訊介面,目前較為簡單,標記所屬資料中心名。一般情況下,我們使用 Name.MyOwn。介面實現程式碼如下:

    public interface DataCenterInfo {
    /**
    * 資料中心名列舉
    */

    enum Name {
       Netflix,
       Amazon,
       MyOwn
    }

    /**
    * @return 歸屬的資料中心名
    */

    Name getName();

    }

  • #getNamespace() :配置名稱空間,預設使用 eureka。以 eureka-client.properties 舉個例子:

    eureka.name=eureka
    eureka.port=8080
    eureka.vipAddress=eureka.mydomain.net
    • 每個屬性最前面的 eureka 即是配置名稱空間,一般情況無需修改。

  • TODO[0004]:健康檢查

  • #isInstanceEnabledOnit() :應用初始化後是否開啟。在「3. InstanceInfo」詳細解析。

2.3 AbstractInstanceConfig

com.netflix.appinfo.AbstractInstanceConfig,Eureka 應用實體配置抽象基類,主要實現一些相對通用的配置,實現程式碼如下:

public abstract class AbstractInstanceConfig implements EurekaInstanceConfig {

   /**
    * 契約過期時間,單位:秒
    */

   private static final int LEASE_EXPIRATION_DURATION_SECONDS = 90;
   /**
    * 租約續約頻率,單位:秒。
    */

   private static final int LEASE_RENEWAL_INTERVAL_SECONDS = 30;
   /**
    * 應用 https 埠關閉
    */

   private static final boolean SECURE_PORT_ENABLED = false;
   /**
    * 應用 http 埠開啟
    */

   private static final boolean NON_SECURE_PORT_ENABLED = true;
   /**
    * 應用 http 埠
    */

   private static final int NON_SECURE_PORT = 80;
   /**
    * 應用 https 埠
    */

   private static final int SECURE_PORT = 443;
   /**
    * 應用初始化後開啟
    */

   private static final boolean INSTANCE_ENABLED_ON_INIT = false;
   /**
    * 主機資訊
    * key:主機 IP 地址
    * value:主機名
    */

   private static final Pair hostInfo = getHostInfo();
   /**
    * 資料中心資訊
    */

   private DataCenterInfo info = new DataCenterInfo() {
       @Override
       public Name getName() {
           return Name.MyOwn;
       }
   };

   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;
   }

   // .... 省略 setting / getting 方法
}
  • #getHostInfo() 方法,獲取本地伺服器的主機名和主機 IP 地址。如果主機有多網絡卡或者虛擬機器網絡卡,這塊要小心,解決方式如下:

    • 手動配置本機的 hostname + etc/hosts 檔案,從而對映主機名和 IP 地址。

    • 使用 Spring-Cloud-Eureka-Client 的話,參考周立 —— 《Eureka服務註冊過程詳解之IpAddress》解決。

2.4 PropertiesInstanceConfig

com.netflix.appinfo.PropertiesInstanceConfig,基於配置檔案的 Eureka 應用實體配置抽象基類,實現程式碼如下:

public abstract class PropertiesInstanceConfig extends AbstractInstanceConfig implements EurekaInstanceConfig {

   /**
    * 名稱空間
    */

   protected final String namespace;
   /**
    * 配置檔案物件
    */

   protected final DynamicPropertyFactory configInstance;
   /**
    * 應用分組
    * 從 環境變數 獲取
    */

   private String appGrpNameFromEnv;

   public PropertiesInstanceConfig() {
       this(CommonConstants.DEFAULT_CONFIG_NAMESPACE);
   }

   public PropertiesInstanceConfig(String namespace) {
       this(namespace, new DataCenterInfo() {
           @Override
           public Name getName() {
               return Name.MyOwn;
           }
       });
   }

   public PropertiesInstanceConfig(String namespace, DataCenterInfo info) {
       super(info);
       // 設定 namespace,為 "." 結尾
       this.namespace = namespace.endsWith(".")
               ? namespace
               : namespace + ".";
       // 從 環境變數 獲取 應用分組
       appGrpNameFromEnv = ConfigurationManager.getConfigInstance()
               .getString(FALLBACK_APP_GROUP_KEY, Values.UNKNOWN_APPLICATION);
       // 初始化 配置檔案物件
       this.configInstance = Archaius1Utils.initConfig(CommonConstants.CONFIG_FILE_NAME);
   }

   @Override
   public String getAppGroupName() {
       return configInstance.getStringProperty(namespace + APP_GROUP_KEY, appGrpNameFromEnv).get().trim();
   }
}
  • configInstance 屬性,配置檔案物件,基於 Netflix Archaius 1.x 實現配置檔案的讀取。在 com.netflix.appinfo.PropertyBasedInstanceConfigConstants 可以看到配置檔案的每個屬性 KEY 。

  • appGrpNameFromEnv 屬性,應用分組,從環境變數中獲取。從 #getAppGroupName() 方法中,可以看到優先還是從配置檔案讀取。設定方法如下:

    System.setProperty(FALLBACK_APP_GROUP_KEY, "app_gropu_name");
    • FALLBACK_APP_GROUP_KEY,私有靜態變數,實際得使用 NETFLIX_APP_GROUP

    • com.netflix.config.ConfigurationManager 可以從環境變數獲取到值。

  • 呼叫 Archaius1Utils#initConfig(...) 方法,初始化讀取的配置檔案物件,實現程式碼如下:

    public final class Archaius1Utils {
    private static final Logger logger = LoggerFactory.getLogger(Archaius1Utils.class);

    private static final String ARCHAIUS_DEPLOYMENT_ENVIRONMENT = "archaius.deployment.environment";
    private static final String EUREKA_ENVIRONMENT = "eureka.environment";

    public static DynamicPropertyFactory initConfig(String configName) {
       // 配置檔案物件
       DynamicPropertyFactory configInstance = DynamicPropertyFactory.getInstance();
       // 配置檔案名
       DynamicStringProperty EUREKA_PROPS_FILE = configInstance.getStringProperty("eureka.client.props", configName);
       // 配置檔案環境
       String env = ConfigurationManager.getConfigInstance().getString(EUREKA_ENVIRONMENT, "test");
       ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_ENVIRONMENT, env);
       // 將配置檔案載入到環境變數
       String eurekaPropsFile = EUREKA_PROPS_FILE.get();
       try {
           ConfigurationManager.loadCascadedPropertiesFromResources(eurekaPropsFile);
       } catch (IOException e) {
           logger.warn(
                   "Cannot find the properties specified : {}. This may be okay if there are other environment "
                           + "specific properties or the configuration is installed with a different mechanism.",
                   eurekaPropsFile);

       }
       return configInstance;
    }

    }

    • 從環境變數 eureka.client.props,獲取配置檔案名。如果未配置,使用引數 configName,即 CommonConstants.CONFIG_FILE_NAME ( "eureka-client" )。

    • 從環境變數 eureka.environment ( EUREKA_ENVIRONMENT ),獲取配置檔案環境。

    • 呼叫 ConfigurationManager#loadCascadedPropertiesFromResources(...) 方法,讀取配置檔案到環境變數,首先讀取 ${eureka.client.props} 對應的配置檔案;然後讀取 ${eureka.client.props}-${eureka.environment} 對應的配置檔案。若有相同屬性,進行改寫。

2.5 MyDataCenterInstanceConfig

com.netflix.appinfo.MyDataCenterInstanceConfig,非 AWS 資料中心的 Eureka 應用實體配置實現類,實現程式碼如下:

public class MyDataCenterInstanceConfig extends PropertiesInstanceConfig implements EurekaInstanceConfig {

   public MyDataCenterInstanceConfig() {
   }

   public MyDataCenterInstanceConfig(String namespace) {
       super(namespace);
   }

   public MyDataCenterInstanceConfig(String namespace, DataCenterInfo dataCenterInfo) {
       super(namespace, dataCenterInfo);
   }

}

2.6 小結

一般情況下,使用 MyDataCenterInstanceConfig 配置 Eureka 應用實體。

在 Spring-Cloud-Eureka 裡,直接基於 EurekaInstanceConfig 介面重新實現了配置類,實際邏輯差別不大,在TODO[0007] :《Spring-Cloud-Eureka-Client》詳細解析。

3. InstanceInfo

com.netflix.appinfo.InstanceInfo應用實體資訊。Eureka-Client 向 Eureka-Server 註冊該物件資訊。註冊成功後,可以被其他 Eureka-Client 發現

本文僅分享 InstanceInfo 的初始化。InstanceInfo 裡和註冊發現相關的屬性和方法,暫時跳過。

com.netflix.appinfo.providers.EurekaConfigBasedInstanceInfoProvider,基於 EurekaInstanceConfig 建立 InstanceInfo 的工廠,實現程式碼如下:

  1: @Singleton
 2: public class EurekaConfigBasedInstanceInfoProvider implements Provider<InstanceInfo> {
 3:     private static final Logger LOG = LoggerFactory.getLogger(EurekaConfigBasedInstanceInfoProvider.class);
 4:
 5:     private final EurekaInstanceConfig config;
 6:
 7:     private InstanceInfo instanceInfo;
 8:
 9:     @Inject(optional = true)
10:     private VipAddressResolver vipAddressResolver = null;
11:
12:     @Inject
13:     public EurekaConfigBasedInstanceInfoProvider(EurekaInstanceConfig config) {
14:         this.config = config;
15:     }
16:
17:     @Override
18:     public synchronized InstanceInfo get() {
19:         if (instanceInfo == null) {
20:             // Build the lease information to be passed to the server based on config
21:             // 建立 租約資訊構建器,並設定屬性
22:             LeaseInfo.Builder leaseInfoBuilder = LeaseInfo.Builder.newBuilder()
23:                     .setRenewalIntervalInSecs(config.getLeaseRenewalIntervalInSeconds())
24:                     .setDurationInSecs(config.getLeaseExpirationDurationInSeconds());
25:
26:             // 建立 VIP地址解析器
27:             if (vipAddressResolver == null) {
28:                 vipAddressResolver = new Archaius1VipAddressResolver();
29:             }
30:
31:             // Builder the instance information to be registered with eureka server
32:             // 建立 應用實體資訊構建器
33:             InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder(vipAddressResolver);
34:
35:             // 應用實體編號
36:             // set the appropriate id for the InstanceInfo, falling back to datacenter Id if applicable, else hostname
37:             String instanceId = config.getInstanceId();
38:             DataCenterInfo dataCenterInfo = config.getDataCenterInfo();
39:             if (instanceId == null || instanceId.isEmpty()) {
40:                 if (dataCenterInfo instanceof UniqueIdentifier) {
41:                     instanceId = ((UniqueIdentifier) dataCenterInfo).getId();
42:                 } else {
43:                     instanceId = config.getHostName(false);
44:                 }
45:             }
46:
47:             // 獲得 主機名
48:             String defaultAddress;
49:             if (config instanceof RefreshableInstanceConfig) {
50:                 // Refresh AWS data center info, and return up to date address
51:                 defaultAddress = ((RefreshableInstanceConfig) config).resolveDefaultAddress(false);
52:             } else {
53:                 defaultAddress = config.getHostName(false);
54:             }
55:             // fail safe
56:             if (defaultAddress == null || defaultAddress.isEmpty()) {
57:                 defaultAddress = config.getIpAddress();
58:             }
59:
60:             // 設定 應用實體資訊構建器 的 屬性
61:             builder.setNamespace(config.getNamespace())
62:                     .setInstanceId(instanceId)
63:                     .setAppName(config.getAppname())
64:                     .setAppGroupName(config.getAppGroupName())
65:                     .setDataCenterInfo(config.getDataCenterInfo())
66:                     .setIPAddr(config.getIpAddress())
67:                     .setHostName(defaultAddress) // 主機名
68:                     .setPort(config.getNonSecurePort())
69:                     .enablePort(PortType.UNSECURE, config.isNonSecurePortEnabled())
70:                     .setSecurePort(config.getSecurePort())
71:                     .enablePort(PortType.SECURE, config.getSecurePortEnabled())
72:                     .setVIPAddress(config.getVirtualHostName()) // VIP 地址
73:                     .setSecureVIPAddress(config.getSecureVirtualHostName())
74:                     .setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())
75:                     .setStatusPageUrl(config.getStatusPageUrlPath(), config.getStatusPageUrl())
76:                     .setASGName(config.getASGName())
77:                     .setHealthCheckUrls(config.getHealthCheckUrlPath(),
78:                             config.getHealthCheckUrl(), config.getSecureHealthCheckUrl());
79:
80:             // 應用初始化後是否開啟
81:             // Start off with the STARTING state to avoid traffic
82:             if (!config.isInstanceEnabledOnit()) {
83:                 InstanceStatus initialStatus = InstanceStatus.STARTING;
84:                 LOG.info("Setting initial instance status as: " + initialStatus);
85:                 builder.setStatus(initialStatus);
86:             } else {
87:                 LOG.info("Setting initial instance status as: {}. This may be too early for the instance to advertise "
88:                          + "itself as available. You would instead want to control this via a healthcheck handler.",
89:                          InstanceStatus.UP);
90:             }
91:
92:             // 設定 應用實體資訊構建器 的 元資料( Metadata )集合
93:             // Add any user-specific metadata information
94:             for (Map.Entry mapEntry : config.getMetadataMap().entrySet()) {
95:                 String key = mapEntry.getKey();
96:                 String value = mapEntry.getValue();
97:                 builder.add(key, value);
98:             }
99:
100:             // 建立 應用實體資訊
101:             instanceInfo = builder.build();
102:
103:             // 設定 應用實體資訊 的 租約資訊
104:             instanceInfo.setLeaseInfo(leaseInfoBuilder.build());
105:         }
106:         return instanceInfo;
107:     }
108:
109: }
  • 該類實現 javax.inject.Provider 介面,設定 InstanceInfo 的生成工廠。感興趣的同學,可以點選《Google-Guice入門介紹》搜尋 Provider 關鍵字。目前處於試驗階段,未完成。

  • EurekaConfigBasedInstanceInfoProvider(config) 構造方法,設定生成 InstanceInfo 的 EurekaInstanceConfig 配置。

  • 呼叫 #get() 方法,根據 EurekaInstanceConfig 建立 InstanceInfo。InstanceInfo 的絕大數屬性和 EurekaInstanceConfig 是一致的 。實現程式碼如下:

    • 第 82 至 85 行 :應用不開啟,應用實體處於 STARTING 狀態。

    • 第 86 至 90 行 :應用開啟,應用實體處於 UP 狀態。

    • 使用應用初始化後不開啟,可以透過呼叫 ApplicationInfoManager#setInstanceStatus(...) 方法改變應用實體狀態,在《Eureka 原始碼解析 —— 應用實體註冊發現 (一)之註冊》「2.1 應用實體資訊複製器」有詳細解析。

    • 使用 #resolveDeploymentContextBasedVipAddresses() 方法,將 VIP地址 裡的 ${(.*?)} 查詢配置檔案裡的鍵值進行替換。例如,${eureka.env}.domain.com,查詢配置檔案裡的鍵 ${eureka.env} 對應值進行替換。TODO[0005]:除錯下來,發現 Archaius 已經替換,等到找到答案修改此處。

    • 第 21 至 24 行 :建立租約資訊構建器( com.netflix.appinfo.LeaseInfo.Builder ),並設定 renewalIntervalInSecs / durationInSecs 屬性。

    • 第 26 至 29 行 :建立 VIP地址解析器( com.netflix.appinfo.providers.VipAddressResolver )。實現程式碼如下:

      // VipAddressResolver.java
      public interface VipAddressResolver {
      String resolveDeploymentContextBasedVipAddresses(String vipAddressMacro);

      }

      public class Archaius1VipAddressResolver implements VipAddressResolver {

      private static final Pattern VIP_ATTRIBUTES_PATTERN = Pattern.compile("\\$\\{(.*?)\\}");

      @Override
      public String resolveDeploymentContextBasedVipAddresses(String vipAddressMacro) {
         if (vipAddressMacro == null) {
             return null;
         }
         String result = vipAddressMacro;
         // 替換運算式
         Matcher matcher = VIP_ATTRIBUTES_PATTERN.matcher(result);
         while (matcher.find()) {
             String key = matcher.group(1);
             String value = DynamicPropertyFactory.getInstance().getStringProperty(key, "").get();
          logger.debug("att:" + matcher.group());
         logger.debug(", att key:" + key);
         logger.debug(", att value:" + value);
         logger.debug("");

         result = result.replaceAll("\\$\\{" + key + "\\}", value);
         matcher = VIP_ATTRIBUTES_PATTERN.matcher(result);
      }

      return result;

      }
      }

    • 第 32 至 33 行 :建立應用實體資訊構建器( com.netflix.appinfo.InstanceInfo.Builder )。

    • 第 35 至 45 行 :獲得應用實體編號( instanceId )。

    • 第 47 至 58 行 :獲得主機名。

    • 第 60 至 78 行 :設定應用實體資訊構建器的屬性。

    • 第 80 至 90 行 :應用初始化後是否開啟。

    • 第 92 至 98 行 :設定應用實體資訊構建器的元資料( Metadata )集合。

    • 第 100 至 101 行 :建立應用實體資訊( com.netflix.appinfo.InstanceInfo )。

    • 第 103 至 104 行 :設定應用實體資訊的租約資訊( com.netflix.appinfo.InstanceInfo )。

4. ApplicationInfoManager

com.netflix.appinfo.ApplicationInfoManager,應用資訊管理器。實現程式碼如下:

public class ApplicationInfoManager {

   /**
    * 單例
    */

   private static ApplicationInfoManager instance = new ApplicationInfoManager(null, null, null);

   /**
    * 狀態變更監聽器
    */

   protected final Map listeners;
   /**
    * 應用實體狀態匹配
    */

   private final InstanceStatusMapper instanceStatusMapper;
   /**
    * 應用實體資訊
    */

   private InstanceInfo instanceInfo;
   /**
    * 應用實體配置
    */

   private EurekaInstanceConfig config;

   // ... 省略其它構造方法

   public ApplicationInfoManager(EurekaInstanceConfig config, InstanceInfo instanceInfo, OptionalArgs optionalArgs) {
       this.config = config;
       this.instanceInfo = instanceInfo;
       this.listeners = new ConcurrentHashMap();
       if (optionalArgs != null) {
           this.instanceStatusMapper = optionalArgs.getInstanceStatusMapper();
       } else {
           this.instanceStatusMapper = NO_OP_MAPPER;
       }

       // Hack to allow for getInstance() to use the DI'd ApplicationInfoManager
       instance = this;
   }

   // ... 省略其它方法
}
  • listeners 屬性,狀態變更監聽器集合。在《Eureka 原始碼解析 —— 應用實體註冊發現 (一)之註冊》「2.1 應用實體資訊複製器」有詳細解析。

  • instanceStatusMapper 屬性,應用實體狀態匹配。實現程式碼如下:

    // ApplicationInfoManager.java

    public static interface InstanceStatusMapper {
    }

    private static final InstanceStatusMapper NO_OP_MAPPER = new InstanceStatusMapper() {
      @Override
      public InstanceStatus map(InstanceStatus prev) {
          return prev;
      }
    };
    • #map 方法,根據傳入 pre 引數,轉換成對應的應用實體狀態。

    • 預設情況下,使用 NO_OP_MAPPER 。一般情況下,不需要關註該類。

666. 彩蛋

涉及到配置,內容初看起來會比較多,慢慢理解後,就會變得很“囉嗦”,請保持耐心。

胖友,分享一個朋友圈可好。

贊(0)

分享創造快樂