zoukankan      html  css  js  c++  java
  • 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集群,之前是不存在此问题的(如下图所示)。

    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-7001、springcloud-eurekaserver-7002、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

  • 相关阅读:
    【08】Python数据分析基础
    C#类和类成员初始化顺序
    C# 在子窗体调用父窗体的值(转)
    求实对称阵的 特征值 和 特征向量(转)
    Dev express 笔记
    Arcgis桌面开发,Python引用GDAL库的方法
    操作系统学习笔记--第三章--进程
    操作系统学习笔记--第一章--计算机系统概述
    GIS专业分析方法(待更新)
    C#中不用安装Oracle客户端连接Oracle数据库(转)
  • 原文地址:https://www.cnblogs.com/lonelyJay/p/9940199.html
Copyright © 2011-2022 走看看