熔断器设计模式

若是你们有印象的话,尤为是夏天,若是家里用电负载过大,好比开了不少家用电器,就会”自动跳闸”,此时电路就会断开。在之前更古老的一种方式是”保险丝”,当负载过大,或者电路发生故障或异常时,电流会不断升高,为防止升高的电流有可能损坏电路中的某些重要器件或贵重器件,烧毁电路甚至形成火灾。保险丝会在电流异常升高到必定的高度和热度的时候,自身熔断切断电流,从而起到保护电路安全运行的做用。html

一样,在大型的软件系统中,若是调用的远程服务或者资源因为某种缘由没法使用时,若是没有这种过载保护,就会致使请求的资源阻塞在服务器上等待从而耗尽系统或者服务器资源。不少时候刚开始可能只是系统出现了局部的、小规模的故障,然而因为种种缘由,故障影响的范围愈来愈大,最终致使了全局性的后果。软件系统中的这种过载保护就是本文将要谈到的熔断器模式(Circuit Breaker)数据库

一 问题的产生

在大型的分布式系统中,一般须要调用或操做远程的服务或者资源,这些远程的服务或者资源因为调用者不能够控的缘由好比网络链接缓慢,资源被占用或者暂时不可用等缘由,致使对这些远程资源的调用失败。这些错误一般在稍后的一段时间内能够恢复正常。设计模式

可是,在某些状况下,因为一些没法预知的缘由致使结果很难预料,远程的方法或者资源可能须要很长的一段时间才能修复。这种错误严重到系统的部分失去响应甚至致使整个服务的彻底不可用。在这种状况下,采用不断地重试可能解决不了问题,相反,应用程序在这个时候应该当即返回而且报告错误。安全

一般,若是一个服务器很是繁忙,那么系统中的部分失败可能会致使 “连锁失效”(cascading failure)。好比,某个操做可能会调用一个远程的WebService,这个service会设置一个超时的时间,若是响应时间超过了该时间就会抛出一个异常。可是这种策略会致使并发的请求调用一样的操做会阻塞,一直等到超时时间的到期。这种对请求的阻塞可能会占用宝贵的系统资源,如内存,线程,数据库链接等等,最后这些资源就会消耗殆尽,使得其余系统不相关的部分所使用的资源也耗尽从而拖累整个系统。在这种状况下,操做当即返回错误而不是等待超时的发生多是一种更好的选择。只有当调用服务有可能成功时咱们再去尝试。服务器

二 解决方法

熔断器模式能够防止应用程序不断地尝试执行可能会失败的操做,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器模式也可使应用程序可以诊断错误是否已经修正,若是已经修正,应用程序会再次尝试调用操做。网络

熔断器模式就像是那些容易致使错误的操做的一种代理。这种代理可以记录最近调用发生错误的次数,而后决定使用容许操做继续,或者当即返回错误。数据结构

Circuit Breaker

熔断器可使用状态机来实现,内部模拟如下几种状态。并发

  • 闭合(closed)状态: 对应用程序的请求可以直接引发方法的调用。代理类维护了最近调用失败的次数,若是某次调用失败,则使失败次数加1。若是最近失败次数超过了在给定时间内容许失败的阈值,则代理类切换到断开(Open)状态。此时代理开启了一个超时时钟,当该时钟超过了该时间,则切换到半断开(Half-Open)状态。该超时时间的设定是给了系统一次机会来修正致使调用失败的错误。分布式

  • 断开(Open)状态:在该状态下,对应用程序的请求会当即返回错误响应。ide

  • 半断开(Half-Open)状态:容许对应用程序的必定数量的请求能够去调用服务。若是这些请求对服务的调用成功,那么能够认为以前致使调用失败的错误已经修正,此时熔断器切换到闭合状态(而且将错误计数器重置);若是这必定数量的请求有调用失败的状况,则认为致使以前调用失败的问题仍然存在,熔断器切回到断开方式,而后开始重置计时器来给系统必定的时间来修正错误。半断开状态可以有效防止正在恢复中的服务被忽然而来的大量请求再次拖垮。

各个状态之间的转换以下图:

Circuit Breaker State Change

在Close状态下,错误计数器是基于时间的。在特定的时间间隔内会自动重置。这可以防止因为某次的偶然错误致使熔断器进入断开状态。触发熔断器进入断开状态的失败阈值只有在特定的时间间隔内,错误次数达到指定错误次数的阈值才会产生。在Half-Open状态中使用的连续成功次数计数器记录调用的成功次数。当连续调用成功次数达到某个指定值时,切换到闭合状态,若是某次调用失败,当即切换到断开状态,连续成功调用次数计时器在下次进入半断开状态时归零。

实现熔断器模式使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,而且减小了错误对系统性能的影响。它经过快速的拒绝那些试图有可能调用会致使错误的服务,而不会去等待操做超时或者永远不会不返回结果来提升系统的响应事件。若是熔断器设计模式在每次状态切换的时候会发出一个事件,这种信息能够用来监控服务的运行状态,可以通知管理员在熔断器切换到断开状态时进行处理。

能够对熔断器模式进行定制以适应一些可能会致使远程服务失败的特定场景。好比,能够在熔断器中对超时时间使用不断增加的策略。在熔断器开始进入断开状态的时候,能够设置超时时间为几秒钟,而后若是错误没有被解决,而后将该超时时间设置为几分钟,依次类推。在一些状况下,在断开状态下咱们能够返回一些错误的默认值,而不是抛出异常。

三 要考虑的因素

在实现熔断器模式的时候,如下这些因素需可能须要考虑:

  • 异常处理:调用受熔断器保护的服务的时候,咱们必需要处理当服务不可用时的异常状况。这些异常处理一般须要视具体的业务状况而定。好比,若是应用程序只是暂时的功能降级,可能须要切换到其它的可替换的服务上来执行相同的任务或者获取相同的数据,或者给用户报告错误而后提示他们稍后重试。

  • 异常的类型:请求失败的缘由可能有不少种。一些缘由可能会比其它缘由更严重。好比,请求会失败多是因为远程的服务崩溃,这可能须要花费数分钟来恢复;也多是因为服务器暂时负载太重致使超时。熔断器应该可以检查错误的类型,从而根据具体的错误状况来调整策略。好比,可能须要不少次超时异常才能够判定须要切换到断开状态,而只须要几回错误提示就能够判断服务不可用而快速切换到断开状态。

  • 日志:熔断器应该可以记录全部失败的请求,以及一些可能会尝试成功的请求,使得的管理员可以监控使用熔断器保护的服务的执行状况。

  • 测试服务是否可用:在断开状态下,熔断器能够采用按期的ping远程的服务或者资源,来判断是否服务是否恢复,而不是使用计时器来自动切换到半断开状态。这种ping操做能够模拟以前那些失败的请求,或者可使用经过调用远程服务提供的检查服务是否可用的方法来判断。

  • 手动重置:在系统中对于失败操做的恢复时间是很难肯定的,提供一个手动重置功能可以使得管理员能够手动的强制将熔断器切换到闭合状态。一样的,若是受熔断器保护的服务暂时不可用的话,管理员可以强制的将熔断器设置为断开状态。

  • 并发问题:相同的熔断器有可能被大量并发请求同时访问。熔断器的实现不该该阻塞并发的请求或者增长每次请求调用的负担。

  • 资源的差别性:使用单个熔断器时,一个资源若是有分布在多个地方就须要当心。好比,一个数据可能存储在多个磁盘分区上(shard),某个分区能够正常访问,而另外一个可能存在暂时性的问题。在这种状况下,不一样的错误响应若是混为一谈,那么应用程序访问的这些存在问题的分区的失败的可能性就会高,而那些被认为是正常的分区,就有可能被阻塞。

  • 加快熔断器的熔断操做:有时候,服务返回的错误信息足够让熔断器当即执行熔断操做而且保持一段时间。好比,若是从一个分布式资源返回的响应提示负载超重,那么能够判定出不建议当即重试,而是应该等待几分钟后再重试。(HTTP协议定义了”HTTP 503 Service Unavailable”来表示请求的服务当前不可用,他能够包含其余信息好比,超时等)

  • 重复失败请求:当熔断器在断开状态的时候,熔断器能够记录每一次请求的细节,而不是仅仅返回失败信息,这样当远程服务恢复的时候,能够将这些失败的请求再从新请求一次。

四 使用场景

应该使用该模式来:

  • 防止应用程序直接调用那些极可能会调用失败的远程服务或共享资源。

不适合的场景

  • 对于应用程序中的直接访问本地私有资源,好比内存中的数据结构,若是使用熔断器模式只会增长系统额外开销。

  • 不适合做为应用程序中业务逻辑的异常处理替代品

五 实现

根据上面的状态切换图,咱们很容易实现一个基本的熔断器,只须要在内部维护一个状态机,并定义好状态转移的规则,可使用State模式来实现。首先,咱们定义一个表示状态转移操做的抽象类CircuitBreakerState:

public abstract class CircuitBreakerState{    protected CircuitBreakerState(CircuitBreaker circuitBreaker)
    {        this.circuitBreaker = circuitBreaker;
    }    /// <summary>
    /// 调用受保护方法以前处理的操做    /// </summary>    public virtual void ProtectedCodeIsAboutToBeCalled() {        //若是是断开状态,直接返回
        //而后坐等超时转换到半断开状态        if (circuitBreaker.IsOpen)
        {            throw new OpenCircuitException();
        }
    }    /// <summary>
    /// 受熔断器保护的方法调用成功以后的操做    /// </summary>    public virtual void ProtectedCodeHasBeenCalled()
    {
        circuitBreaker.IncreaseSuccessCount();
    }    /// <summary>
    ///受熔断器保护的方法调用发生异常操做后的操做    /// </summary>
    /// <param name="e"></param>    public virtual void ActUponException(Exception e)
    {        //增长失败次数计数器,而且保存错误信息        circuitBreaker.IncreaseFailureCount(e);        //重置连续成功次数        circuitBreaker.ResetConsecutiveSuccessCount();
    }    protected readonly CircuitBreaker circuitBreaker;
}

抽象类中,状态机CircuitBreaker经过构造函数注入;当发生错误时,咱们增长错误计数器,而且重置连续成功计数器,在增长错误计数器操做中,同时也记录了出错的异常信息。

而后在分别实现表示熔断器三个状态的类。首先实现闭合状态CloseState:

public class ClosedState : CircuitBreakerState{    public ClosedState(CircuitBreaker circuitBreaker)
        : base(circuitBreaker)
    {        //重置失败计数器        circuitBreaker.ResetFailureCount();
    }    public override void ActUponException(Exception e)
    {        base.ActUponException(e);        //若是失败次数达到阈值,则切换到断开状态        if (circuitBreaker.FailureThresholdReached())
        {
            circuitBreaker.MoveToOpenState();
        }
    }
}

在闭合状态下,若是发生错误,而且错误次数达到阈值,则状态机切换到断开状态。断开状态OpenState的实现以下:

public class OpenState : CircuitBreakerState{    private readonly Timer timer;    public OpenState(CircuitBreaker circuitBreaker)
        : base(circuitBreaker)
    {
        timer = new Timer(circuitBreaker.Timeout.TotalMilliseconds);
        timer.Elapsed += TimeoutHasBeenReached;
        timer.AutoReset = false;
        timer.Start();
    }    //断开超过设定的阈值,自动切换到半断开状态    private void TimeoutHasBeenReached(object sender, ElapsedEventArgs e)
    {
        circuitBreaker.MoveToHalfOpenState();
    }    public override void ProtectedCodeIsAboutToBeCalled()
    {        base.ProtectedCodeIsAboutToBeCalled();        throw new OpenCircuitException();
    }
}

断开状态内部维护一个计数器,若是断开达到必定的时间,则自动切换到版断开状态,而且,在断开状态下,若是须要执行操做,则直接抛出异常。

最后半断开Half-Open状态实现以下:

public class HalfOpenState : CircuitBreakerState{    public HalfOpenState(CircuitBreaker circuitBreaker)
        : base(circuitBreaker)
    {        //重置连续成功计数        circuitBreaker.ResetConsecutiveSuccessCount();
    }    public override void ActUponException(Exception e)
    {        base.ActUponException(e);        //只要有失败,当即切换到断开模式        circuitBreaker.MoveToOpenState();
    }    public override void ProtectedCodeHasBeenCalled()
    {        base.ProtectedCodeHasBeenCalled();        //若是连续成功次数达到阈值,切换到闭合状态        if (circuitBreaker.ConsecutiveSuccessThresholdReached())
        {
            circuitBreaker.MoveToClosedState();
        }
    }
}

切换到半断开状态时,将连续成功调用计数重置为0,当执行成功的时候,自增改字段,当达到连读调用成功次数的阈值时,切换到闭合状态。若是调用失败,当即切换到断开模式。

有了以上三种状态切换以后,咱们要实现CircuitBreaker类了:

public class CircuitBreaker{    private readonly object monitor = new object();    private CircuitBreakerState state;    public int FailureCount { get; private set; }    public int ConsecutiveSuccessCount { get; private set; }    public int FailureThreshold { get; private set; }    public int ConsecutiveSuccessThreshold { get; private set; }    public TimeSpan Timeout { get; private set; }    public Exception LastException { get; private set; }    public bool IsClosed
    {        get { return state is ClosedState; }
    }    public bool IsOpen
    {        get { return state is OpenState; }
    }    public bool IsHalfOpen
    {        get { return state is HalfOpenState; }
    }    internal void MoveToClosedState()
    {
        state = new ClosedState(this);
    }    internal void MoveToOpenState()
    {
        state = new OpenState(this);
    }    internal void MoveToHalfOpenState()
    {
        state = new HalfOpenState(this);
    }    internal void IncreaseFailureCount(Exception ex)
    {
        LastException = ex;
        FailureCount++;
    }    internal void ResetFailureCount()
    {
        FailureCount = 0;
    }    internal bool FailureThresholdReached()
    {        return FailureCount >= FailureThreshold;
    }    internal void IncreaseSuccessCount()
    {
        ConsecutiveSuccessCount++;
    }    internal void ResetConsecutiveSuccessCount()
    {
        ConsecutiveSuccessCount = 0;
    }    internal bool ConsecutiveSuccessThresholdReached()
    {        return ConsecutiveSuccessCount >= ConsecutiveSuccessThreshold;
    }}

在该类中首先:

  • 定义了一些记录状态的变量,如FailureCount,ConsecutiveSuccessCount 记录失败次数,连续成功次数,以及FailureThreshold,ConsecutiveSuccessThreshold记录最大调用失败次数,连续调用成功次数。这些对象对外部来讲是只读的。

  • 定义了一个 CircuitBreakerState类型的state变量,以表示当前系统的状态。

  • 定义了一些列获取当前状态的方法IsOpen,IsClose,IsHalfOpen,以及表示状态转移的方法MoveToOpenState,MoveToClosedState等,这些方法比较简单,根据名字便可看出用意。

而后,能够经过构造函数将在Close状态下最大失败次数,HalfOpen状态下使用的最大连续成功次数,以及Open状态下的超时时间经过构造函数传进来:

public CircuitBreaker(int failedthreshold, int consecutiveSuccessThreshold, TimeSpan timeout)
{    if (failedthreshold < 1 || consecutiveSuccessThreshold < 1)
    {        throw new ArgumentOutOfRangeException("threshold", "Threshold should be greater than 0");
    }    if (timeout.TotalMilliseconds < 1)
    {        throw new ArgumentOutOfRangeException("timeout", "Timeout should be greater than 0");
    }

    FailureThreshold = failedthreshold;
    ConsecutiveSuccessThreshold = consecutiveSuccessThreshold;
    Timeout = timeout;
    MoveToClosedState();
}

在初始状态下,熔断器切换到闭合状态。

而后,能够经过AttempCall调用,传入指望执行的代理方法,该方法的执行受熔断器保护。这里使用了锁来处理并发问题。

public void AttemptCall(Action protectedCode)
{    using (TimedLock.Lock(monitor))
    {
        state.ProtectedCodeIsAboutToBeCalled();
    }    try    {
        protectedCode();
    }    catch (Exception e)
    {        using (TimedLock.Lock(monitor))
        {
            state.ActUponException(e);
        }        throw;
    }    using (TimedLock.Lock(monitor))
    {
        state.ProtectedCodeHasBeenCalled();
    }
}

最后,提供Close和Open两个方法来手动切换当前状态。

public void Close()
{    using (TimedLock.Lock(monitor))
    {
        MoveToClosedState();
    }
}public void Open()
{    using (TimedLock.Lock(monitor))
    {
        MoveToOpenState();
    }
}

六 测试

以上的熔断模式,咱们能够对其创建单元测试。

首先咱们编写几个帮助类以模拟连续执行次数:

private static void CallXAmountOfTimes(Action codeToCall, int timesToCall)
{    for (int i = 0; i < timesToCall; i++)
    {
        codeToCall();
    }
}

如下类用来抛出特定异常:

private static void AssertThatExceptionIsThrown<T>(Action code) where T : Exception{    try    {
        code();
    }    catch (T)
    {        return;
    }    Assert.Fail("Expected exception of type {0} was not thrown", typeof(T).FullName);
}

而后,使用NUnit,能够创建以下Case:

[Test]public void ClosesIfProtectedCodeSucceedsInHalfOpenState()
{    var stub = new Stub(10);    //定义熔断器,失败10次进入断开状态
    //5秒后进入半断开状态
    //在半断开状态下,连续成功15次,进入闭合状态    var circuitBreaker = new CircuitBreaker(10, 15, TimeSpan.FromMilliseconds(5000));    Assert.That(circuitBreaker.IsClosed);    //失败10次调用    CallXAmountOfTimes(() => AssertThatExceptionIsThrown<ApplicationException>(() => circuitBreaker.AttemptCall(stub.DoStuff)), 10);    Assert.AreEqual(10, circuitBreaker.FailureCount);    Assert.That(circuitBreaker.IsOpen);    //等待从Open转到HalfOpen    Thread.Sleep(6000);    Assert.That(circuitBreaker.IsHalfOpen);    //成功调用15次    CallXAmountOfTimes(()=>circuitBreaker.AttemptCall(stub.DoStuff), 15);    Assert.AreEqual(15, circuitBreaker.ConsecutiveSuccessCount);    Assert.AreEqual(0, circuitBreaker.FailureCount);    Assert.That(circuitBreaker.IsClosed);
}

这个Case模拟了熔断器中状态的转换。首先初始化时,熔断器处于闭合状态,而后连续10次调用抛出异常,这时熔断器进去了断开状态,而后让线程等待6秒,此时在第5秒的时候,状态切换到了半断开状态。而后连续15次成功调用,此时状态又切换到了闭合状态。

七 结论

在应用系统中,咱们一般会去调用远程的服务或者资源(这些服务或资源一般是来自第三方),对这些远程服务或者资源的调用一般会致使失败,或者挂起没有响应,直到超时的产生。在一些极端状况下,大量的请求会阻塞在对这些异常的远程服务的调用上,会致使一些关键性的系统资源耗尽,从而致使级联的失败,从而拖垮整个系统。熔断器模式在内部采用状态机的形式,使得对这些可能会致使请求失败的远程服务进行了包装,当远程服务发生异常时,能够当即对进来的请求返回错误响应,并告知系统管理员,将错误控制在局部范围内,从而提升系统的稳定性和可靠性。

本文首先介绍了熔断器模式使用的场景,可以解决的问题,以及须要考虑的因素,最后使用代码展现了如何实现一个简单的熔断器,而且给出了测试用例,但愿这些对您有帮助,尤为是在当您的系统调用了外部的远程服务或者资源,同时访问量又很大的状况下对提升系统的稳定性和可靠性有所帮助。

八 参考文献

1. 互联网巨头为何会“宕机”, http://edge.iteye.com/blog/1933145

2. 互联网巨头为何会“宕机”(二), http://edge.iteye.com/blog/1936151

3. Circuit Breaker, http://martinfowler.com/bliki/CircuitBreaker.html

4. Circuit Breaker Pattern, http://msdn.microsoft.com/en-us/library/dn589784.aspx

做者: yangecnuyangecnu's Blog on 博客园) 
出处:http://www.cnblogs.com/yangecnu/ 
做品yangecnu 创做,采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。 欢迎转载,但任何转载必须保留完整文章,在显要地方显示署名以及原文连接。如您有任何疑问或者受权方面的协商,请 给我留言

相关文章
相关标签/搜索