写在前面
该文是 Spring Cloud Eureka 服务治理完整过程搭建 的知识点拓展。
前文中我们搭建了一个可用的注册中心,可是,由于某些不可抗因素,导致该注册中心挂掉了怎么办呢?哪怕该注册中心没挂,怎么保证获取到的服务实例都是可用的?你很难不去假设它会挂掉。
解决第一个问题;默认情况下,每个 Eureka 服务器也是一个 Eureka 客户端,需要至少一个 服务URL 来定位一个对等点。当然,不提供它,服务也会运行并正常工作,但是日志中会充满不能向对等方注册的噪音。
所以,我们需要基于当前的注册中心,构建它的对等点。实际上,注册中心的高可用就是将自己作为服务向其它服务注册中心注册自己,这样便形成了互相注册的服务注册中心,各服务注册中心之间实现了服务清单的互相同步。
举个例子:存在注册中心 A 以及 它的对等点 B ,我们的注册服务C仅仅填写了注册中心 A 的地址作为其服务URL,由于注册中心间服务清单的互相同步,我们的消费服务 D(仅仅填写了注册中心 B 的地址作为其服务URL)能够正常调用 C。这样,注册中心 A 以及 B 便构成了一个双节点的服务注册中心集群,无论其中哪一个挂掉,仍然能够正常提供服务。
下面给出例子的代码实现:
-
注册中心 A 、B:基于前文创建的 eureka-server ,重新创建两个子模块 eureka-server-node1 和 eureka-server-node2 ,并且需要删掉 application.yml 文件中的
register-with-eureka
和fetch-registry
配置,这也是我们为什么没有在日志中收到不能向对等方注册的噪音 的原因。注册中心 A :
server: port: 1112 eureka: instance: hostname: localhost client: service-url: defaultZone: http://localhost:1113/eureka/ server: my-url: http://localhost:1112/ spring: application: name: eureka-server-node1
注册中心 B:
server: port: 1113 eureka: instance: hostname: localhost client: service-url: defaultZone: http://localhost:1112/eureka/ server: my-url: http://localhost:1113/ spring: application: name: eureka-server-node2
注意的是,我们并没有像网上那样修改 hostname 为不一样的值(我想这是有意义的),因为如果那样做的话,还需要修改 hosts 文件,我个人觉得太麻烦,所以就遇到了无法创建分片的问题。但我在日志上发现了一行警告:
The replica size seems to be empty. Check the route 53 DNS Registry
定位到记录该日志的类
PeerEurekaNodes
,发现是在更新对等节点的时候出现这个问题:protected void updatePeerEurekaNodes(List<String> newPeerUrls) { if (newPeerUrls.isEmpty()) { logger.warn("The replica size seems to be empty. Check the route 53 DNS Registry"); return; } // ... 省略 }
可以发现
newPeerUrls
为空了,所以我们需要查看该方法的调用者,由于我们是在启动时,遇到这个问题,所以定位调用者在当前类的start
方法,在该方法中通过调用了resolvePeerUrls
来取得newPeerUrls
的值:/** * Resolve peer URLs. * * @return peer URLs with node's own URL filtered out */ protected List<String> resolvePeerUrls() { InstanceInfo myInfo = applicationInfoManager.getInfo(); String zone = InstanceInfo.getZone(clientConfig.getAvailabilityZones(clientConfig.getRegion()), myInfo); List<String> replicaUrls = EndpointUtils .getDiscoveryServiceUrls(clientConfig, zone, new EndpointUtils.InstanceInfoBasedUrlRandomizer(myInfo)); int idx = 0; while (idx < replicaUrls.size()) { if (isThisMyUrl(replicaUrls.get(idx))) { replicaUrls.remove(idx); } else { idx++; } } return replicaUrls; }
断点调试发现,最后在 while 循环中移除了本来存在的分片url,查看 isThisMyUrl 方法:
/** * Checks if the given service url contains the current host which is trying * to replicate. Only after the EIP binding is done the host has a chance to * identify itself in the list of replica nodes and needs to take itself out * of replication traffic. * * @param url the service url of the replica node that the check is made. * @return true, if the url represents the current node which is trying to * replicate, false otherwise. */ public boolean isThisMyUrl(String url) { final String myUrlConfigured = serverConfig.getMyUrl(); if (myUrlConfigured != null) { return myUrlConfigured.equals(url); } return isInstanceURL(url, applicationInfoManager.getInfo()); } /** * Checks if the given service url matches the supplied instance * * @param url the service url of the replica node that the check is made. * @param instance the instance to check the service url against * @return true, if the url represents the supplied instance, false otherwise. */ public boolean isInstanceURL(String url, InstanceInfo instance) { String hostName = hostFromUrl(url); String myInfoComparator = instance.getHostName(); if (clientConfig.getTransportConfig().applicationsResolverUseIp()) { myInfoComparator = instance.getIPAddr(); } return hostName != null && hostName.equals(myInfoComparator); }
不难发现,默认情况下,在我们并没有配置 myurl 的情况下,是不允许使用相同的主机名作为对等点的。所以问题解决了,我们只需要配置上 myurl 即可。
由于注册中心启动的先后顺序,会导致先启动的节点报错,不用担心,当另一节点启动好以后,就不会报错了。
-
修改前文创建的 eureka-service-consumer 配置文件中的 defaultZone 值,将其作为我们的消费服务 D :
eureka.client.defaultZone=http://localhost:1112/eureka/
-
修改前文创建的 eureka-service-provider 配置文件中的 defaultZone 值,将其作为我们的注册服务 C :
eureka.client.defaultZone=http://localhost:1113/eureka/
注意,在同一主机上启用上述服务,端口应不一致的。
接下来,访问注册服务的请求接口:http://localhost:8080/provider/hello ,正确打印出值;访问消费服务的请求接口:http://localhost:8081/consumer/ribbon-consumer ,正确打印出值(刚开始访问时,提示无法找到我所调用的服务,但刷新几次就好了)。
那么,第二个问题怎么保证获取到的服务都是可用的呢?
首先,通过结合客户端和服务端的心跳机制检测注册到的服务是否可用,不可用的会进行驱逐,其次,服务端具有自我保护机制,该机制是指 Eureka Server 在运行期间,会统计心跳失败的比例,如果在 15分钟之内低于85%,Eureka Server 会将当前的实例注册信息保护起来,让这些实例不会过期。可如果这样的话,客户端就很容易拿到实际已经不存在的服务实例,会出现调用失败的情况,所以客户端需要有容错机制,比如可以使用请求重试,断路器等机制。
本地调试很容易触发注册中心的保护机制,可以通过
eureka.server.enable-self-preservation = false
参数来关闭保护机制。