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

最近在接 spring cloug hystrix 熔斷,了解一下熔斷的基本概念和原理。此篇原文是馬丁花的文章 https://martinfowler.com/bliki/CircuitBreaker.html 覺得不錯,一邊閱讀一邊翻譯過來以加深理解,語文不好請見諒。

軟件系統(tǒng)向其它運行于不同進(jìn)程,或是網(wǎng)絡(luò)中不同機器上的軟件發(fā)起遠(yuǎn)程調(diào)用是很常見的。內(nèi)存調(diào)用和遠(yuǎn)程調(diào)用的一個比較大的差異是,遠(yuǎn)程調(diào)用可能失敗或者掛起直到某一超時時限。而對于一個不能響應(yīng)的服務(wù)提供方,如果有很多調(diào)用方依賴于它,情況會更糟糕,因為你將會耗盡關(guān)鍵資源,然后引發(fā)多個系統(tǒng)的級聯(lián)奔潰。在作者的神書 《Release It》中, Michael Nygard 推廣 Circuit Breaker 模式來防止這種可怕的級聯(lián)影響。

斷路器的基本思路很簡單。通過將待保護(hù)的函數(shù)調(diào)用包裹在斷路器對象中,讓斷路器對象來監(jiān)控失敗。當(dāng)失敗次數(shù)達(dá)到特定的閾值時,斷路器打開,后續(xù)對此斷路器對象的訪問將直接返回 error,根本不會調(diào)用受保護(hù)的函數(shù)。通常,你會想在斷路器打開的時候得到某種監(jiān)控預(yù)警。


斷路器執(zhí)行過程

這邊是個 ruby 寫的描述斷路器這種行為(對超時調(diào)用的保護(hù))的簡單示例。(雖然是 ruby 的,但是基本代碼邏輯還是可以看懂的)

首先新建一個斷路器,傳入需保護(hù)的函數(shù)調(diào)用,這是個 lambda 表達(dá)式
cb = CircuitBreaker.new {|arg| @supplier.func arg}

斷路器保存函數(shù)塊,初始化一些參數(shù)(閾值、超時時間和監(jiān)控),并重置斷路器為關(guān)閉狀態(tài)。

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

如果斷路器處于關(guān)閉狀態(tài),調(diào)用會通過斷路器訪問內(nèi)部的函數(shù);如果處于開啟狀態(tài)的話,則會直接返回錯誤。

# client code
    aCircuitBreaker.call(5)

class CircuitBreaker...
  def call args
    case state
    when :closed
      begin
        do_call args              // 斷路器關(guān)閉時,調(diào)用內(nèi)部函數(shù)
      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

當(dāng)調(diào)用超時時,遞增失敗計數(shù)器;調(diào)用成功的話,再把它重置為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

斷路器的狀態(tài)由一個閾值和失敗次數(shù)的對比決定

class CircuitBreaker...

  def state
     (@failure_count >= @failure_threshold) ? :open : :closed
  end

這個簡易的斷路器在開啟時可以避免對保護(hù)函數(shù)的調(diào)用,不過在系統(tǒng)恢復(fù)的時候需要額外的干預(yù)來重置斷路器。在建筑物中的電路斷路器是合理的,不過對于軟件斷路器,我們可以讓斷路器自身檢測內(nèi)部調(diào)用是否恢復(fù)。為了實現(xiàn)這個,我們可以間隔一個合適時間段后嘗試調(diào)用,如果調(diào)用成功,則重置斷路器。

斷路器的開啟和恢復(fù)邏輯

這種斷路器,需要增加一個閾值來重置斷路器,還需要一個變量來存儲上次失敗發(fā)生的時間

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

現(xiàn)在就出現(xiàn)了第三種狀態(tài) “半開”,這時,斷路器準(zhǔn)備好發(fā)起一個真實的調(diào)用來檢驗問題是否已經(jīng)修復(fù)。

class ResetCircuitBreaker...

  def state
    case
// 失敗次數(shù)大于閾值(開啟) 并且開啟一段時間后,為“半開”狀態(tài), 這種狀態(tài)會去檢驗
    when (@failure_count >= @failure_threshold) && 
        (Time.now - @last_failure_time) > @reset_timeout
      :half_open
    when (@failure_count >= @failure_threshold)
      :open
    else
      :closed
    end
  end

“半開” 狀態(tài)下的調(diào)用是一個測探,調(diào)用成功就重置斷路器,否則重置超時

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

這個例子用以解釋原理的,相對簡單,真實的斷路器會提供更多的功能和參數(shù)。通常它們會將受保護(hù)的調(diào)用可能引發(fā)的異常(如網(wǎng)絡(luò)連接失?。┙o隔離開來。并不是所有的錯誤都會造成短路,有些錯誤是反應(yīng)正常的失敗的,應(yīng)該作為正常邏輯的一部分處理。

訪問量大的時候,我們的很多請求會遇到一直等待響應(yīng)超時的問題。由于遠(yuǎn)程調(diào)用通常都比較慢,將每個請求放置在不同的線程里,并使用 future or promise 的方式來處理響應(yīng)會是個好主意。這些線程從線程池中獲取,這樣,你就可以在線程池過載的時候?qū)嗦菲鲾嚅_。

例子展示了斷路跳閘的一種簡單方式,在調(diào)用成功的時候重置計數(shù)器。復(fù)雜點的方式可能會觀察錯誤頻率,比如說在 50% 失敗率的時候跳閘。你也可以對不同的錯誤設(shè)置不同的閾值,比如設(shè)置超時的閾值為 10% ,而將連接失敗的閾值設(shè)置為 3% 。

上面斷路器的例子只是針對同步調(diào)用的,其實斷路器在異步通信時也是有用的。常用的技術(shù)有,將所有請求推入到一個隊列中,服務(wù)提供方根據(jù)自身頻率來消費,有效的避免了服務(wù)器超載。這時,斷路器則在隊列滿的時候斷開。

斷路器能減少在操作過程中容易失敗的資源捆綁在一起。你不再需要等待客戶端超時,斷路器避免了再向壓力過大的系統(tǒng)增加負(fù)載。這邊講的遠(yuǎn)程調(diào)用是斷路器常見的使用場景,它們也能用于任何你想將系統(tǒng)的一些部分隔離于其他部分的失敗的場景。
監(jiān)控斷路器是很有價值的。斷路器狀態(tài)的任何變化都應(yīng)該記錄于日志中,并且披露出這些狀態(tài)的詳細(xì)細(xì)節(jié)以供更深入的監(jiān)控。斷路器的行為通常預(yù)示著更深層次的環(huán)境問題。操作員應(yīng)該能打開和重置斷路器。

斷路器本身是很有價值的,但是客戶端使用它的時候需要對斷路失敗做相應(yīng)的處理。比如,對于遠(yuǎn)程調(diào)用,你需要思考調(diào)用失敗的時候怎么處理。是你執(zhí)行的操作失敗了,或是有什么你可以補救的?信用卡授權(quán)可以放于隊列中后續(xù)再處理,獲取數(shù)據(jù)失敗可以通過顯示某些固定的數(shù)據(jù),這些數(shù)據(jù)足夠豐富,不會影響展示。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容