负载测试前需要做哪些准备?电子负载仪的工作原理
865
2022-10-04
一个线上问题的思考:Eureka注册中心集群如何实现客户端请求负载及故障转移?
扫描下方二维码了解详情,试听课程
《互联网 Java 工程师面试突击(第3季)》免费升级,由原来的70讲增至160讲,内容扩充一倍多,升级部分内容请参见文末
前言
先抛一个问题给我聪明的读者,如果你们使用微服务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服务的端口号来模拟注册中心集群部署,分别以8761和8762两个端口进行启动2、启动客户端SeviceA,配置注册中心地址为:http://localhost:8761/eureka,http://localhost:8762/eureka
3、启动SeviceA时在发送注册请求的地方打断点:AbstractJerseyEurekaHttpClient.register(),如下图所示:
这里看到请求注册中心时,连接的是8761这个端口的服务。
4、更改ServiceA中注册中心的配置:http://localhost:8762/eureka,http://localhost:8761/eureka5、重新启动SeviceA然后查看端口,如下图所示:
此时看到请求注册中心,连接的是8762这个端口的服务。
注册中心故障转移测试
以两个端口分别启动EurekaServer服务,再启动一个客户端ServiceA。启动成功后,关闭一个8761端口对应的服务,查看此时客户端是否会自动迁移请求到8762端口对应的服务:
1、以8761和8762两个端口号启动EurekaServer2、启动ServiceA,配置注册中心地址为:http://localhost:8761/eureka,http://localhost:8762/eureka3、启动成功后,关闭8761端口的EurekaServer4、在EurekaClient端发送心跳请求的地方打上断点:AbstractJerseyEurekaHttpClient.sendHeartBeat()5、查看断点处数据,第一次请求的EurekaServer是8761端口的服务,因为该服务已经关闭,所以返回的response是null
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顺序可能不同,在使用过程中,取列表的第一个元素作为server端host,从而达到负载的目的。
思考
原来代码是通过EurekaClient的IP进行负载的,所以刚才通过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
负载测试结果如下:
可以看到第二个机器会有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() 中:
我们来分析下这个代码:
第101行,获取client上次成功server端的host,如果有值则直接使用这个host第105行,getHostCandidates()是获取client端配置的serverList数据,且通过ip进行重排序的列表第114行,candidateHosts.get(endpointIdx++),初始endpointIdx=0,获取列表中第1个元素作为host请求第120行,获取返回的response结果,如果返回的状态码是200,则将此次请求的host设置到全局的delegate变量中第133行,执行到这里说明第120行执行的response返回的状态码不是200,也就是执行失败,将全局变量delegate中的数据清空再次循环第一步,此时endpointIdx=1,获取列表中的第二个元素作为host请求依次执行,第100行的循环条件numberOfRetries=3,最多重试2次就会跳出循环
我们还可以看123和129行,这也正是我们业务抛出来的日志信息,所有的一切都对应上了。
总结
感谢你看到这里,相信你已经清楚了开头提问的问题。
上面已经分析完了Eureka集群下Client端请求时负载均衡的选择以及集群故障时自动重试请求的实现原理。
END
《Java工程师面试突击第三季》加餐部分大纲:(注:1-66讲的大纲请扫描文末二维码,在课程详情页获取)
详细的课程内容和试听,大家可以扫描下方二维码了解:
发表评论
暂时没有评论,来抢沙发吧~