如果大家有印象的话,尤其是夏天,如果家里用电负载过大,比如开了很多家用电器,就会”自动跳闸”,此时电路就会断开。在以前更古老的一种方式是”保险丝”,当负载过大,或者电路发生故障或异常时,电流会不断升高,为防止升高的电流有可能损坏电路中的某些重要器件或贵重器件,烧毁电路甚至造成火灾。保险丝会在电流异常升高到一定的高度和热度的时候,自身熔断切断电流,从而起到保护电路安全运行的作用。
同样,在大型的软件系统中,如果调用的远程服务或者资源由于某种原因无法使用时,如果没有这种过载保护,就会导致请求的资源阻塞在服务器上等待从而耗尽系统或者服务器资源。很多时候刚开始可能只是系统出现了局部的、小规模的故障,然而由于种种原因,故障影响的范围越来越大,最终导致了全局性的后果。软件系统中的这种过载保护就是本文将要谈到的熔断器模式(Circuit Breaker)
在大型的分布式系统中,通常需要调用或操作远程的服务或者资源,这些远程的服务或者资源由于调用者不可以控的原因比如网络连接缓慢,资源被占用或者暂时不可用等原因,导致对这些远程资源的调用失败。这些错误通常在稍后的一段时间内可以恢复正常。
但是,在某些情况下,由于一些无法预知的原因导致结果很难预料,远程的方法或者资源可能需要很长的一段时间才能修复。这种错误严重到系统的部分失去响应甚至导致整个服务的完全不可用。在这种情况下,采用不断地重试可能解决不了问题,相反,应用程序在这个时候应该立即返回并且报告错误。
通常,如果一个服务器非常繁忙,那么系统中的部分失败可能会导致 “连锁失效”(cascading failure)。比如,某个操作可能会调用一个远程的WebService,这个service会设置一个超时的时间,如果响应时间超过了该时间就会抛出一个异常。但是这种策略会导致并发的请求调用同样的操作会阻塞,一直等到超时时间的到期。这种对请求的阻塞可能会占用宝贵的系统资源,如内存,线程,数据库连接等等,最后这些资源就会消耗殆尽,使得其他系统不相关的部分所使用的资源也耗尽从而拖累整个系统。在这种情况下,操作立即返回错误而不是等待超时的发生可能是一种更好的选择。只有当调用服务有可能成功时我们再去尝试。
熔断器模式可以防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器模式也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。
熔断器模式就像是那些容易导致错误的操作的一种代理。这种代理能够记录最近调用发生错误的次数,然后决定使用允许操作继续,或者立即返回错误。
熔断器可以使用状态机来实现,内部模拟以下几种状态。
各个状态之间的转换如下图:
在Close状态下,错误计数器是基于时间的。在特定的时间间隔内会自动重置。这能够防止由于某次的偶然错误导致熔断器进入断开状态。触发熔断器进入断开状态的失败阈值只有在特定的时间间隔内,错误次数达到指定错误次数的阈值才会产生。在Half-Open状态中使用的连续成功次数计数器记录调用的成功次数。当连续调用成功次数达到某个指定值时,切换到闭合状态,如果某次调用失败,立即切换到断开状态,连续成功调用次数计时器在下次进入半断开状态时归零。
实现熔断器模式使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,并且减少了错误对系统性能的影响。它通过快速的拒绝那些试图有可能调用会导致错误的服务,而不会去等待操作超时或者永远不会不返回结果来提高系统的响应事件。如果熔断器设计模式在每次状态切换的时候会发出一个事件,这种信息可以用来监控服务的运行状态,能够通知管理员在熔断器切换到断开状态时进行处理。
可以对熔断器模式进行定制以适应一些可能会导致远程服务失败的特定场景。比如,可以在熔断器中对超时时间使用不断增长的策略。在熔断器开始进入断开状态的时候,可以设置超时时间为几秒钟,然后如果错误没有被解决,然后将该超时时间设置为几分钟,依次类推。在一些情况下,在断开状态下我们可以返回一些错误的默认值,而不是抛出异常。
在实现熔断器模式的时候,以下这些因素需可能需要考虑:
应该使用该模式来:
不适合的场景
根据上面的状态切换图,我们很容易实现一个基本的熔断器,只需要在内部维护一个状态机,并定义好状态转移的规则,可以使用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;
}}
在该类中首先:
然后,可以通过构造函数将在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次成功调用,此时状态又切换到了闭合状态。
在应用系统中,我们通常会去调用远程的服务或者资源(这些服务或资源通常是来自第三方),对这些远程服务或者资源的调用通常会导致失败,或者挂起没有响应,直到超时的产生。在一些极端情况下,大量的请求会阻塞在对这些异常的远程服务的调用上,会导致一些关键性的系统资源耗尽,从而导致级联的失败,从而拖垮整个系统。熔断器模式在内部采用状态机的形式,使得对这些可能会导致请求失败的远程服务进行了包装,当远程服务发生异常时,可以立即对进来的请求返回错误响应,并告知系统管理员,将错误控制在局部范围内,从而提高系统的稳定性和可靠性。
本文首先介绍了熔断器模式使用的场景,能够解决的问题,以及需要考虑的因素,最后使用代码展示了如何实现一个简单的熔断器,并且给出了测试用例,希望这些对您有帮助,尤其是在当您的系统调用了外部的远程服务或者资源,同时访问量又很大的情况下对提高系统的稳定性和可靠性有所帮助。