断路器原理

断路器在HystrixCommand和HystrixObservableCommand执行过程中起到了非常重要的作用,它是Hystrix的核心部件。那么问题来了,断路器是如何决策熔断和记录信息的呢?本篇就来深入了解这其中的原理。

首先我们在服务消费者的入口类上添加@EnableCircuitBreaker注解,它的作用是开启断路器,查看一下该注解的源码:

1
2
3
4
5
6
7
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableCircuitBreakerImportSelector.class})
public @interface EnableCircuitBreaker {
}

既然开启的是CircuitBreaker(断路器),且此处必定和Hystrix相关,因此可以搜索HystrixCircuitBreaker这个对象,可以发现存在一个同名的类和接口,此处主要关注接口,查看一下该接口的源码:

1
2
3
4
5
6
7
8
9
10
11
12
public interface HystrixCircuitBreaker {
boolean allowRequest();

boolean isOpen();

void markSuccess();

public static class NoOpCircuitBreaker implements HystrixCircuitBreaker {......}

public static class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {......}

public static class Factory {......}

可以看到上面接口中定义了3个抽象方法和3个静态内部类。首先查看一下该接口中定义的三个断路器抽象方法:

  • allowRequest():每个Hystrix命令的请求都通过它判断是否被执行;
  • isOpen():返回当前断路器是否被打开;
  • markSuccess():用来闭合断路器。

再来查看一下三个静态内部类:

  • Factory类中定义了一个ConcurrentHashMap对象,该其中String类型的key通过HystrixCommandKey定义,每一个Hystrix命令都需要有一个key来标识,同时一个Hystrix命令也会在该集合中找到它所对应的断路器HystrixCircuitBreaker实例。value就是HystrixCircuitBreaker对象:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public static class Factory {
    private static ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand = new ConcurrentHashMap();

    public Factory() {
    }

    public static HystrixCircuitBreaker getInstance(HystrixCommandKey key, HystrixCommandGroupKey group, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
    HystrixCircuitBreaker previouslyCached = (HystrixCircuitBreaker)circuitBreakersByCommand.get(key.name());
    if (previouslyCached != null) {
    return previouslyCached;
    } else {
    HystrixCircuitBreaker cbForCommand = (HystrixCircuitBreaker)circuitBreakersByCommand.putIfAbsent(key.name(), new HystrixCircuitBreaker.HystrixCircuitBreakerImpl(key, group, properties, metrics));
    return cbForCommand == null ? (HystrixCircuitBreaker)circuitBreakersByCommand.get(key.name()) : cbForCommand;
    }
    }

    public static HystrixCircuitBreaker getInstance(HystrixCommandKey key) {
    return (HystrixCircuitBreaker)circuitBreakersByCommand.get(key.name());
    }

    static void reset() {
    circuitBreakersByCommand.clear();
    }
    }
  • NoOpCircuitBreaker类就是一个什么都不做的断路器实现,它允许所有的请求,且断路器始终处于闭合状态:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public static class NoOpCircuitBreaker implements HystrixCircuitBreaker {
    public NoOpCircuitBreaker() {
    }

    public boolean allowRequest() {
    return true;
    }

    public boolean isOpen() {
    return false;
    }

    public void markSuccess() {
    }
    }
  • HystrixCircuitBreakerImpl类是实现了该HystrixCircuitBreaker断路器接口的类,查看一下该类的源码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    public static class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {
    private final HystrixCommandProperties properties;
    private final HystrixCommandMetrics metrics;
    private AtomicBoolean circuitOpen = new AtomicBoolean(false);
    private AtomicLong circuitOpenedOrLastTestedTime = new AtomicLong();

    protected HystrixCircuitBreakerImpl(HystrixCommandKey key, HystrixCommandGroupKey commandGroup, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
    this.properties = properties;
    this.metrics = metrics;
    }

    public void markSuccess() {
    if (this.circuitOpen.get() && this.circuitOpen.compareAndSet(true, false)) {
    this.metrics.resetStream();
    }

    }

    public boolean allowRequest() {
    if ((Boolean)this.properties.circuitBreakerForceOpen().get()) {
    return false;
    } else if ((Boolean)this.properties.circuitBreakerForceClosed().get()) {
    this.isOpen();
    return true;
    } else {
    return !this.isOpen() || this.allowSingleTest();
    }
    }

    public boolean allowSingleTest() {
    long timeCircuitOpenedOrWasLastTested = this.circuitOpenedOrLastTestedTime.get();
    return this.circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested + (long)(Integer)this.properties.circuitBreakerSleepWindowInMilliseconds().get() && this.circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis());
    }

    public boolean isOpen() {
    if (this.circuitOpen.get()) {
    return true;
    } else {
    HealthCounts health = this.metrics.getHealthCounts();
    if (health.getTotalRequests() < (long)(Integer)this.properties.circuitBreakerRequestVolumeThreshold().get()) {
    return false;
    } else if (health.getErrorPercentage() < (Integer)this.properties.circuitBreakerErrorThresholdPercentage().get()) {
    return false;
    } else if (this.circuitOpen.compareAndSet(false, true)) {
    this.circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
    return true;
    } else {
    return true;
    }
    }
    }
    }
    可以看到我们在里面定义了4个属性,这是断路器的4个核心对象,接下来分别介绍这4个对象:
    (1)HystrixCommandProperties properties;,断路器对应HystrixCommand实例的属性对象,关于它的详细内容,我们会在后面进行介绍。
    (2)HystrixCommandMetrics metrics;,这是一个用来让HystrixCommand记录各类度量指标的对象。
    (3)AtomicBoolean circuitOpen;断路器是否打开的标志,默认为false。
    (4)AtomicLong circuitOpenedOrLastTestedTime;断路器打开或是上一次测试的时间戳。

前面也说了这个HystrixCircuitBreaker对象是一个接口,需要注意的是这个接口的实现此处采用的是内部类方式,我们只要考虑HystrixCircuitBreakerImpl这个内部类,它对CircuitBreaker接口的各个方法进行了实现,接下来将详细介绍这些方法实现的逻辑。

isOpen()方法,用于判断断路器的打开/关闭状态。首先查看该方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean isOpen() {
if (this.circuitOpen.get()) {
return true;
} else {
HealthCounts health = this.metrics.getHealthCounts();
if (health.getTotalRequests() < (long)(Integer)this.properties.circuitBreakerRequestVolumeThreshold().get()) {
return false;
} else if (health.getErrorPercentage() < (Integer)this.properties.circuitBreakerErrorThresholdPercentage().get()) {
return false;
} else if (this.circuitOpen.compareAndSet(false, true)) {
this.circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
return true;
} else {
return true;
}
}
}

简单解释一下上述代码的含义:

  • 如果断路器打开标识为true,则直接返回true,表示断路器处于打开状态,否则就从度量指标对象metrics中获取HealthCounts统计对象做进一步判断(注意该对象记录了一个滚动时间窗内的请求信息快照,默认时间窗为10秒)。

  • 如果它的请求总数(QPS)在预设的断路器请求阈值范围内就返回false,表示断路器处于未打开状态。该阈值的配置参数为circuitBreakerRequestVolumeThreshold,它的值默认为20。

  • 如果错误百分比在阈值范围内就返回false,表示断路器处于未打开状态。该阈值的配置参数为circuitBreakerErrorThresholdPercentage,它的值默认为50。

  • 如果上面两个条件都不满足,则将断路器设置为打开状态(熔断/短路)。同时,如果是从关闭状态切换到打开状态的话,就将当前时间记录到之前提到过的circuitOpenedOrLastTestedTime对象中:


allowRequest()方法,用于判断请求是否被允许。首先查看该方法的源码:

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

简单解释一下上述代码的含义:

  • 首先根据配置对象properties中的断路器判断强制打开或关闭属性是否被设置。如果强制打开,就直接返回false,拒绝请求。如果强制关闭,它会允许所有请求,但是同样也会调用isOpen()方法来执行断路器的计算逻辑,用来模拟断路器打开/关闭的行为。

在默认情况下,断路器并不会进入这两个强制打开或关闭的分支中去,而是通过!this.isOpen() || this.allowSingleTest()来判断是否允许请求访问。前面已经说过isOpen()方法用于判断和计算当前断路器是否打开,如果是断开状态就允许请求;打开状态则不允许请求。那么问题来了,这个allowSingleTest()方法的作用是干嘛的呢?


首先查看一下该方法的源码:

1
2
3
4
5
6
   public boolean allowSingleTest() {
long timeCircuitOpenedOrWasLastTested = this.circuitOpenedOrLastTestedTime.get();
return this.circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested
+ (long)(Integer)this.properties.circuitBreakerSleepWindowInMilliseconds().get()
&& this.circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis());
}

从源码中我们可以知道,这里使用了在isOpen()方法中当断路器从闭合到打开时候所记录的时间戳。当断路器在打开状态的时候,这里就会判断断开时的时间戳+配置中的circuitBreakerSleepWindowInMilliseconds时间是否小于当前时间,如果是的话,就将当前时间更新到记录断路器打开的时间对象circuitOpenedOrLastTestedTime中,并且允许此次请求。简单地说,通过circuitBreakerSleepWindowInMilliseconds属性设置了一个断路器打开之后的休眠时间(默认为5秒),在该休眠时间到达之后,将再次允许请求尝试访问,此时断路器处于“半开”状态,若此时请求继续失败,断路器又进入打开状态,并继续等待下一个休眠窗口过去之后再次尝试;若请求成功,则将断路器重新置于关闭状态。所以通过allowSingleTest()isOpen()方法的配合,实现了断路器打开和关闭状态的切换。


markSuccess()方法用于在“半开路”状态时使用。若Hystrix命令调用成功,则通过调用它将打开的断路器关闭,并重置度量指标对象。查看一下该方法的源码,如下所示:

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

断路器的工作逻辑

下图是Netflix Hystrix官方提供的关于断路器的详细执行逻辑,通过这张图就能更加清楚理解上面的内容:

依赖隔离

在前面提到了“舱壁模式”,但是并没有介绍它的含义。如果你之前使用过Docker,那么对“舱壁模式”肯定有所了解。Docker通过“舱壁模式”实现进程的隔离,使得容器和容器之间不会互相影响。而Hystrix则使用该模式实现线程池的隔离,它会为每一个依赖服务创建一个独立的线程池,这样就算某个依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的依赖服务。

通过实现对依赖服务的线程池隔离,可以带来如下优势:
(1)应用自身得到完全保护,不会受不可控的依赖服务影响。即便给依赖服务分配的线程池被填满,也不会影响应用自身的其余部分。
(2)有效降低接入新服务的风险。如果新服务接入后,运行不稳定或者存在问题,完全不会影响到应用其他的请求。
(3)当依赖的服务从失效恢复正常后,它的线程池就会被清理并且能够马上恢复健康的服务,相比之下,容器级别的清理恢复速度要慢的多。
(4)当依赖的服务出现配置错误的时候,线程池会快速反映出此问题(通过失败次数、延迟、超时、拒绝等指标的增加情况)。同时我们也可以在不影响应用功能的情况下,通过实时的动态属性刷新(后续会通过Spring Cloud Config和Spring Cloud Bus的联合使用来介绍)来处理它。
(5)当依赖的服务因实现机制调整等原因,造成其性能出现很大变化的时候,线程池的监控指标信息会反映出这样的变化。同时我们也可以通过实时动态刷新自身应用对依赖服务的阈值进行调整以适应依赖方的改变。
(6)除了上面通过线程池隔离服务发挥的优点之外,每个专有线程池都提供了内置的并发实现,可以使用它为同步的依赖服务构建异步访问。

总而言之,通过对依赖服务实现线程池隔离,可以让我们的应用变得更加健壮,不会因为某个依赖服务出现问题而引起非相关服务的异常。同时也使得我们的应用变得更加灵活,可以在不停止服务的情况下,配合动态配置刷新实现性能配置上的调整。

现在开发者可能就有一个问题,就是这里需要为每一个依赖服务都分配一个线程池,那么是否因为分配过多而导致系统的负载和开销增加呢?其实这一点不用担心,Netflix在设计Hystrix的时候,认为线程池上的开销相对于隔离所带来的优势是无法比拟的,同时也对线程池的开销做了相关测试,这个测试结果足以打消开发者对于Hystrix实现对性能影响的顾虑。

下图是Netflix Hystrix官方提供的一个Hystrix命令的性能监控图,该图以每秒处理60个请求的速度(QPS)对一个单服务实例进行访问,该服务实例每秒运行的线程数峰值为350个:

从图中的统计可以得到,使用线程隔离和不使用线程隔离的耗时差异如下表所示:

比较情况 未使用线程池隔离 使用线程池隔离 耗时差距
中位数 2ms 2ms 2ms
90百分位 5ms 8ms 3ms
99百分位 28ms 37ms 9ms
在99%的情况下,使用线程池隔离的延迟有9ms,对于大多数需求来说这样的消耗是微乎其微的,更何况可为系统在稳定性和灵活性上带来巨大的提升。虽然对于大部分的请求,我们可以忽略线程池的额外开销,而对于小部分延迟本身就非常小的请求(可能只需要1ms),那么9ms的延迟开销还是非常昂贵的。此时Hystrix也为此设计了另外的解决方案:信号量。

在Hystrix中除了可以使用线程池外,还可以使用信号量来控制单个依赖服务的并发度,信号量的开销远比线程池的开销小,但是它不能设置超时和实现异步访问。所以,只有在依赖服务是足够可靠的情况下才使用信号量。在HystrixCommand和HystrixObservableCommand中有两处是支持信号量的使用:命令执行降级逻辑
(1)命令执行。如果将隔离策略参数execution.isolation.strategy设置为SEMAPHORE,此时Hystrix则会使用信号量来替代线程池,进而控制依赖服务的并发。
(2)降级逻辑。当Hystrix尝试降级逻辑的时候,它会在调用线程中使用信号量。

信号量的默认值为10,我们也可以通过动态刷新配置的方式来控制并发线程的数量。对于信号量大小的估算方法与线程池并发的估算类似。仅访问内存数据的请求一般耗时在1ms以内,性能可以达到5000rps(rps是指每秒的请求数),这样级别的请求可以将信号量设置为1或者2,我们可以按照此标准并根据实际请求耗时来设置信号量。

隔离策略

在上面我们只是简单的介绍了依赖隔离的作用和特点,但是并没有对依赖隔离的策略进行展开,因此接下来就详细介绍其隔离策略。

Hystrix采用舱壁模式来隔离相互之间的依赖关系,并限制对其中任何一个的并发访问: