[享學Eureka] 二、Eureka的最核心概念:InstanceInfo實例資訊
- 2020 年 3 月 27 日
- 筆記
前言
如果說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屬性要用@@
- 在eureka項目本身的配置是:
- 你常見的在Spring Cloud的配置是:
appName
:應用名。如ACCOUNT
(同一應用可以有N多個實例)- 此屬性最終會被序列化
app
這個key,如app=ACCOUNT
- 此屬性最終會被序列化
appGroupName
:應用組名。多個應用可分組,很少用,一般為nullipAddr
:本實例的ip地址。如ipAddr=192.168.1.100
sid
:已過期屬性。不用搭理port
:埠號。默認值是7001securePort
:安全埠號。默認值是7002homePageUrl
:主頁。如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,顯然默認是falseisUnsecurePortEnabled
:默認是truedataCenterInfo
:數據中心。- 關於數據中心的概念,後文也會有詳細分析其使用
hostName
:必須的。主機名,如hostName=LP-BJ4556.baidu.work
- 說明:Spring Cloud下的服務註冊實例id默認使用主機名,而非ip地址,畢竟
http://hostname:port
這種方式也是可以訪問的介面的(當然這個可配)
- 說明:Spring Cloud下的服務註冊實例id默認使用主機名,而非ip地址,畢竟
status
:實例狀態。默認值是InstanceStatus.UP
,是個枚舉starting
:實例初始化狀態,此狀態主要給實例預留初始化時間down
:當健康檢查失敗時,實例的狀態轉變到downup
:正常服務狀態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:表示一致,不需要做額外動作
- true:表示 InstanceInfo 在 Eureka-Client 和 Eureka-Server 數據不一致,需要註冊。
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實例資訊就介紹到這了,別看僅是一個資訊類,內容還真不少。本文對每個屬性均做了介紹,希望能作為字典可以查詢,可以幫助到你。