最近在接 spring cloug hystrix 熔斷叭爱,了解一下熔斷的基本概念和原理座每。此篇原文是馬丁花的文章 https://martinfowler.com/bliki/CircuitBreaker.html 覺得不錯杨幼,一邊閱讀一邊翻譯過來以加深理解,語文不好請見諒虑啤。
軟件系統(tǒng)向其它運行于不同進程拂募,或是網(wǎng)絡(luò)中不同機器上的軟件發(fā)起遠程調(diào)用是很常見的。內(nèi)存調(diào)用和遠程調(diào)用的一個比較大的差異是帅刊,遠程調(diào)用可能失敗或者掛起直到某一超時時限纸泡。而對于一個不能響應(yīng)的服務(wù)提供方,如果有很多調(diào)用方依賴于它赖瞒,情況會更糟糕女揭,因為你將會耗盡關(guān)鍵資源蚤假,然后引發(fā)多個系統(tǒng)的級聯(lián)奔潰。在作者的神書 《Release It》中吧兔, Michael Nygard 推廣 Circuit Breaker 模式來防止這種可怕的級聯(lián)影響磷仰。
斷路器的基本思路很簡單。通過將待保護的函數(shù)調(diào)用包裹在斷路器對象中境蔼,讓斷路器對象來監(jiān)控失敗灶平。當失敗次數(shù)達到特定的閾值時,斷路器打開欧穴,后續(xù)對此斷路器對象的訪問將直接返回 error民逼,根本不會調(diào)用受保護的函數(shù)泵殴。通常涮帘,你會想在斷路器打開的時候得到某種監(jiān)控預(yù)警。
這邊是個 ruby 寫的描述斷路器這種行為(對超時調(diào)用的保護)的簡單示例笑诅。(雖然是 ruby 的调缨,但是基本代碼邏輯還是可以看懂的)
首先新建一個斷路器,傳入需保護的函數(shù)調(diào)用吆你,這是個 lambda 表達式
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
當調(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
這個簡易的斷路器在開啟時可以避免對保護函數(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) “半開”势誊,這時,斷路器準備好發(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ù)。通常它們會將受保護的調(diào)用可能引發(fā)的異常(如網(wǎng)絡(luò)連接失斝銮荨)給隔離開來赏僧。并不是所有的錯誤都會造成短路,有些錯誤是反應(yīng)正常的失敗的扭倾,應(yīng)該作為正常邏輯的一部分處理淀零。
訪問量大的時候,我們的很多請求會遇到一直等待響應(yīng)超時的問題膛壹。由于遠程調(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)增加負載。這邊講的遠程調(diào)用是斷路器常見的使用場景蓬坡,它們也能用于任何你想將系統(tǒng)的一些部分隔離于其他部分的失敗的場景猿棉。
監(jiān)控斷路器是很有價值的。斷路器狀態(tài)的任何變化都應(yīng)該記錄于日志中屑咳,并且披露出這些狀態(tài)的詳細細節(jié)以供更深入的監(jiān)控萨赁。斷路器的行為通常預(yù)示著更深層次的環(huán)境問題。操作員應(yīng)該能打開和重置斷路器兆龙。
斷路器本身是很有價值的杖爽,但是客戶端使用它的時候需要對斷路失敗做相應(yīng)的處理。比如详瑞,對于遠程調(diào)用掂林,你需要思考調(diào)用失敗的時候怎么處理臣缀。是你執(zhí)行的操作失敗了坝橡,或是有什么你可以補救的?信用卡授權(quán)可以放于隊列中后續(xù)再處理精置,獲取數(shù)據(jù)失敗可以通過顯示某些固定的數(shù)據(jù)计寇,這些數(shù)據(jù)足夠豐富,不會影響展示脂倦。