[享學Eureka] 二、Eureka的最核心概念:InstanceInfo實例資訊

  • 2020 年 3 月 27 日
  • 筆記

程式碼下載地址:https://github.com/f641385712/netflix-learning

前言

如果說Eureka里最核心的一個對象/類是什麼,我想當屬InstanceInfo了。它貫穿於Client、Server倆端,承載著一個實例的所有描述,它是事件的主體,一切皆圍繞著它來進行。

另外,還需注意的是Eureka使用的是Guice作為它的依賴注入DI基礎組件,因此源碼處你進場能看見@Singleton、@Inject等註解的使用,為了不妨礙你的閱讀和研究,建議可先認識下Google Guice這個輕量級依賴注入的相關支援,這裡我也很暖心的給你準備好了直達電梯:3分鐘帶你了解輕量級依賴注入框架Google Guice【享學Java】


正文

本文會逐個介紹InstanceInfo以及LeaseInfo的屬性,最後輔以一個示例講解。可以作為字典、參考文章來使用,建議搜藏or轉發。


InstanceInfo 實例資訊

InstanceInfo代表一個實例的資訊,是一個趨近於POJO的類,因此理解起來並不難。但由於欄位眾多(30+個),因此先圍繞它理解一圈,基本能窺探到註冊中心的全貌,所以還是蠻重要的。


成員屬性

// @ProvidedBy是Guice的註解,用於在Guice的DI依賴注入時生成一個InstanceInfo實例  @ProvidedBy(EurekaConfigBasedInstanceInfoProvider.class)  // 序列化/反序列化化使用訂製的序列化器  @Serializer("com.netflix.discovery.converters.EntityBodyConverter")  // 支援xml的序列化/反序列化  它倆都用instance前綴包裹著。形如{"instance" : {...}} 才行  @XStreamAlias("instance")  // 支援json的序列化/反序列化  @JsonRootName("instance")  public class InstanceInfo {        private volatile String instanceId;      private volatile String appName;      @Auto      private volatile String appGroupName;      private volatile String ipAddr;      @Deprecated      private volatile String sid = SID_DEFAULT;      private volatile int port = DEFAULT_PORT;      private volatile int securePort = DEFAULT_SECURE_PORT;      @Auto      private volatile String homePageUrl;      @Auto      private volatile String statusPageUrl;      @Auto      private volatile String healthCheckUrl;      @Auto      private volatile String secureHealthCheckUrl;      @Auto      private volatile String vipAddress;      @Auto      private volatile String secureVipAddress;      @XStreamOmitField      private String statusPageRelativeUrl;      @XStreamOmitField      private String statusPageExplicitUrl;      @XStreamOmitField      private String healthCheckRelativeUrl;      @XStreamOmitField      private String healthCheckSecureExplicitUrl;      @XStreamOmitField      private String vipAddressUnresolved;      @XStreamOmitField      private String secureVipAddressUnresolved;      @XStreamOmitField      private String healthCheckExplicitUrl;      @Deprecated      private volatile int countryId = DEFAULT_COUNTRY_ID; // Defaults to US      private volatile boolean isSecurePortEnabled = false;      private volatile boolean isUnsecurePortEnabled = true;      private volatile DataCenterInfo dataCenterInfo;      private volatile String hostName;      private volatile InstanceStatus status = InstanceStatus.UP;      private volatile InstanceStatus overriddenStatus = InstanceStatus.UNKNOWN;      @XStreamOmitField      private volatile boolean isInstanceInfoDirty = false;      private volatile LeaseInfo leaseInfo;      @Auto      private volatile Boolean isCoordinatingDiscoveryServer = Boolean.FALSE;      @XStreamAlias("metadata")      private volatile Map<String, String> metadata;      @Auto      private volatile Long lastUpdatedTimestamp;      @Auto      private volatile Long lastDirtyTimestamp;      @Auto      private volatile ActionType actionType;      @Auto      private volatile String asgName;      private String version = VERSION_UNKNOWN;    }

需要強調一點:在Guice下注入該實例時由EurekaConfigBasedInstanceInfoProvider負責創建;但是在Spring Cloud下該實例由自己提供的InstanceInfoFactory完成創建的。

Spring Cloud下完全沒有使用Guice來管理依賴,而是自己實現的管理,畢竟它也支援@Inject等標準註解嘛,接手過來比較容易

  • instanceId:實例id。在同一個應用appName的範圍內是必須是惟一的
    • 你常見的在Spring Cloud的配置是:eureka.instance.instance-id = ${spring.cloud.client.ipAddress}:${spring.application.name}:${server.port}:@project.version@
      • 在eureka項目本身的配置是:eureka.instanceId = xxxxxxx,請注意區別
      • 此處project.version是引用maven裡面的屬性,因為Spring Boot的parent包將maven中默認的${*}修改成了@*@,所以引用maven屬性要用@@
  • appName:應用名。如ACCOUNT(同一應用可以有N多個實例)
    • 此屬性最終會被序列化app這個key,如app=ACCOUNT
  • appGroupName:應用組名。多個應用可分組,很少用,一般為null
  • ipAddr:本實例的ip地址。如ipAddr=192.168.1.100
  • sid:已過期屬性。不用搭理
  • port:埠號。默認值是7001
  • securePort:安全埠號。默認值是7002
  • homePageUrl:主頁。如homePageUrl=http://localhost:8080
    • 一般使用佔位符形式配置xxx.homePageUrl = http://${mynamespace.hostname}:7001
    • 這個佔位符在運行期會被用hostname屬性替換掉,而hostname屬於必配的屬性
  • statusPageUrl:狀態頁。如http://localhost:8080/actuator/info
    • 同上,一般也使用佔位符形式
  • healthCheckUrl:健康檢查的URL(Rest)。如http://localhost:8080/actuator/health
  • secureHealthCheckUrl:一般不用,為null即可。
  • vipAddress:邏輯地址。如vipAddress=ACCOUNT
    • 關於它在eureka中如何使用,會有詳解
  • secureVipAddress:略
  • statusPageRelativeUrl:相對URL。最終會拼接全了給statusPageUrl賦值,如你/api/v1/status最終給statusPageUrl賦值為(舉個例子):http://localhost:8080//api/v1/status
  • statusPageExplicitUrl:明確的URL。最終處理後給statusPageUrl賦值。如你配置了${mynamespace.hostname}/api/v1/status,那麼最終佔位符部分會被替換為hostname的值,從而效果同上
  • healthCheckRelativeUrl:略
  • healthCheckSecureExplicitUrl:略
  • vipAddressUnresolved:略
  • secureVipAddressUnresolved:略
  • healthCheckExplicitUrl:略
  • countryId:過期。國家ID,默認值是1表示美國,不用搭理
  • isSecurePortEnabled:是否啟用安全埠,securePort有值就是true,顯然默認是false
  • isUnsecurePortEnabled:默認是true
  • dataCenterInfo:數據中心。
    • 關於數據中心的概念,後文也會有詳細分析其使用
  • hostName:必須的。主機名,如hostName=LP-BJ4556.baidu.work
    • 說明:Spring Cloud下的服務註冊實例id默認使用主機名,而非ip地址,畢竟http://hostname:port這種方式也是可以訪問的介面的(當然這個可配)
  • status:實例狀態。默認值是InstanceStatus.UP,是個枚舉
    • starting:實例初始化狀態,此狀態主要給實例預留初始化時間
    • down:當健康檢查失敗時,實例的狀態轉變到down
    • up:正常服務狀態
    • out_of_service:不參與接收服務 。但是服務正常
    • unknown:未知狀態
  • overriddenStatus:eureka解決狀態覆蓋而存在的屬性欄位。它的默認是是InstanceStatus.UNKNOWN
    • 狀態覆蓋也是ereuka里的一個小亮點,後有詳細介紹
  • isInstanceInfoDirty:標註實例數據是否是髒的(client和server端對比)
    • true:表示 InstanceInfo 在 Eureka-Client 和 Eureka-Server 數據不一致,需要註冊。
      • 每次 InstanceInfo 發生屬性變化時,以及InstanceInfo 剛被創建時,會標記此值是true
      • 當符合條件時,InstanceInfo 不會立即向 Eureka-Server 註冊,而是後台執行緒定時註冊(當然若開啟了eureka.shouldOnDemandUpdateStatusChange = true時是立即註冊)
        • 該配置屬於EurekaClientConfig端配置哦,不屬於Instance配置
    • false:表示一致,不需要做額外動作
  • leaseInfo:一個com.netflix.appinfo.LeaseInfo對象,表示續約相關資訊,見文下
  • isCoordinatingDiscoveryServer:是否是協調Server。默認是false,不用搭理
  • metadata自定義元數據,可以是任何k-v
    • 關於eureka的元數據還是比較重要的,後有專門詳解
  • lastUpdatedTimestamp:上次修改時間
  • lastDirtyTimestamp:上次標記為Dirty的時間
  • actionType:動作類型。如ADDED/MODIFIED/DELETED
  • asgName: 與此實例相關聯 AWS自動縮放組名稱
    • 此項配置是在AWS環境專門使用的實例啟動,它已被用於流量停用後自動把一個實例退出服務。實際可忽略
  • version:版本。默認值是unknown,已標記過期,可不用搭理

構建方式

任何對象都需要構建嘛,哪怕是自動的。可以想到這麼多屬性,那必然採取的就是Builder方式構建嘍。其實它提供了全參數的構造器,但那不會使用~

若我們自己構建,肯定這麼玩:

InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder()          .setInstanceId("account-001")          .setHostName("localhost")          .setIPAddr("127.0.0.1")          .setDataCenterInfo(new MyDataCenterInfo(DataCenterInfo.Name.MyOwn))          .setAppName("account") // 大小寫無所謂          .build();

對於使用Builder構建也比較簡單,但有如下方法特別指出,有一定小邏輯,可以稍加註意:

InstanceInfo.Builder    	// 命名空間  一個InstanceInfo一個命名空間  	// 默認的命名空間值是:eureka 這就是為何你的配置都是eureka.xxx的原因嘍  	private String namespace;      public Builder setNamespace(String namespace) {          this.namespace = namespace.endsWith(".") ? namespace : namespace + ".";          return this;      }      	// 支援相對URL:explicitUrl 以及絕對URL:explicitUrl  	// 相對URL會幫你拼接上http、埠等前綴  	// 絕對url就幫你處理下佔位符即可(若無佔位符就不處理嘍)      public Builder setHomePageUrl(String relativeUrl, String explicitUrl) {          String hostNameInterpolationExpression = "${" + namespace + "hostname}";          if (explicitUrl != null) {              result.homePageUrl = explicitUrl.replace(                      hostNameInterpolationExpression, result.hostName);          } else if (relativeUrl != null) {              result.homePageUrl = HTTP_PROTOCOL + result.hostName + COLON                      + result.port + relativeUrl;          }          return this;      }      // 當然,你也可以來一個完全絕對的URL(並不太推薦)      public Builder setHomePageUrlForDeser(String homePageUrl) {          result.homePageUrl = homePageUrl;          return this;      }    	... // 像什麼setStatusPageUrl、setHealthCheckUrls等都支援這兩種方式    	// appName應用名稱是必須的。其實還有InstanceId hostName等也都是必須的      public InstanceInfo build() {          if (!isInitialized()) {              throw new IllegalStateException("name is required!");          }          return result;      }      public boolean isInitialized() {          return (result.appName != null);      }

關於各URL的設值,推薦使用相對URL。另外,該實例的創建一般不通過手動顯示各種膚質,而是通過配置的方式倆指定,具體可參見下篇文章EurekaInstanceConfig配置介面。


屬性讀取

同樣的道理,亦僅需關注其一些特殊方法而已:

InstanceInfo:    	// key使用的是app      @JsonProperty("app")      public String getAppName() {          return appName;      }    	// 返回實例的唯一id。並不是直接返回instanceId哦      @JsonIgnore      public String getId() {      	// 若你自己配置了instanceId就直接返回          if (instanceId != null && !instanceId.isEmpty()) {              return instanceId;    		// 否則取值字數據中心。  		// 說明:`MyDataCenterInfo`實現了UniqueIdentifier  		// AmazonInfo實現了此介面          } else if (dataCenterInfo instanceof UniqueIdentifier) {              String uniqueId = ((UniqueIdentifier) dataCenterInfo).getId();              if (uniqueId != null && !uniqueId.isEmpty()) {                  return uniqueId;              }          }          return hostName;      }        @JsonIgnore      public boolean isPortEnabled(PortType type) {          if (type == PortType.SECURE) {              return isSecurePortEnabled;          } else {              return isUnsecurePortEnabled;          }      }    	// 只要狀態Status發生了變更,那麼一定會setIsDirty();  	// 標記此實例已經dirty了,需要中心註冊      public synchronized InstanceStatus setStatus(InstanceStatus status) {          if (this.status != status) {              InstanceStatus prev = this.status;              this.status = status;              setIsDirty();              return prev;          }          return null;      }      // 當然也可以對dirty進行免疫      public synchronized void setStatusWithoutDirty(InstanceStatus status) {          if (this.status != status) {              this.status = status;          }      }      public synchronized void setOverriddenStatus(InstanceStatus status) {          if (this.overriddenStatus != status) {              this.overriddenStatus = status;          }      }    	//增加元數據資訊時,也會標記dirty了  	// 它會被ApplicationInfoManager#registerAppMetadata調用      synchronized void registerRuntimeMetadata(Map<String, String> runtimeMetadata) {          metadata.putAll(runtimeMetadata);          setIsDirty();      }

另外,它還提供一個static的工具方法可以直接調用:獲取該Info所在的可用區zone

InstanceInfo:    	// 獲取到當前實例InstanceInfo所在的zone區  	// availZones:可用區(若為空就是default,否則就取第一個)  	// 若實例使用的數據中心是Amazon類型,那就從其元數據裡面拿出availabilityZone可用區(若配置了的話)      public static String getZone(String[] availZones, InstanceInfo myInfo) {          String instanceZone = ((availZones == null || availZones.length == 0) ? "default" : availZones[0]);          if (myInfo != null && myInfo.getDataCenterInfo().getName() == DataCenterInfo.Name.Amazon) {                String awsInstanceZone = ((AmazonInfo) myInfo.getDataCenterInfo())                      .get(AmazonInfo.MetaDataKey.availabilityZone);              if (awsInstanceZone != null) {                  instanceZone = awsInstanceZone;              }            }          return instanceZone;      }

邏輯總結為一句話:若你使用的是Amazon數據中心類型,那麼你可以通過元數據availabilityZone來配置當前實例所在的zone(關於AmazonInfo的元數據後文也有所講解)。否則它所在的zone就是方法入參或者是defualt


LeaseInfo 續租資訊

續租資訊。續租是Eureka里特別重要的一個概念,Eureka會決定根據此租約中的EurekaInstanceConfig.getLeaseExpirationDurationInSeconds()中設置的持續時間將實例從其視圖中移除。租約還記錄了上次續租的時間

// 它的JSON形式表示形式是:{"leaseInfo":{xxx}}  @JsonRootName("leaseInfo")  public class LeaseInfo {    	// 默認值們      public static final int DEFAULT_LEASE_RENEWAL_INTERVAL = 30;      public static final int DEFAULT_LEASE_DURATION = 90;        private int renewalIntervalInSecs = DEFAULT_LEASE_RENEWAL_INTERVAL;      private int durationInSecs = DEFAULT_LEASE_DURATION;        private long registrationTimestamp;      private long lastRenewalTimestamp;      private long evictionTimestamp;      private long serviceUpTimestamp;    }
  • renewalIntervalInSecs:續租間隔時間(多長時間續約一次),默認是30s。
    • 用於Client客戶端:每隔30s上報續約一次
  • durationInSecs:續約持續時間(過期時間),默認是90s。90s倒計時,期間沒有收到續約就會執行對應動作
    • 用於Server服務端,90s內木有收到心跳,就T除掉對應實例
  • registrationTimestamp:租約的註冊時間
  • lastRenewalTimestamp:最近一次的續約時間(服務端記錄,用於倒計時的起始值)
  • evictionTimestamp:下線時間(服務的上、下線屬於比較頻繁的操作。但是此時服務實例並未T除去)
  • serviceUpTimestamp:上線時間

程式碼示例

創建一個InstanceInfo這裡就不手動Builder了,這裡我使用Guice依賴注入的方式來得到一個實例:

@Test  public void fun5(){      Injector injector = Guice.createInjector(new EurekaModule());        InstanceInfo instance1 = injector.getInstance(InstanceInfo.class);      InstanceInfo instance2= injector.getInstance(InstanceInfo.class);      System.out.println(instance1.getId());      System.out.println(instance2.getId());        System.out.println(System.identityHashCode(instance1));      System.out.println(System.identityHashCode(instance2));  }

說明,因為我沒有配置數據中心,因此我需要加個配置eureka.validateInstanceId = false,這樣運行程式,輸出為:

2.0.0.2  2.0.0.2  1434234664  1434234664

what???竟然是單例。是的,InstanceInfo全局僅需要一個,畢竟你一個應用就是一個實例嘛,那麼為何呢???

究其原因就在這裡:

@ProvidedBy(EurekaConfigBasedInstanceInfoProvider.class)  public class InstanceInfo { ... }    @Singleton // 單例  public class EurekaConfigBasedInstanceInfoProvider implements Provider<InstanceInfo> { ... }

Spring Cloud下使用的是InstanceInfoFactory倆創建實例,然後交給ApplicationInfoManager去管理的~


總結

關於Eureka的最核心概念:InstanceInfo實例資訊就介紹到這了,別看僅是一個資訊類,內容還真不少。本文對每個屬性均做了介紹,希望能作為字典可以查詢,可以幫助到你。