Eureka: unavailable-replicas解决方案及原因分析

背景

最近在搭建eureka注册中心时,踩到了一个坑,大概情况是:三个eureka server节点,配置文件如下(不同的文件换了一下eureka.client.serviceUrl.defaultZone里的端口),但访问时发现都处于unavailable状态。

eureka:
  client:
    register-with-eureka: true
    fetch-registry: false
    serviceUrl:
      defaultZone: http://localhost:6001/eureka/, http://localhost:6002/eureka/

使用的eureka server版本为:

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
	<version>2.2.9.RELEASE</version>
</dependency>

解决方案

这个问题的解决方案不只一种,这里只说一下我的解决方案。如果想尝试其他方案,大家可以看了后面的原因分析后自行修改:

  1. 三个节点的spring.application.name配置为一样的
  2. eureka.instance.hostname和eureka.client.serviceUrl.defaultZone里的主机名必须设置成一样
  3. 关闭eureka.instance.prefer-ip-address

原因分析

这里按照实例启动,接收其他实例注册信息,查询replica状态的流程进行分析,注意这里只讨论和本问题相关的部分

启动

eureka server启动时会调用PeerEurekaNodes的resolvePeerUrls方法,该方法返回一个数组,数组的内容是eureka.client.serviceUrl.defaultZone中除了自身之外的地址。

protected List<String> resolvePeerUrls() {
	InstanceInfo myInfo = this.applicationInfoManager.getInfo();
	String zone = InstanceInfo.getZone(this.clientConfig.getAvailabilityZones(this.clientConfig.getRegion()), myInfo);
   	List<String> replicaUrls = EndpointUtils.getDiscoveryServiceUrls(this.clientConfig, zone, new InstanceInfoBasedUrlRandomizer(myInfo));
    int idx = 0;

    while(idx < replicaUrls.size()) {
        if (this.isThisMyUrl((String)replicaUrls.get(idx))) {
            replicaUrls.remove(idx);
        } else {
           	++idx;
        }
    }

    return replicaUrls;
}

然后eureka调用该类的updatePeerEurekaNodes方法,将数组中String类型的url转为PeerEurekaNode对象,存储在PeerEurekaNodes.peerEurekaNodes中。

注册

eureka将注册信息存储AbstractInstanceRegistry的registry中:

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap();

在收到注册信息时,会调用AbstractInstanceRegistry的register方法,其中我们要关心的部分是:

// public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication)
Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName());
EurekaMonitors.REGISTER.increment(isReplication);
if (gMap == null) {
	ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap();
	gMap = (Map)this.registry.putIfAbsent(registrant.getAppName(), gNewMap);
	if (gMap == null) {
		gMap = gNewMap;
	 }
}

这里registrant就是要注册的实例的信息(InstanceInfo类),appName就是spring.application.name,可见会首先根据appname尝试从registry中获取,如果获取不到,则会新建一个concurrentHashMap放入其中。
中间是一些更新注册信息的代码,因为和本文讨论的无关就省略了,然后就是下面的代码:

((Map)gMap).put(registrant.getId(), lease);

gMap就是上一段代码中获取或是新建的map。getId会优先获取instanceId, lease对象就是对registrant进行了一层包装构成的对象,主要是增加了一些注册时间之类的信息,这里把它看成存储实例信息的对象就可以。

查询

查询时,eureka会调用StatusUtil的getStatusInfo方法。该方法的核心代码如下:

Iterator var6 = this.peerEurekaNodes.getPeerEurekaNodes().iterator();

while(var6.hasNext()) {
    PeerEurekaNode node = (PeerEurekaNode)var6.next();
    if (replicaHostNames.length() > 0) {
        replicaHostNames.append(", ");
    }

    replicaHostNames.append(node.getServiceUrl());
    if (this.isReplicaAvailable(node.getServiceUrl())) {
        upReplicas.append(node.getServiceUrl()).append(',');
        ++upReplicasCount;
    } else {
        downReplicas.append(node.getServiceUrl()).append(',');
    }
}

builder.add("registered-replicas", replicaHostNames.toString());
builder.add("available-replicas", upReplicas.toString());
builder.add("unavailable-replicas", downReplicas.toString());

this.peerEurekaNodes.getPeerEurekaNodes()获取到了启动 时得到的数组,然后对数组中的每一项获取url,然后将url传入isReplicaAvailable方法,如果isReplicaAvailable返回true,则加入upReplicas,否则加入downReplicas。所以我们要让isReplicaAvailable返回true
isReplicaAvailable方法代码如下:

try {
    Application app = this.registry.getApplication(this.myAppName, false);
    if (app == null) {
        return false;
    }

    Iterator var3 = app.getInstances().iterator();

    while(var3.hasNext()) {
        InstanceInfo info = (InstanceInfo)var3.next();
        if (this.peerEurekaNodes.isInstanceURL(url, info)) {
            return true;
        }
    }
} catch (Throwable var5) {
    logger.error("Could not determine if the replica is available ", var5);
}

return false;

首先根据当前节点的appName,从注册 时得到的concurrentHashMap中获取对应的值,这就是为什么spring.application.name需要设置成一样,否则无法获取到值就会直接返回false。
然后遍历同一个appName下的实例,将上一步的url和当前的实例信息传入this.peerEurekaNodes.isInstanceURL方法进行判断,如果返回true时就返回true。
this.peerEurekaNodes.isInstanceURL方法代码如下:

String hostName = hostFromUrl(url);
String myInfoComparator = instance.getHostName();
if (this.clientConfig.getTransportConfig().applicationsResolverUseIp()) {
    myInfoComparator = instance.getIPAddr();
}

return hostName != null && hostName.equals(myInfoComparator);

首先根据传入的url获取hostname,然后根据eureka.instance.prefer-ip-address的配置决定获取主机名还是ip地址,因此要将该配置设置为false,最后,比较从实例信息获取主机名,也就是eureka.instance.hostname和url中的主机名是否相等,所以我们要将这两项配置为一样的
最后,官网这里也说得不甚明了:

In fact, the eureka.instance.hostname is not needed if you are running on a machine that knows its own hostname (by default, it is looked up by using java.net.InetAddress).

这里只说了不需要配置eureka.instance.hostname,但是没有说要和defaultZone里的主机名配置成一样。
起初我就是没有配置eureka.instance.hostname,defaultZone里填了localhost,然而java.net.InetAddress获取出来的是设备名称,导致二者不一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值