熔断器

——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,一个处理分布式系统延迟和容错的复杂工具。它包含了带有线程池限制的熔断器模式。

也有许多其他熔断器模式的开源实现,包括RubyJavaGrails PluginC#AspectJ, and Scala

感谢

Pavel Shpak 发现并指出了示例代码中的错误。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容