——Martin Fowler
原文
远程调用运行在不同的进程(有的可能还是跨网络的不同机器)的程序对软件系统来说是很常见的。基于内存的调用和远程调用最大的不同在于,远程调用可能失败,或者挂起,无应答直到设定超时。如果存在对多提供者调用,那情况更糟。你可能耗尽关键资源,导致跨多个系统的级联故障。Michael Nygard在他出版的非常棒的《Release It》中提出“熔断器模式”(Circuit Breaker pattern)来防止这种灾难性的连锁反应。
熔断器模式基本思想很简单,在熔断器对象里封装受保护的函数调用,由其监控调用故障。一旦故障达到某个阈值,熔断器生效熔断,接下来的调用熔断器返回错误,而根本不会进行实际的函数调用。通常你也需要知道哪些监控告警触发熔断器生效。
这里是一个超时熔断器的例子,使用Ruby编写。
我创建了一个熔断器,配置了需要保护的调用块(Lambda)。
cb = CircuitBreaker.new {|arg| @supplier.func arg}
熔断器创建时保存调用块,初始化参数(阈值,超时,监控),并设置为“关闭”状态。
class CircuitBreaker...
attr_accessor :invocation_timeout, :failure_threshold, :monitor
def initialize &block
@circuit = block
@invocation_timeout = 0.01
@failure_threshold = 5
@monitor = acquire_monitor
reset
end
如果熔断器关闭,调用熔断器会调用底层调用块,否则熔断器打开,就返回错误。
# client code
aCircuitBreaker.call(5)
class CircuitBreaker...
def call args
case state
when :closed
begin
do_call args
rescue Timeout::Error
record_failure
raise $!
end
when :open then raise CircuitBreaker::Open
else raise "Unreachable Code"
end
end
def do_call args
result = Timeout::timeout(@invocation_timeout) do
@circuit.call args
end
reset
return result
end
如果超时,我们将增加失败计数,调用成功将重置为零。
class CircuitBreaker...
def record_failure
@failure_count += 1
@monitor.alert(:open_circuit) if :open == state
end
def reset
@failure_count = 0
@monitor.alert :reset_circuit
end
我通过比较失败计数和阈值决定熔断器的状态。
class CircuitBreaker...
def state
(@failure_count >= @failure_threshold) ? :open : :closed
end
该简单的熔断器可以防止链路断开后受保护函数的调用。但是需要一个额外的措施在链路恢复正常是将熔断器复位。对于建筑中电气熔断器这是合理的方法。但软件熔断器可以让它自身来侦测底层的调用是否恢复工作。我们通过间隔一段时间再次尝试受保护的调用来实现自复位行为,如果成功就将熔断器复位。
创建这种类型的熔断器意味着需要增加一个尝试复位的阈值,并设置一个变量来记录产生最后错误的时间。
class ResetCircuitBreaker...
def initialize &block
@circuit = block
@invocation_timeout = 0.01
@failure_threshold = 5
@monitor = BreakerMonitor.new
@reset_timeout = 0.1
reset
end
def reset
@failure_count = 0
@last_failure_time = nil
@monitor.alert :reset_circuit
end
现在存在第三个状态”半开“(half open),表示链路准备好尝试一个真实的调用看看问题是否修复。
class ResetCircuitBreaker...
def state
case
when (@failure_count >= @failure_threshold) &&
(Time.now - @last_failure_time) > @reset_timeout
:half_open
when (@failure_count >= @failure_threshold)
:open
else
:closed
end
end
半开状态下会产生一个试调用,如果成功熔断器就复位,失败就重新超时计时。
class ResetCircuitBreaker...
def call args
case state
when :closed, :half_open
begin
do_call args
rescue Timeout::Error
record_failure
raise $!
end
when :open
raise CircuitBreaker::Open
else
raise "Unreachable"
end
end
def record_failure
@failure_count += 1
@last_failure_time = Time.now
@monitor.alert(:open_circuit) if :open == state
end
这是一个简单的阐述性的例子,实践中,熔断器提供更多更好的特性和参数化,常常会防范受保护调用可能产生的一系列错误,比如网络连接错误。不是所有的错误都应该使电路跳闸,有些是正常的错误,应该作为常规逻辑的一部分来处理。
当流量高峰期的时候,可能会碰到许多调用在等待最初的超时。因为远程调用通常比较慢,每个调用使用一个线程常常是一个好主意。使用future或者promise来处理回调结果。将这线程放入一个线程池,当线程池耗尽的时候,你可以将链路熔断。
这个例子显示了一个简单的跳闸方法——一个调用成功后就复位的计数器。一个更复杂的方法可以是查看错误的频率,一旦你发现比如说存在50%的失败率就跳闸。你也可以为不同的错误配置不同的阈值,比如timeout为10次,连接错误为3次。
上面我展示的例子是同步调用的熔断器,但熔断器也可以用于异步通信。这里惯用的做法就是把所有的请求放入一个队列,服务提供者以它自己的速度处理请求——这是一种避免服务过载有效方法。在这种场景下,队列填满电路就熔断。
对于自身来说,熔断器有助于降低被可能失败的操作消耗的资源的使用。你也可以避免了客户等待超时。熔断器避免了将工作负载分配到受困的服务器上。我这里说的远程调用是熔断器常用场景,但它也可以用于保护系统的一部分受其他部分故障的影响。
熔断器对于监控很有价值,熔断器状态的变更应该记录日志,熔断器应该披露详细的状态信息用于更深入的监控。熔断器常常是其环境中很深层次问题的很好的告警源。运维人员应该能够执行熔断和恢复。
熔断器本身很有价值,但使用它的用户需要对故障熔断做出反应。与任何远程调用一样你需要考虑一旦失败该如何做。它是否会导致你正在执行的操作失败? 你有没有应对的措施?信用卡的授权可以放在队列里稍后被处理;通过显示虽陈旧但够用的数据来缓解一些最新数据无法获得的情况。
进一步阅读
Netflix技术博客包含了许多关于如何改善由众多服务构成系统的可靠性的信息。Their Dependency Command讨论了断路器和线程池限制。
Netflix也开源了Hystrix,一个处理分布式系统延迟和容错的复杂工具。它包含了带有线程池限制的熔断器模式。
也有许多其他熔断器模式的开源实现,包括Ruby, Java, Grails Plugin, C#, AspectJ, and Scala。
感谢
Pavel Shpak 发现并指出了示例代码中的错误。