最近在接 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ù)警。

這邊是個 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ā)生的時間
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ù)足夠豐富,不會影響展示。