结合 Ribbon 实现微服务故障自动剔除

Ribbon 是 Netflix 出品的一套负载均衡组件,提供了许多 Rule 规则从负载列表中选取合适的 server 实例。
当实例出现问题时候,需要将这部分异常的服务提供者从负载列表中剔除,从而避免雪崩效应。而 Riibbon 本身具有自动移除问题实例的功能,于是我们可以结合 Ribbon 对现有的负载均衡策略做一些改进,实现自动故障剔除功能。

工作流程


Ribbon 中实现自动移除问题实例的 Rule 是 AvailabilityFilteringRule,它的运行原理如上图所示,大致可以分为以下几步:

  1. 在 LoadbalancerCommand 发起 choose server 请求时候,首先会通过 RoundRobinRule 去服务器列表 serverList 获取 server,此处 RoundRobinRule 是修改过的带权重的加权轮询算法器。
  2. 从 RoundRobinRule 获取一个 server 之后,会 AvailabilityPredicate 这个断言器去判断是否是问题实例。简单来说,就是从 LoadBalancerStats 中获取这个 server 的状态 serverStats,然后在 serverStats 这个状态机中去判断是否应该断路。
  3. 经过 serverStats 的断路判断之后,如果是需要断路的,那么表明是问题实例,如果不需要,则返回给 LoadBalancerCommand 一个正常的 server 实例。
  4. LoadBalancerCommand 在处理完请求后,无论成功与否都对 serverStats 做一次相应的状态记录。

服务状态 ServerStats

结合实际业务需求,在 serverStats 中,目前定义了两类异常,连接异常(主要指 tcp 层面的,包括 sokcetException,socketTimeoutException,ConnectException)以及不可用异常(主要是底层通信框架 rxNetty 抛出的异常,包括 timeoutException 以及 PoolExhaustedException)。

参数定义

考虑到两种异常的发生场景的差异,为这两类异常分别设置了各自的连续失败阈值(connectionFailureThreshold,unavailableThreshold)以及断路超时时间(connectionFailureCircuitTimeout,unavailableCircuitTimeout)。
其中不可用异常参数(unavailableThreshold,unavailableCircuitTimeout)会暴露给使用者,使用 SpringBoot 的项目可以直接读取 YAM 文件或者托管到第三方配置中心。

而其中连接失败异常相关的参数,考虑到较为底层,使用者不大关心,是默认设置好了,只通过读取环境变量的方式来允许改动。

断路器工作原理

从上面定义的参数就指定,目前断路器统计失败是靠连续失败次数去判断断路逻辑的。之后可以根据不同场景做不同的适配。
目前断路器的工作算法大致如下

  1. 计算累计连接失败计数 successiveConnectionFailureCount 是否超过 链接失败阈值 connectionFailureThreshold。如果 successiveConnectionFailureCount < connectionFailureThreshold,即尚未超过限额,则熔断时间为 0 ;反之,如果超过限额,则进行步骤 2 的计算
  2. 计算失败基数,最大不得超过 16。diff = (failureCount - threshold) > 16 ? 16 : (failureCount - threshold)
  3. 根据超时因子 timeoutFactor(目前默认写死为 10)计算超时时间: blackOutSeconds = (1 << diff) * timeoutFactor;
  4. 超时时间不得超过最大超时时间 connectionFailureCircuitTimeout 上线
  5. 计算完连接失败异常相应的超时时间 connectionFailureblackOutPeriod 之后,用不可用异常对应的参数去重复一下 1-4 步骤算出不可用异常对应的超时时间 unavailableblackOutPeriod。两者取最大值作为 blackOutPeriod。
  6. 将最后一次失败时间 lastConnectionFailedTimestamp 加上 blackOutPeriod 超时间隔,作为当前断路器刚关闭的时间。
  7. 每一次请求来,只要比较当前时间与步骤 6 算出的时间即可,如果当前时间较大,说明过了断路器刚关闭的时间,可以正常提供服务,否则表明服务是断路状态。

当有链接失败情况出现断路逻辑时,将会最多:1<<16 * 10 = 655360 s (如果超过自定义超时时间阈值,则最大为自定义超时时间),最少 1<<0*10 = 10 s 的请求熔断时间,再此期间内,此 Server 将会被忽略。
熔断的超时时间在没有超过自定义超时阈值的情况下,会随着该 server 的失败次数呈指数级动态增加。如果当自定义阈值很大,而一个服务实例连续失败很多次,那他基本就没什么希望被调用到了。。

框架中算法实现如下:

    private long getCircuitBreakerBlackoutPeriod(AtomicInteger atomicInteger, Integer threshold, Integer circuitTimeout) {
        final int failureCount = atomicInteger.get();
        if (failureCount < threshold) {
            return 0;
        }
        final int diff = (failureCount - threshold) > 16 ? 16 : (failureCount - threshold);
        int blackOutSeconds = (1 << diff) * timeoutFactor;
        if (blackOutSeconds > circuitTimeout) {
            blackOutSeconds = circuitTimeout;
        }
        return blackOutSeconds * 1000L;
    }

记录服务状态

当 loadBalancerCommand 对服务请求做出处理之后,根据返回的状态对 serverStats 做出记录。

  1. 每当服务正常返回时候,会清空两种异常的熔断统计。
  2. 每当服务异常,但是异常不是定义的两种异常中的任何一种,也会清空两种异常的熔断统计。
  3. 每当服务异常,并且异常是两种异常中的一种,则会增加相应异常的熔断统计,并刷新最后失败时间 lastConnectionFailedTimestamp 这一参数。

服务路由规则

AvailabilityFilteringRule 通过一定的规则选择合适的 server 实例。
首先它用 roundRobinRule 加权轮询算法选取一个 sevrer 实例。
接着对选出的 server 实例应用上述的断路算法判断是否应该断路,如果是断路状态,那么将会重新通过 roundRobinRule 去选择 server 实例,这里最多重试 10 次。
如果经过了 10 次,还没有选出合适的实例,意味着也许所有实例都被熔断了。那么死活还是得挑一个的,这里会通过父类 Rule 去选择实例,而父类 rule 的断言器定义的是 always true。

public Server choose(Object key) {
        int count = 0;
        Server server = roundRobinRule.choose(key);
        while (count++ <= 10) {
            if (predicate.apply(new PredicateKey(server))) {
                return server;
            }
            server = roundRobinRule.choose(key);
        }
        return super.choose(key);
    }
留下你的脚步
推荐阅读