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

前言

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

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

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

背景

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

连接拒绝.jpg

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

连接重试.jpg

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

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

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

问题思考梳理.png

注册中心集群负载测试

线上注册中心是由三台机器组成的集群,都是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

EurekaClient端配置.png

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

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

4、更改ServiceA中注册中心的配置:http://localhost:8762/eureka,http://localhost:8761/eureka
5、重新启动SeviceA然后查看端口,如下图所示:
8762在前.png
此时看到请求注册中心是,连接的是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
8761故障.png
6、第二次会重新请求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端请求时负载均衡的选择以及集群故障时自动重试请求的实现原理。

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

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Eureka 是 Netflix 开源的一个基于 RESTful 的服务注册与发现组件,用于服务的注册与发现。Eureka 分为 Eureka Server 和 Eureka Client 两部分。Eureka Server 作为服务注册中心,负责服务的注册与发现;Eureka Client 则作为服务提供者和服务消费者的客户端,通过向 Eureka Server 注册或发现服务。 Eureka Client 启动后会向 Eureka Server 发送心跳包,以保持与 Eureka Server 的连接,同时将自己注册到 Eureka Server 上。Eureka Server 则会维护一个服务注册表,记录所有已注册的服务列表。当服务提供者启动时,它会向 Eureka Server 发送一个注册请求,告诉 Eureka Server 它的服务实例已经启动。当服务消费者需要调用某个服务时,它会向 Eureka Server 发送一个发现请求Eureka Server 则会返回该服务的所有实例列表,消费者可以根据负载均衡策略选择其中的一个实例进行调用。 在 Eureka Client 向 Eureka Server 发送心跳包时,Eureka Server 会将该服务实例的状态设置为 UP。如果 Eureka Server 在一定时间内没有收到该服务实例的心跳包,就会将该服务实例的状态设置为 DOWN,并从服务注册表中删除该实例。当服务提供者关闭时,它会向 Eureka Server 发送一个取消注册请求,告诉 Eureka Server 它的服务实例已经关闭,Eureka Server 则会将该服务实例从服务注册表中删除。 总之,Eureka 注册中心客户端请求原理是基于 RESTful 风格的。Eureka Client 通过向 Eureka Server 发送注册和发现请求实现服务的注册与发现。Eureka Server 则维护服务注册表,记录所有已注册的服务列表,并根据心跳包更新服务实例的状态。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值