前言

先抛一个问题给我聪明的读者,如果你们使用微服务SpringCloud-Netflix进行业务开发,那么线上注册中心肯定也是用了集群部署,问题来了:

你了解Eureka注册中心集群如何实现客户端请求负载及故障转移吗?

可以先思考一分钟,我希望你能够带着问题来阅读此篇文章,也希望你看完文章后会有所收获!

背景

前段时间线上Sentry平台报警,多个业务服务在和注册中心交互时,例如续约注册表增量拉取等都报了Request execution failed with message : Connection refused 的警告:

紧接着又看到 Request execution succeeded on retry #2 的日志。

看到这里,表明我们的服务在尝试两次重连后和注册中心交互正常了。

一切都显得那么有惊无险,这里报Connection refused 是注册中心网络抖动导致的,接着触发了我们服务的重连,重连成功后一切又恢复正常。

这次的报警虽然没有对我们线上业务造成影响,并且也在第一时间恢复了正常,但作为一个爱思考的小火鸡,我很好奇这背后的一系列逻辑:Eureka注册中心集群如何实现客户端请求负载及故障转移?

注册中心集群负载测试

线上注册中心是由三台机器组成的集群,都是4c8g的配置,业务端配置注册中心地址如下(这里的peer来代替具体的ip地址):

eureka.client.serviceUrl.defaultZone=http://peer1:8080/eureka/,http://peer2:8080/eureka/,http://peer3:8080/eureka/

我们可以写了一个Demo进行测试:

注册中心集群负载测试

1、本地通过修改EurekaServer服务的端口号来模拟注册中心集群部署,分别以87618762两个端口进行启动

2、启动客户端SeviceA,配置注册中心地址为:http://localhost:8761/eureka,http://localhost:8762/eureka

3、启动SeviceA时在发送注册请求的地方打断点:AbstractJerseyEurekaHttpClient.register(),如下图所示:

这里看到请求注册中心时,连接的是8761这个端口的服务。

4、更改ServiceA中注册中心的配置:http://localhost:8762/eureka,http://localhost:8761/eureka

5、重新启动SeviceA然后查看端口,如下图所示:



此时看到请求注册中心是,连接的是8762这个端口的服务。

注册中心故障转移测试

以两个端口分别启动EurekaServer服务,再启动一个客户端ServiceA。启动成功后,关闭一个8761端口对应的服务,查看此时客户端是否会自动迁移请求到8762端口对应的服务:

1、以87618762两个端口号启动EurekaServer

2、启动ServiceA,配置注册中心地址为:http://localhost:8761/eureka,http://localhost:8762/eureka

3、启动成功后,关闭8761端口的EurekaServer

4、在EurekaClient发送心跳请求的地方打上断点:AbstractJerseyEurekaHttpClient.sendHeartBeat()

5、查看断点处数据,第一次请求的EurekaServer8761端口的服务,因为该服务已经关闭,所以返回的responsenull



6、第二次会重新请求8762端口的服务,返回的response为状态为200,故障转移成功,如下图:

思考

通过这两个测试Demo,我以为EurekaClient每次都会取defaultZone配置的第一个host作为请求EurekaServer的请求的地址,如果该节点故障时,会自动切换配置中的下一个EurekaServer进行重新请求。

那么疑问来了,EurekaClient每次请求真的是以配置的defaultZone配置的第一个服务节点作为请求的吗?这似乎也太弱了!!?

EurekaServer集群不就成了伪集群!!?除了客户端配置的第一个节点,其它注册中心的节点都只能作为备份和故障转移来使用!!?

真相是这样吗?NO!我们眼见也不一定为实,源码面前毫无秘密!

翠花,上干货!

客户端请求负载原理

原理图解

还是先上结论,负载原理如图所示:

这里会以EurekaClient端的IP作为随机的种子,然后随机打乱serverList,例如我们在商品服务(192.168.10.56)中配置的注册中心集群地址为:peer1,peer2,peer3,打乱后的地址可能变成peer3,peer2,peer1

用户服务(192.168.22.31)中配置的注册中心集群地址为:peer1,peer2,peer3,打乱后的地址可能变成peer2,peer1,peer3

EurekaClient每次请求serverList中的第一个服务,从而达到负载的目的。

代码实现

我们直接看最底层负载代码的实现,具体代码在

com.netflix.discovery.shared.resolver.ResolverUtils.randomize() 中:

这里面random 是通过我们EurekaClient端的ipv4做为随机的种子,生成一个重新排序的serverList,也就是对应代码中的randomList,所以每个EurekaClient获取到的serverList顺序可能不同,在使用过程中,取列表的第一个元素作为serverhost,从而达到负载的目的。

思考

原来代码是通过EurekaClientIP进行负载的,所以刚才通过DEMO程序结果就能解释的通了,因为我们做实验都是用的同一个IP,所以每次都是会访问同一个Server节点。

既然说到了负载,这里肯定会有另一个疑问:

通过IP进行的负载均衡,每次请求都会均匀分散到每一个Server节点吗?

比如第一次访问Peer1,第二次访问Peer2,第三次访问Peer3,第四次继续访问Peer1等,循环往复......

我们可以继续做个试验,假如我们有10000个EurekaClient节点,3个EurekaServer节点。

Client节点的IP区间为:192.168.0.0 ~ 192.168.255.255,这里面共覆盖6w多个ip段,测试代码如下:

/**
* 模拟注册中心集群负载,验证负载散列算法
*
* @author 一枝花算不算浪漫
* @date 2020/6/21 23:36
*/
public class EurekaClusterLoadBalanceTest { public static void main(String[] args) {
testEurekaClusterBalance();
} /**
* 模拟ip段测试注册中心负载集群
*/
private static void testEurekaClusterBalance() {
int ipLoopSize = 65000;
String ipFormat = "192.168.%s.%s";
TreeMap<String, Integer> ipMap = Maps.newTreeMap();
int netIndex = 0;
int lastIndex = 0;
for (int i = 0; i < ipLoopSize; i++) {
if (lastIndex == 256) {
netIndex += 1;
lastIndex = 0;
} String ip = String.format(ipFormat, netIndex, lastIndex);
randomize(ip, ipMap);
System.out.println("IP: " + ip);
lastIndex += 1;
} printIpResult(ipMap, ipLoopSize);
} /**
* 模拟指定ip地址获取对应注册中心负载
*/
private static void randomize(String eurekaClientIp, TreeMap<String, Integer> ipMap) {
List<String> eurekaServerUrlList = Lists.newArrayList();
eurekaServerUrlList.add("http://peer1:8080/eureka/");
eurekaServerUrlList.add("http://peer2:8080/eureka/");
eurekaServerUrlList.add("http://peer3:8080/eureka/"); List<String> randomList = new ArrayList<>(eurekaServerUrlList);
Random random = new Random(eurekaClientIp.hashCode());
int last = randomList.size() - 1;
for (int i = 0; i < last; i++) {
int pos = random.nextInt(randomList.size() - i);
if (pos != i) {
Collections.swap(randomList, i, pos);
}
} for (String eurekaHost : randomList) {
int ipCount = ipMap.get(eurekaHost) == null ? 0 : ipMap.get(eurekaHost);
ipMap.put(eurekaHost, ipCount + 1);
break;
}
} private static void printIpResult(TreeMap<String, Integer> ipMap, int totalCount) {
for (Map.Entry<String, Integer> entry : ipMap.entrySet()) {
Integer count = entry.getValue();
BigDecimal rate = new BigDecimal(count).divide(new BigDecimal(totalCount), 2, BigDecimal.ROUND_HALF_UP);
System.out.println(entry.getKey() + ":" + count + ":" + rate.multiply(new BigDecimal(100)).setScale(0, BigDecimal.ROUND_HALF_UP) + "%");
}
}
}

负载测试结果如下:

可以看到第二个机器会有50%的请求,最后一台机器只有17%的请求,负载的情况并不是很均匀,我认为通过IP负载并不是一个好的方案。

还记得我们之前讲过Ribbon默认的轮询算法RoundRobinRule【一起学源码-微服务】Ribbon 源码四:进一步探究Ribbon的IRule和IPing

这种算法就是一个很好的散列算法,可以保证每次请求都很均匀,原理如下图:

故障转移原理

原理图解

还是先上结论,如下图:

我们的serverList按照client端的ip进行重排序后,每次都会请求第一个元素作为和Server端交互的host,如果请求失败,会尝试请求serverList列表中的第二个元素继续请求,这次请求成功后,会将此次请求的host放到全局的一个变量中保存起来,下次client端再次请求 就会直接使用这个host

这里最多会重试请求两次。

代码实现

直接看底层交互的代码,位置在

com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute() 中:

我们来分析下这个代码:

  1. 第101行,获取client上次成功server端的host,如果有值则直接使用这个host
  2. 第105行,getHostCandidates()是获取client端配置的serverList数据,且通过ip进行重排序的列表
  3. 第114行,candidateHosts.get(endpointIdx++),初始endpointIdx=0,获取列表中第1个元素作为host请求
  4. 第120行,获取返回的response结果,如果返回的状态码是200,则将此次请求的host设置到全局的delegate变量中
  5. 第133行,执行到这里说明第120行执行的response返回的状态码不是200,也就是执行失败,将全局变量delegate中的数据清空
  6. 再次循环第一步,此时endpointIdx=1,获取列表中的第二个元素作为host请求
  7. 依次执行,第100行的循环条件numberOfRetries=3,最多重试2次就会跳出循环

我们还可以第123和129行,这也正是我们业务抛出来的日志信息,所有的一切都对应上了。

总结

感谢你看到这里,相信你已经清楚了开头提问的问题。

上面已经分析完了Eureka集群下Client端请求时负载均衡的选择以及集群故障时自动重试请求的实现原理。

如果还有不懂的问题,可以添加我的微信或者给我公众号留言,我会单独和你讨论交流。

本文首发自:一枝花算不算浪漫 公众号,如若转载请在文章开头标明出处,如需开白可直接公众号回复即可。



一个线上问题的思考:Eureka注册中心集群如何实现客户端请求负载及故障转移?的更多相关文章

  1. springCloud系列教程01:Eureka 注册中心集群搭建

    springCloud系列教程包含如下内容: springCloud系列教程01:Eureka 注册中心集群搭建 springCloud系列教程02:ConfigServer 配置中心server搭建 ...

  2. SpringCloud Eureka(注册中心集群)

    多个注册中心,其实用不同的配置对应 不同的端口号注册就行了. 注册中心自己也是个服务,看看之前的单个注册中心是怎么样的呢? server: port: 8888 # 服务端口eureka: insta ...

  3. eureka注册中心集群

    注册中心集群: 思想:让一个eureka注册中心 注册到 另一个eureka注册中心上去即A注册到B,B注册到A 注册中心配置: 1:端口号不同2:应用名称相同3:相互注册

  4. Spring Cloud Eureka 注册中心集群搭建,Greenwich 最新版!

    Spring Cloud 的注册中心可以由 Eureka.Consul.Zookeeper.ETCD 等来实现,这里推荐使用 Spring Cloud Eureka 来实现注册中心,它基于 Netfl ...

  5. spring cloud Eureka注册中心集群搭建

    1.创建springcloud-eureka maven项目 pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0&quo ...

  6. 2、eureka注册中心集群

    1. Eureka作为spring cloud的服务发现与注册中心,在整个的微服务体系中,处于核心位置.单机模式下的eureka服务,显然不能满足高可用的实际生产环境,这就要求配置一个能够应对各种突发 ...

  7. centos7环境搭建Eureka-Server注册中心集群

    目的:测试和线上使用这套独立的Eureka-Server注册中心集群,目前3台虚拟机集群,后续可直接修改配置文件进行新增或减少集群机器. 系统环境: Centos7x64 java8+(JDK1.8+ ...

  8. dubbo-admin与多注册中心(注册中心集群)

    在使用dubbo时,注册中心是一个必要的架构组成成员.当我们的注册中心没有采取集群时,如何在dubbo-admin中配置,我们可以根据dubbo官方文档,很快找到我们的答案. 但是当注册中心集群之后怎 ...

  9. 【微服务架构】SpringCloud之Eureka(注册中心集群篇)(三)

    上一篇讲解了spring注册中心(eureka),但是存在一个单点故障的问题,一个注册中心远远无法满足实际的生产环境,那么我们需要多个注册中心进行集群,达到真正的高可用.今天我们实战来搭建一个Eure ...

随机推荐

  1. JVM调优总结(三)-垃圾回收面临的问题

    如何区分垃圾 上面说到的“引用计数”法,通过统计控制生成对象和删除对象时的引用数来判断.垃圾回收程序收集计数为0的对象即可.但是这种方法无法解决循环引用.所以,后来实现的垃圾判断算法中,都是从程序运行 ...

  2. 【Ubuntu】Ubuntu中下载特定版本内核和设置某版本内核为默认启动内核

    0. 基本命令 uname -a # 查看当前所使用内核 dpkg -l | grep linux # dpkg后是lmn的l.查看当前操作系统的内核 dekg -l | grep linux-ima ...

  3. Servlet配置及生命周期

    1.设置Ecilipse快捷  file new 2.创建Servlet程序 1). 创建一个 Servlet 接口的实现类.              public class HelloServl ...

  4. AD17无法复制原理图到Word的解决方法

    标题: 解决AD17无法复制原理图到WORD 作者: 梦幻之心星 347369787@QQ.com 标签: [AD, Word, 原理图] 目录: 软件 日期: 2019-3-17 目录 前提说明: ...

  5. Azure AD(三)知识补充-Azure资源的托管标识

    一,引言 来个惯例,吹水! 前一周因为考试,还有个人的私事,一下子差点颓废了.想了想,写博客这种的东西还是得坚持,再忙,也要检查.要养成一种习惯,同时这也是自我约束的一种形式.虽然说不能浪费大量时间在 ...

  6. jQuery-操作元素的内容,属性,样式

    1.操作内容 获取: 双标签:html() input:val() 设置: 双标签:html('新内容') input:val('新内容') 2.操作属性 * 获取:attr('属性名') * 设置: ...

  7. Rocket - tilelink - fastProperty

    https://mp.weixin.qq.com/s/9nikweQUGG5FO3Z8t6feaw 介绍Parameters中定义的fastProperty的实现.(使用最近的新版本,差别不大)   ...

  8. Java实现LeetCode_0028_ImplementStrStr

    package javaLeetCode.primary; import java.util.Scanner; public class ImplementStrStr_28 { public sta ...

  9. 哦,Vendor

    vendor踩坑笔记: 接过公司里面X平台golang部分的后端后,需要新添加一个业务功能,美滋滋~ 拿过项目一顿写之后后遇到事了. 报错的描述如下: 报错的大意说:方法的入参类型不匹配,我们不能把 ...

  10. 「持续集成实践系列 」Jenkins 2.x 构建CI自动化流水线常见技巧

    在上一篇文章中,我们介绍了Jenkins 2.x实现流水线的两种语法,以及在实际工作中该如何选择脚本式语法或声明式语法.原文可查阅:「持续集成实践系列」Jenkins 2.x 搭建CI需要掌握的硬核要 ...