Circuit Breaker

Circuit Breaker模式会处理一些需要一定时间来重连远程服务和远端资源的错误。该模式可以提高一个应用的稳定性和弹性。

问题

在类似于云的分布式环境中,当一个应用需要执行一些访问远程资源或者是远端服务的时候,是很容易碰到一些偶然的错误的,比如说,网络连接速度很慢,超时,或者是资源的过量使用,或者临时资源不再可用等等。这一类的错误通常来说会在短暂的时间内,自动恢复过来。一个健壮的云应用也该能够通过一些策略能够处理这类错误,比如使用Retry模式。

然而,也有一些情况,错误是出于一些意想不到的事件,这类事件很难预期,而且需要消耗很多时间来修正。这些错误在严重性上也从丢失部分连接到整个服务的失败。在这些情况下,让应用继续重试或者执行操作就已经没有意义了。相对的,应用应该迅速接收服务的失败,而尝试根据错误类型来采取对应的措施。

另外,如果一个服务非常繁忙,系统的部分错误可能会导致雪崩效应。举例来说,一个操作其他服务的操作可以配置超时时间的,如果服务再一段时间无法应答,调用方可以返回错误信息的。然而这个策略可能导致大量针对这个服务的请求阻塞直至Timeout时间到了。这些阻塞的请求可能会持有系统关键的资源,比如内存,线程,数据库连接等等信息。因此,引用的资源也可能会被耗尽,造成系统内其他不相关部分得失败。在这些情况下,最好的方法是令这些配置超时的操作立刻失败,并且只有当服务可能成功的时候才去调用。当然,配置较短的超时时间也能改善这一问题,但是超时时间不能配置太短,那样服务的调用反而会因为大量的超时而失败。

解决方案

Circuit-Breaker模式可以防止应用重复的尝试调用容易失败的操作,当Circuit-Breaker模式判断错会会持续的时候,它会令操作不再持续等待去浪费CPU资源。当然,Circuit-Breaker模式也令应用本身可以发现错误有没有被修复。如果发生的问题已经被修复了,应用可以重新尝试去调用服务。

Circuit-Breaker模式的目的和Retry模式的目的是不同的。Retry模式令应用不断的重试调用,直到最后成功。而Circuit-Breaker模式是阻止应用继续尝试无意义的请求。应用可以同时使用两种模式。然而,重试逻辑应用对于所有的Circuit-Breaker返回的异常十分敏感,这样可以在Circuit-Breaker发现错误短时间无法修复的情况下直接不再继续重试。

Circuit-Breaker的作用就好似可能失败操作的代理。代理会监控最近发生的错误,然后依据这一信息来决定是否允许操作的继续执行,或者直接立刻返回异常信息。

Circuit-Breaker可以按照如下的状态来模仿一个断路器来实现:

超时Timer的目的是为了给系统一段时间来自我修复之前碰到的问题。

参考如下状态变换图:

需要注意的是,上图中,关闭状态所用的错误计数器是基于时间的。它会以一定的时间间隔来重置。这也能够在常见错误的情况下不让Circuit-Breaker模式进入打开状态。而错误计数阈值才会令Circuit-Breaker进入到打开状态,只有当指定时间间隔内,错误计数达到阈值才能令Circuit-Breaker进入到打开状态。半开状态所使用的成功计数器则会记录成功的调用次数。Circuit-Breaker如果在之后出现了连续的成功的调用,那么Circuit-Breaker就会进入关闭状态。如果任何调用的失败了,那么Circuit-Breaker也会重新进入到打开状态,成功计数器也会重置,直到下次重新进入到半开状态。

通常系统的外部恢复,很多时候都是通过重启失败的组件或者修复网络连接来完成的。

实现Circuit-Braker模式可以增加系统的稳定性和弹性,当系统从错误恢复的时候,可以尽可能所有失败对系统性能的影响。Circuit-Breaker模式可以通过拒绝外部调用来保证服务的响应时间,而不是等待操作的超时(或者持续阻塞)。如果Circuit-Breaker在每一次状态改变的时候启动一些事件的话,这个状态的改变也可以用来监视Circuit-Breaker保护模块的健康状态,或者是对监控Circuit-Breaker的管理员发出警告,Circuit-Breaker已经进入了打开状态。

Circuit-Breaker模式可以很好的定制并适配很多可能的错误。举例来说,开发者可以应用一个增长的超时Timer,也可以直接令Circuit-Breaker在处于打开状态几秒,如果错误在之后还没有解决,就超时几分钟等等。在有些场景下,打开状态的Circuit-Breaker也可以不抛出异常而是返回默认值来改善应用的响应。

需要考虑的问题

开发者在实现Circuit-Breaker模式的时候,有如下的一些地方需要注意:

何时使用该模式

使用该模式:

什么场景不适合使用该模式:

Circuit-Breaker使用举例

在web应用中,有些页面是需要从外部的服务来获取数据的。如果系统实现了最小额度的缓存,那么页面的大量访问可能就会引起大量的调用。如果web应用和外部服务之间配置了超时(比如60s)的话,如果外部服务没有响应,页面会认为服务失效并抛出异常。 然而,如果服务失败了,而系统仍然非常的频繁访问,用户可能会被迫等待60秒,然后看到无结果。最后,像是内存,连接数,以及线程等资源都不足了,就算用户不再访问外部资源,可能服务也会被拒绝的。 当然,增加web服务器和使用负载均衡等方式都能一定程度上防止资源的耗尽,但是,这样仍然无法解决用户长时间等待没有响应页面的问题。 通过使用Circuit-Breaker来包裹链接外部服务的逻辑可以有效削弱上面提到的问题。用户请求将会失败,但是请求会立刻失败,但是不会导致请求资源的阻塞。 CircuitBreaker类通过内部一个ICircuitBreakerStateStore对象来维护Circuit-Breaker的状态信息。参考如下代码:

interface ICircuitBreakerStateStore
{
    CircuitBreakerStateEnum State { get; }
    Exception LastException { get; }
    DateTime LastStateChangedDateUtc { get; }
    void Trip(Exception ex);
    void Reset();
    void HalfOpen();
    bool IsClosed { get; }
}

其中的State属性表示Circuit-Breaker当前的状态,其中包含前面所提到的三个状态Open,HalfOpen,ClosedIsClose属性在状态为Closed的时候就会返回trueTrip(Exception ex)方法会将Circuit-Breaker的状态,转换到Open的状态,并且记录引起状态变化的异常信息,以及发生异常的时间等信息。LastException属性以及LastStateChangeDateUtc属性就是用来获取状态转换的异常以及时间信息的。Reset()方法则会关闭Circuit-Breaker,HalfOpen()方法则是将Circuit-Breaker的状态置为HalfOpen

public class CircuitBreaker
{
    private readonly ICircuitBreakerStateStore stateStore =
        CircuitBreakerStateStoreFactory.GetCircuitBreakerStateStore();
    private readonly object halfOpenSyncObject = new object ();
    ...

    public bool IsClosed { get { return stateStore.IsClosed; } }
    
    public bool IsOpen { get { return !IsClosed; } }
    
    public void ExecuteAction(Action action)
    {
        ...
        if (IsOpen)
        {
            // The circuit breaker is Open.
            ... (see code sample below for details)
        }
        // The circuit breaker is Closed, execute the action.
        try
        {
            action();
        }
        catch (Exception ex)
        {
            // If an exception still occurs here, simply
            // re-trip the breaker immediately.
            this.TrackException(ex);
            // Throw the exception so that the caller can tell
            // the type of exception that was thrown.
            throw;
        }
    }
    private void TrackException(Exception ex)
    {
        // For simplicity in this example, open the circuit breaker on the first exception.
        // In reality this would be more complex. A certain type of exception, such as one
        // that indicates a service is offline, might trip the circuit breaker immediately.
        // Alternatively it may count exceptions locally or across multiple instances and
        // use this value over time, or the exception/success ratio based on the exception
        // types, to open the circuit breaker.
        this.stateStore.Trip(ex);
    }
}

CircuitBreaker会创建一个实现ICircuitBreakerStateStore的实例来维护CircuitBreaker的状态。其中的ExecuteAction(Action action)方法会包含一个可能出错的方法。当这个方法运行的时候,会首先检查Circuit-Breaker的状态,如果是关闭状态,则会正常的执行远端服务的调用。如果这个操作失败掉了,则会通过TrackException(Exception ex)方法将Circuit-Breaker的状态置为打开状态。下面参考IsOpen中的代码:

if (IsOpen)
{
    // The circuit breaker is Open. Check if the Open timeout has expired.
    // If it has, set the state to HalfOpen. Another approach may be to simply
    // check for the HalfOpen state that had be set by some other operation.
    if (stateStore.LastStateChangedDateUtc + OpenToHalfOpenWaitTime < DateTime.UtcNow)
    {
        // The Open timeout has expired. Allow one operation to execute. Note that, in
        // this example, the circuit breaker is simply set to HalfOpen after being
        // in the Open state for some period of time. An alternative would be to set
        // this using some other approach such as a timer, test method, manually, and
        // so on, and simply check the state here to determine how to handle execution
        // of the action.
        // Limit the number of threads to be executed when the breaker is HalfOpen.
        // An alternative would be to use a more complex approach to determine which
        // threads or how many are allowed to execute, or to execute a simple test
        // method instead.
        bool lockTaken = false;
        try
        {
            Monitor.TryEnter(halfOpenSyncObject, ref lockTaken)
            if (lockTaken)
            {
                // Set the circuit breaker state to HalfOpen.
                stateStore.HalfOpen();
                // Attempt the operation.
                action();
                // If this action succeeds, reset the state and allow other operations.
                // In reality, instead of immediately returning to the Open state, a counter
                // here would record the number of successful operations and return the
                // circuit breaker to the Open state only after a specified number succeed.
                this.stateStore.Reset();
                return;
            }
        }
        catch (Exception ex)
        {
            // If there is still an exception, trip the breaker again immediately.
            this.stateStore.Trip(ex);
            // Throw the exception so that the caller knows which exception occurred.
            throw;
        } 
        finally
        {
            if (lockTaken)
            {
                Monitor.Exit(halfOpenSyncObject);
            }
        }
    }
}

上面的Circuit-Breaker的策略很简单,就是等待一定的时间,然后才进入HalfOpen状态,如果action()成功了,则重新恢复到Close状态。如果action()失败了则Circuit-Breaker重新进入到Open状态。

相关的其他模式

下面的模式跟Circuit-Breaker模式也是相关的: