结合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的项目可以直接读取yml文件或者托管到第三方配置中心。

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

断路器工作原理

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

  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);
    }

标题:结合Ribbon实现微服务故障自动剔除
作者:fredalxin
地址:https://fredal.xin/loadbalancer-with-ribbon