背景
最近在搭建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>
解决方案
这个问题的解决方案不只一种,这里只说一下我的解决方案。如果想尝试其他方案,大家可以看了后面的原因分析后自行修改:
- 三个节点的spring.application.name配置为一样的
- eureka.instance.hostname和eureka.client.serviceUrl.defaultZone里的主机名必须设置成一样
- 关闭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获取出来的是设备名称,导致二者不一致。