SpringCloud從入門到進階(三)——原始碼探究Eureka叢集之replicas的unavailable故障
內容
本節從原始碼的角度探討了Eureka控制檯中為何replicas(副本)顯示unavailable(不可用)的原因。在原始碼層級解讀了Eureka Server的replicas是如何解析,以及replica的狀態是如何判定。
版本
IDE:IDEA 2017.2.2 x64
JDK:1.8.0_171
manve:3.3.3
SpringBoot:1.5.9.RELEASE
SpringCloud:Dalston.SR1
適合人群
Java開發人員
用詞釋義
Eureka例項:表示啟動的Eureka Server專案。
peer:同伴,Eureka叢集中所有Eureka例項之間互稱peer。
replica:副本,由於Eureka叢集中的Eureka例項之間相互同步註冊資訊,Eureka例項稱其他Eureka例項為自己的replica。
說明
轉載請說明出處:SpringCloud從入門到進階(三)——原始碼探究Eureka叢集之replicas的unavailable故障
參考
SpringCloud從入門到進階(二)——註冊中心Eureka的偽分散式部署
內容
上一節講解了Eureka Server(下面簡稱Eureka)的偽分散式部署。但是在專案部署後,通過Eureka的管理頁面發現所有Eureka例項的replicas(副本)都是unavailable(不可用)狀態。但是此時部署的三個Eureka例項都正常執行著,難道出現靈異事件了嗎?筆者對此問題感到非常費解。因為寫部落格的過程中部署過多次Eureka叢集,之前是不存在此問題的(如下圖所示)。
在網上查閱了一些資料,有些網友是由於一些基礎的配置有誤導致該問題出現,比如:Eureka例項的spring.application.name配置的不一致、serviceUrl的配置中使用了localhost等等。最後提到了preferIpAddress的設定,筆者正是配置了這個屬性之後才出現本篇文章要解釋的這個問題!
上一節也提到,預設情況下,Eureka Client使用主機名進行註冊,同時,他們之間的呼叫也是通過主機名實現。將eureka.instance.preferIpAddress=true
後,Eureka Client便通過IP地址進行註冊和複製的互相呼叫。但是,這又跟replicas的unavailable故障有什麼關係呢?下面,我們從原始碼角度分析下Eureka的replica是如何解析,以及replica的狀態是如何判定的。
回顧上節的yaml中peer1的部分配置
spring: profiles: peer1 application: name: application-eurekaserver server: port: 7001 eureka: instance: #設定例項的hostname hostname: eureka7001.com instance-id: springcloud-eurekaserver-7001 #要求Client通過ip的方式進行註冊 prefer-ip-address: true client: #不將eureka server 註冊進來,會提示unavailable-replicas #預設情況下,Eureka Server會向自己註冊,這時需要配置eureka.client.registerWithEureka 和 eureka.client.fetchRegistry為false,防止自己註冊自己。 register-with-eureka: true fetch-registry: true service-url: #defaultZone中填寫的URL必須包括字尾/eureka,否則各eureka server之間不能通訊 #defaultZone為預設的Zone,來源於AWS的概念。區域(Region)和可用區(Availability Zone,AZ)是AWS的另外兩個概念。區域是指伺服器所在的區域, #比如北美洲、南美洲、歐洲和亞洲等,每個區域一般由多個可用區組成。 在本案例中defaultZone是指Eureka Server的註冊地址。 defaultZone: http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka
DS Replicas之Eureka副本的解析
Eureka的副本是在專案啟動時,通過解析配置檔案中eureka.client.serviceUrl
屬性獲得同區的Eureka peer的url地址;然後判斷這些url地址是否指向當前啟動例項自身,把未指向當前例項的url作為Eureka peer的url。程式碼位於com.netflix.eureka.cluster.PeerEurekaNodes
類的resolvePeerUrls
方法。
原始碼
//用於解析Eureka叢集中Eureka例項的url地址 protected List<String> resolvePeerUrls() { //myInfo物件包含了當前例項的Eureka註冊資訊,比如例項id,應用名,IP地址,埠號,主機名等資訊,詳見下問"除錯"。 InstanceInfo myInfo = applicationInfoManager.getInfo(); //當前例項所在區域,預設是defaultZone,在eureka.client.serviceUrl中配置。 String zone = InstanceInfo.getZone(clientConfig.getAvailabilityZones(clientConfig.getRegion()), myInfo); //解析當前例項在eureka.client.serviceUrl引數配置的所有Eureka例項的url地址。詳見下文“除錯”。 List<String> replicaUrls = EndpointUtils .getDiscoveryServiceUrls(clientConfig, zone, new EndpointUtils.InstanceInfoBasedUrlRandomizer(myInfo)); int idx = 0; //遍歷replicaUrls while (idx < replicaUrls.size()) { //判斷replicaUrls中的url是否指向當前例項,詳見下文“補充”。 if (isThisMyUrl(replicaUrls.get(idx))) { //如果url指向當前例項,那麼該url就不能被認定為是當前例項的replica(副本) replicaUrls.remove(idx); } else { idx++; } } return replicaUrls; }
除錯
在該方法打斷點,專案啟動過程中,會執行到此斷點。
myInfo
myInfo物件包含了當前例項的註冊資訊,比如例項id,應用名,IP地址,埠號,主機名等資訊。可以發現,當配置eureka.instance.preferIpAddress=true
後,例項的主機名就是該例項的IP地址,使用eureka.instance.hostname
引數修改也是無效的!
replicaUrls
replicaUrls是當前例項在eureka.client.serviceUrl引數配置的所有Eureka peer的url地址。由配置檔案可知,當前例項中配置的兩個Eureka例項的url地址是http://eureka7002.com:7002/eureka
和http://eureka7003.com:7003/eureka
,除錯結果與配置一致。
本段小結
由peer1的配置檔案可知,eureka.client.serviceUrl引數為http://eureka7002.com:7002/eureka
和http://eureka7003.com:7003/eureka
。這就是當前Eureka例項的兩個peer。由於開啟preferIpAddress,因此當前Eureka例項的主機名為ip地址,ip與eureka7002.com和eureka7003.com在字面量上都不相等,由原始碼可知,當前Eureka例項會認為eureka7002.com和eureka7003.com這兩個例項是它的replica。於是就有了:
unavailable-replicas之Eureka副本的狀態判定
Eureka副本的狀態判定是通過遍歷當前Eureka例項的peer,將其url地址與當前所有可用的Eureka例項的主機名進行比對,來判斷此peer是否可用。程式碼位於com.netflix.eureka.util.StatusUtil
類的getStatusInfo
方法。
原始碼
public StatusInfo getStatusInfo() { //例項資訊的構造器 StatusInfo.Builder builder = StatusInfo.Builder.newBuilder(); //線上的replica的數量 int upReplicasCount = 0; //線上的replica的主機名 StringBuilder upReplicas = new StringBuilder(); //不線上的replica的主機名 StringBuilder downReplicas = new StringBuilder(); //所有replica的主機名 StringBuilder replicaHostNames = new StringBuilder(); //遍歷當前Eureka例項的peer(下稱node),通過peerEurekaNode字面意思也能明白,詳見除錯。 for (PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) { //如果有多個replica,用","號隔開主機名。 if (replicaHostNames.length() > 0) { replicaHostNames.append(", "); } //將node的url地址加入到replicaHostNames中 replicaHostNames.append(node.getServiceUrl()); //通過isReplicaAvailable()方法判斷node是否可用 if (isReplicaAvailable(node.getServiceUrl())) { //如果node可用,則將node的名稱加入到upReplicas upReplicas.append(node.getServiceUrl()).append(','); upReplicasCount++; } else { //如果node不可用,則將node的名稱加入到downReplicas downReplicas.append(node.getServiceUrl()).append(','); } } //向builder中新增replica的資訊 builder.add("registered-replicas", replicaHostNames.toString()); builder.add("available-replicas", upReplicas.toString()); builder.add("unavailable-replicas", downReplicas.toString()); // 預設情況下,該條件為false if (peerEurekaNodes.getMinNumberOfAvailablePeers() > -1) { builder.isHealthy(upReplicasCount >= peerEurekaNodes.getMinNumberOfAvailablePeers()); } //向builder中新增Eureka例項的資訊 builder.withInstanceInfo(this.instanceInfo); //通過builder構造當前例項的資訊 return builder.build(); } //判定url指向的replica是否可用 private boolean isReplicaAvailable(String url) { try { //從容器registry中拿到名為“application-eurekaserver”的微服務app,app中包含了所有可用的Eureka例項。詳見下文“除錯”。這裡也就解釋了為什麼application.name不同的Eureka例項不可用。 Application app = registry.getApplication(myAppName, false); //如果不存在可用的peer,直接返回false if (app == null) { return false; } //遍歷應用中所有可用的peer for (InstanceInfo info : app.getInstances()) { //判斷給定的url是否指向可用的peer,如果是,那麼該url表示的replica是可用的,返回true。 //由於開啟了preferIpAddress,例項的主機名就是該例項的IP地址,不再是eureka.instance.hostname的屬性值,因此url解析的主機名與ip地址不一致,會被誤認為當前線上的eureka例項並不是url指向的例項,因此url代表的replica被誤認為不可用。 if (peerEurekaNodes.isInstanceURL(url, info)) { return true; } } } catch (Throwable e) { logger.error("Could not determine if the replica is available ", e); } return false; }
除錯
在該方法打斷點,專案啟動後訪問Eureka例項的管理介面時,會執行到此斷點。
peerEurekaNodes
peerEurekaNodes中包含了當前Eureka例項的peer,即主機名為eureka7002.com和eureka7003.com的Eureka例項。
名為“APPLICATION-EUREKASERVER”的服務-app
從registry容器中拿到名為“application-eurekaserver”的微服務app,app中包含了所有可用的Eureka例項。通過除錯可以看到這三個例項分id分別為:springcloud-eurekaserver-7001、springcloud-eurekaserver-7002、springcloud-eurekaserver-7003。這正是我們啟動的三個Eureka例項,除錯結果與事實一致。
結論
由於開啟了preferIpAddress,例項的主機名就是該例項的IP地址,不再是eureka.instance.hostname的屬性值。因此拿著replica的url(http://eureka7002.com:7002/eureka/
)與例項的主機名(ip地址,比如:192.168.99.1)進行字面量比較,兩者肯定是不相等。從而被誤認為當前線上的eureka例項並不是url指向的例項,因此url代表的replica被誤認為不可用。
全文總結
再用白話解說為何例項可以識別到兩個replica,但是卻認為這些replica不可用。
當前例項會從配置檔案serviceUrl屬性中的url中刨除指向自己的url,將剩下的url指向的例項認定為replica。在判斷replica的可用性時,拿著這些url跟線上的Eureka Server例項的主機名比較,看這些url是否指向線上的例項。但是由於開啟了preferIpAddress,線上例項的主機名變成ip地址,因此拿著replica的url的主機名跟ip地址做equals判斷時,二者必然不相等,也就導致了replica不可用的情況。一句話說,配置時用的域名,比較時用的ip地址,都是直接比較二者是否相等惹的禍。
如何解決此問題
開啟preferIpAddress後,執行在同一個主機上的所有Eureka例項都有相同的主機號,即主機的IP地址。因此在判斷replica狀態的時候必定會判為不可用。只有在真實的分散式主機上部署不同的Eureka例項,結合正確的配置*(serviceUrl需要配置為ip地址),才能做到開啟preferIpAddress後讓replica的狀態顯示正常。詳細過程請看下文:
補充
isInstanceURL()方法
isInstanceURL()方法如何判定給定url是否指向給定的例項instance。
//該方法用於判定給定url是否代表了給定的例項instance public boolean isInstanceURL(String url, InstanceInfo instance) { //解析url地址得到主機名,比如“http://eureka7002.com:7002/eureka/”的主機名為eureka7002.com String hostName = hostFromUrl(url); //拿到例項instance的主機名,當開啟preferIpAddress時,例項的主機名為ip地址。 String myInfoComparator = instance.getHostName(); //預設情況下,該if條件為false,即通過第二句程式碼,從instance的資訊中獲取主機名。 if (clientConfig.getTransportConfig().applicationsResolverUseIp()) { myInfoComparator = instance.getIPAddr(); } //如果url的主機名不為空,且等於instance的主機名,則返回true。 return hostName != null && hostName.equals(myInfoComparator); }
關閉preferIpAddress
關閉preferIpAddress後,例項的主機名就可以通過eureka.instance.hostname屬性進行設定,如下圖: