CircuitBreaker 断路器的基本概念和原理

最近在接 spring cloug hystrix 熔断,了解一下熔断的基本概念和原理。此篇原文是马丁花的文章 https://martinfowler.com/bliki/CircuitBreaker.html 觉得不错,一边阅读一边翻译过来以加深理解,语文不好请见谅。

软件系统向其它运行于不同进程,或是网络中不同机器上的软件发起远程调用是很常见的。内存调用和远程调用的一个比较大的差异是,远程调用可能失败或者挂起直到某一超时时限。而对于一个不能响应的服务提供方,如果有很多调用方依赖于它,情况会更糟糕,因为你将会耗尽关键资源,然后引发多个系统的级联奔溃。在作者的神书 《Release It》中, Michael Nygard 推广 Circuit Breaker 模式来防止这种可怕的级联影响。

断路器的基本思路很简单。通过将待保护的函数调用包裹在断路器对象中,让断路器对象来监控失败。当失败次数达到特定的阈值时,断路器打开,后续对此断路器对象的访问将直接返回 error,根本不会调用受保护的函数。通常,你会想在断路器打开的时候得到某种监控预警。


断路器执行过程

这边是个 ruby 写的描述断路器这种行为(对超时调用的保护)的简单示例。(虽然是 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

当调用超时时,递增失败计数器;调用成功的话,再把它重置为0。

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

现在就出现了第三种状态 “半开”,这时,断路器准备好发起一个真实的调用来检验问题是否已经修复。

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 or promise 的方式来处理响应会是个好主意。这些线程从线程池中获取,这样,你就可以在线程池过载的时候将断路器断开。

例子展示了断路跳闸的一种简单方式,在调用成功的时候重置计数器。复杂点的方式可能会观察错误频率,比如说在 50% 失败率的时候跳闸。你也可以对不同的错误设置不同的阈值,比如设置超时的阈值为 10% ,而将连接失败的阈值设置为 3% 。

上面断路器的例子只是针对同步调用的,其实断路器在异步通信时也是有用的。常用的技术有,将所有请求推入到一个队列中,服务提供方根据自身频率来消费,有效的避免了服务器超载。这时,断路器则在队列满的时候断开。

断路器能减少在操作过程中容易失败的资源捆绑在一起。你不再需要等待客户端超时,断路器避免了再向压力过大的系统增加负载。这边讲的远程调用是断路器常见的使用场景,它们也能用于任何你想将系统的一些部分隔离于其他部分的失败的场景。
监控断路器是很有价值的。断路器状态的任何变化都应该记录于日志中,并且披露出这些状态的详细细节以供更深入的监控。断路器的行为通常预示着更深层次的环境问题。操作员应该能打开和重置断路器。

断路器本身是很有价值的,但是客户端使用它的时候需要对断路失败做相应的处理。比如,对于远程调用,你需要思考调用失败的时候怎么处理。是你执行的操作失败了,或是有什么你可以补救的?信用卡授权可以放于队列中后续再处理,获取数据失败可以通过显示某些固定的数据,这些数据足够丰富,不会影响展示。

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

推荐阅读更多精彩内容