内容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集群,以前是不存在此问题的(以下图所示)。
在网上查阅了一些资料,有些网友是因为一些基础的配置有误致使该问题出现,好比:Eureka实例的spring.application.name配置的不一致、serviceUrl的配置中使用了localhost等等。最后提到了preferIpAddress的设置,笔者正是配置了这个属性以后才出现本篇文章要解释的这个问题!
上一节也提到,默认状况下,Eureka Client使用主机名进行注册,同时,他们之间的调用也是经过主机名实现。将eureka.instance.preferIpAddress=true
后,Eureka Client便经过IP地址进行注册和复制的互相调用。可是,这又跟replicas的unavailable故障有什么关系呢?下面,咱们从源码角度分析下Eureka的replica是如何解析,以及replica的状态是如何断定的。
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
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对象包含了当前实例的注册信息,好比实例id,应用名,IP地址,端口号,主机名等信息。能够发现,当配置eureka.instance.preferIpAddress=true
后,实例的主机名就是该实例的IP地址,使用eureka.instance.hostname
参数修改也是无效的!
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。因而就有了:
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中包含了当前Eureka实例的peer,即主机名为eureka7002.com和eureka7003.com的Eureka实例。
从registry容器中拿到名为“application-eurekaserver”的微服务app,app中包含了全部可用的Eureka实例。经过调试能够看到这三个实例分id分别为:springcloud-eurekaserver-700一、springcloud-eurekaserver-700二、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()方法如何断定给定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后,实例的主机名就能够经过eureka.instance.hostname属性进行设置,以下图: