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

前言

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

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

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

背景

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

链接拒绝.jpg

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

链接重试.jpg

看到这里,代表咱们的服务在尝试两次重连后和注册中心交互正常了。负载均衡

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

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

问题思考梳理.png

注册中心集群负载测试

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

eureka.client.serviceUrl.defaultZone=http://peer1:8080/eureka/,http://peer2:8080/eureka/,http://peer3:8080/eureka/
复制代码

咱们能够写了一个Demo进行测试:oop

注册中心集群负载测试

一、本地经过修改EurekaServer服务的端口号来模拟注册中心集群部署,分别以87618762两个端口进行启动 二、启动客户端SeviceA,配置注册中心地址为:http://localhost:8761/eureka,http://localhost:8762/eureka

EurekaClient端配置.png

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

8761在前.png

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

四、更改ServiceA中注册中心的配置:http://localhost:8762/eureka,http://localhost:8761/eureka 五、从新启动SeviceA而后查看端口,以下图所示:

8762在前.png
此时看到请求注册中心是,链接的是 8762这个端口的服务。

注册中心故障转移测试

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

一、以87618762两个端口号启动EurekaServer 二、启动ServiceA,配置注册中心地址为:http://localhost:8761/eureka,http://localhost:8762/eureka 三、启动成功后,关闭8761端口的EurekaServer 四、在EurekaClient发送心跳请求的地方打上断点:AbstractJerseyEurekaHttpClient.sendHeartBeat() 五、查看断点处数据,第一次请求的EurekaServer8761端口的服务,由于该服务已经关闭,因此返回的responsenull

8761故障.png
六、第二次会从新请求 8762端口的服务,返回的 response为状态为 200,故障转移成功,以下图:
8762故障转移.png

思考

经过这两个测试Demo,我觉得EurekaClient每次都会取defaultZone配置的第一个host做为请求EurekaServer的请求的地址,若是该节点故障时,会自动切换配置中的下一个EurekaServer进行从新请求。

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

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

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

翠花,上干货!

客户端请求负载原理

原理图解

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

负载原理.png

这里会以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() 中:

代码实现.png

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

负载均衡代码实现.png

思考

原来代码是经过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) + "%");
        }
    }
}
复制代码

负载测试结果以下:

负载测试结果.png

能够看到第二个机器会有**50%的请求,最后一台机器只有17%**的请求,负载的状况并非很均匀,我认为经过IP负载并非一个好的方案。

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

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

Ribbon轮询算法.png

故障转移原理

原理图解

仍是先上结论,以下图:

故障转移原理.png

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

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

代码实现

直接看底层交互的代码,位置在 com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute() 中:

重试代码.png

咱们来分析下这个代码:

  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端请求时负载均衡的选择以及集群故障时自动重试请求的实现原理。

若是还有不懂的问题,能够添加个人微信或者给我公众号留言,我会单独和你讨论交流。

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

原创干货分享.png