SpringCloud從入門到進階(三)——源碼探究Eureka集羣之replicas的unavailable故障

內容html

  本節從源碼的角度探討了Eureka控制檯中爲什麼replicas(副本)顯示unavailable(不可用)的緣由。在源碼層級解讀了Eureka Server的replicas是如何解析,以及replica的狀態是如何斷定。node

版本spring

  IDE:IDEA 2017.2.2 x64服務器

  JDK:1.8.0_171app

  manve:3.3.3dom

  SpringBoot:1.5.9.RELEASE分佈式

​  SpringCloud:Dalston.SR1微服務

適合人羣post

  Java開發人員​fetch

用詞釋義

  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集羣,以前是不存在此問題的(以下圖所示)。

1540261620655

  在網上查閱了一些資料,有些網友是因爲一些基礎的配置有誤致使該問題出現,好比: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
    #將ip信息註冊到eureka server。
   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參數修改也是無效的!

1541766672368

replicaUrls

  replicaUrls是當前實例在eureka.client.serviceUrl參數配置的全部Eureka peer的url地址。由配置文件可知,當前實例中配置的兩個Eureka實例的url地址是http://eureka7002.com:7002/eurekahttp://eureka7003.com:7003/eureka,調試結果與配置一致。

1541767604408

本段小結

  由peer1的配置文件可知,eureka.client.serviceUrl參數爲http://eureka7002.com:7002/eurekahttp://eureka7003.com:7003/eureka。這就是當前Eureka實例的兩個peer。因爲開啓preferIpAddress,所以當前Eureka實例的主機名爲ip地址,ip與eureka7002.com和eureka7003.com在字面量上都不相等,由源碼可知,當前Eureka實例會認爲eureka7002.com和eureka7003.com這兩個實例是它的replica。因而就有了:

1541772557956

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實例。

1541773014390

名爲「APPLICATION-EUREKASERVER」的服務-app

  從registry容器中拿到名爲「application-eurekaserver」的微服務app,app中包含了全部可用的Eureka實例。經過調試能夠看到這三個實例分id分別爲:springcloud-eurekaserver-700一、springcloud-eurekaserver-700二、springcloud-eurekaserver-7003。這正是咱們啓動的三個Eureka實例,調試結果與事實一致。

1541776348273

結論

  因爲開啓了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的狀態顯示正常。詳細過程請看下文:SpringCloud從入門到進階(四)——生產環境下Eureka的徹底分佈式部署

補充

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屬性進行設置,以下圖:

1541778296548

相關文章
相關標籤/搜索