Skip to content

服务容错

在分布式系统运行过程中,远程调用发生失败的现象不可避免。为了应对服务访问失败,集群容错是一种简单高效的技术组件。

在分布式系统中,对于服务提供者和消费者而言实际上存在一种依赖关系。
一方面,每个服务自身可能会发生异常情况,更为重要的,这种依赖关系会导致系统中的其他服务也发生调用失败,如下图所示:

那么问题就来了,一旦出现上图中的失败场景,我们有什么应对策略呢?
这是开发任何一个分布式系统所必须要考虑的问题。

就技术体系而言,我们可以把应对远程调用失败场景的各种手段和方法统称为服务容错(Fault Tolerance),而集群容错是服务容错的其中一种实现方式。
我们知道,所谓集群,就是同时存在一个服务的多个实例。一旦我们访问其中一个实例出现问题,原则上可以访问其他实例来获取结果。

集群容错

熔断器

服务熔断的具体表现形式是熔断器
熔断(Circuit Breaker)也是很实用的一种容错机制。
在日常开发过程中,如果你使用的是像 Spring Cloud 这样的主流分布式服务框架,那么系统所产生的每一次远程调用的背后都内置了熔断机制

熔断这个概念实际上并不难理解,类似于日常生活中经常会碰到的电路自动切断机制。
我们知道在电路系统中,当电流过大时保险丝就会被融化从而隔断了整个电路回路。
类比分布式服务,当系统产生大量异常时,我们也应该隔断整个服务调用链路,避免服务的不断重试触发雪崩效应。

那么服务熔断是如何做到的呢?我们需要明确,对于服务消费者发起的每一次远程调用请求,熔断器都需要进行监控和记录。
如果调用的响应时间过长,服务熔断器就应该中断本次调用并直接返回。
请注意服务熔断器判断本次调用是否应该快速失败是有状态的,也就是说它会对一段时间的调用结果进行统计,
如果统计的结果触发了事先设置好的阈值(类似电路系统中保险丝的过载等级),那么服务熔断机制就会被触发,反之将继续执行后续的远程调用。

通过上述分析,我们可以梳理出如下技术上的要点。

  • 状态性:通过合理的状态切换来控制是否对请求进行熔断。
  • 运行时数据:收集服务运行时数据并进行统计分析,为阈值判断提供依据。
  • 阈值控制:各个状态切换的控制开关。

基于熔断器的设计理念,我们对熔断过程进行进行抽象和提炼,可以得到如下图所示的熔断器基本结构。

可以看到,上图所展示的结构简明扼要地给出了熔断器内部所具备的状态机,该状态机包含三个状态,即 Closed(关闭)、Open(打开)和 Half-Open(半开)。

  • Closed。这是熔断器的默认状态。
    在该状态下,相当于熔断器没有发挥任何效果,所有的请求可以得到正常的响应。
    但是,在熔断器内部还是会对所有的调用过程进行监控,如果有异常发生则会进行不断累加。
  • Open。这是熔断器的打开状态。
    在该状态下,相当于熔断器对所有的请求进行了隔断,来自服务消费者的请求不会触达到服务的提供者。
    同时,在熔断器内部也会启动一个计时器,当处于该状态达到一定时间时,熔断器会进入到半开状态。
  • Half-Open。这是熔断器的半开状态。
    所谓半开,指的是熔断器会允许一部分请求通过,然后再对这些请求的响应结果进行统计。 如果这些请求的成功比例达到一定的阈值,则会把熔断器设回到关闭状态,反之则进入打开状态。

Hystrix 熔断机制

在 Hystrix 中,最核心的就是 HystrixCircuitBreaker 接口,该接口代表了对熔断器的抽象过程,如下所示:

1
2
3
4
5
public interface HystrixCircuitBreaker {
        public boolean allowRequest();
        public boolean isOpen();
    void markSuccess();
}

可以看到 HystrixCircuitBreaker 接口只有三个方法。
在 Hystrix 中,该接口的实现类是 HystrixCircuitBreakerImpl。

我们首先来看最重要的 allowRequest方法,该方法用来判断每个请求是否可被执行。
allowRequest 实际上是对 isOpen 方法做了一层封装,在通过调用 isOpen 来触发熔断器的计算逻辑之前,先根据 HystrixCommandProperties 中的配置信息来判断是否强制开启熔断器,具体实现如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public boolean allowRequest() {
    if (properties.circuitBreakerForceOpen().get()) {
        return false;
    }
    if (properties.circuitBreakerForceClosed().get()) {
        isOpen();
        return true;
    }
    return !isOpen() || allowSingleTest();
}

接下来的 isOpen方法用来获取熔断器的当前状态。
请注意,熔断器中关于阈值判断的一系列处理逻辑都位于该方法中,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean isOpen() {
    if (circuitOpen.get()) {
        return true;
    }

    HealthCounts health = metrics.getHealthCounts();
    // 检查是否达到最小请求数,如果未达到的话即使请求全部失败也不会熔断
    if (health.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
        return false;
    }

    // 检查错误百分比是否达到设定的阀值,如果未达到的话也不会熔断
    if (health.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
        return false;
    }
    // 如果错误率过高, 进行熔断,并记录下熔断时间
    if (circuitOpen.compareAndSet(false, true)) {
        circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
    }
    return true;
}

最后,我们来到用来关闭熔断器的 markSuccess 方法。
显然,该方法在熔断器处于半开状态下进行使用,我们可以通过该方法将熔断器设置为关闭状态。
这时候,熔断器要做的一件事情就是重置对请求的统计指标,如下所示:

1
2
3
4
5
6
7
public void markSuccess() {
    if (circuitOpen.get()) {
        if (circuitOpen.compareAndSet(true, false)) {                   
            metrics.resetStream();
        }
    }
}

HystrixCircuitBreaker 接口的这三个方法的执行逻辑实际上都不复杂,HystrixCircuitBreaker 通过一个 circuitOpen 状态位控制着整个熔断判断流程,而这个状态位本身的状态值则取决于系统目前的运行时数据。

用户体验

请求触发熔断后,一般会出现以下 3 种情况。

(1) 用户发出读数据的请求时遇到有些接口降级了,导致部分数据获取不到,就需要在界面上给用户一定的提示,或让用户发现不了这部分数据的缺失

(2) 用户发出写数据的请求时,熔断触发降级后,有些写操作就会改为异步,后续处理对用户没有任何影响,但要根据实际情况判断是否需要给用户提供一定的提示

(3) 用户发出写数据的请求时,熔断触发降级后,操作可能会因回滚而消除,此时必须提示用户重新操作

因此,服务调用触发了熔断降级时需要把这些情况都考虑到,以此来保证用户体验,而不是仅仅保证服务器不宕机

熔断监控

熔断功能上线后,其实只是完成了熔断设计的第一步。

因为 Hystrix 是一个事前配置的熔断框架,关于熔断配置对不对、效果好不好,只有实际使用后才知道

为此,实际使用时,还需要从 Hystrix 的监控面板查看各个服务的熔断数据,然后根据实际情况再做调整,只有这样,才能将服务器的异常损失降到最低

不足

但是 Hystrix 也有个不足。
Hystrix 的设计思想是事前配置熔断机制,也就是说,要事先预见流量是什么情况、系统负载能力如何,然后预先配置好熔断机制。
但这种操作的缺点是,一旦实际流量或系统状况与预测的不一样,预先配置好的机制就达不到预期的效果。

所以项目上线以后,需要根据监控情况调整参数。
也因为这一点,开源 Hystrix 的公司 Netflix 想使用一个动态适应的更灵活的熔断机制。
2018 年后官方不再为 Hystrix 开发新功能,转向开发 Resiliennce4j 了,对于 Hystrix 的原有功能只做简单维护

再接着说熔断。目前的熔断框架已经设计得非常好了。
对于使用熔断的人来说,虽然可以通过简单配置或代码编写实现应用,但是因为它是高并发种非常核心的一个技术,所以有必要理解清楚它的原理、机制及使用场景