架構設計之熔斷設計

 
背景
    熔斷機制這個詞對你來說肯定不陌生,它的靈感來源於我們電閘上的 ” 保險絲 “,當電壓有 問題時(比如短路),自動跳閘,此時電路就會斷開,我們的電器就會受到保護。不然,會 導致電器被燒壞,如果人沒在家或是人在熟睡中,還會導致火災。所以,在電路世界通常都 會有這樣的自我保護裝置。 同樣,在我們的分散式系統設計中,也應該有這樣的方式。前面說過重試機制,如果錯誤太 多,或是在短時間內得不到修復,那麼我們重試也沒有意義了,此時應該開啟我們的熔斷操 作,尤其是後端太忙的時候,使用熔斷設計可以保護後端不會過載。 
 
熔斷設計
   熔斷器模式可以防止應用程式不斷地嘗試執行可能會失敗的操作,使得應用程式繼續執行而 不用等待修正錯誤,或者浪費 CPU 時間去等待長時間的超時產生。熔斷器模式也可以使應 用程式能夠診斷錯誤是否已經修正。如果已經修正,應用程式會再次嘗試調用操作。 換句話來說,我覺得熔斷器模式就像是那些容易導致錯誤的操作的一種代理。這種代理能夠 記錄最近調用發生錯誤的次數,然後決定允許操作繼續,或者立即返回錯誤。

 

 

熔斷器可以使用狀態機來實現,內部模擬以下幾種狀態。

閉合(Closed)狀態:我們需要一個調用失敗的計數器,如果調用失敗,則使失敗次數 加 1。如果最近失敗次數超過了在給定時間內允許失敗的閾值,則切換到斷開 (Open) 狀 態。此時開啟了一個超時時鐘,當該時鐘超過了該時間,則切換到半斷開(HalfOpen)狀態。該超時時間的設定是給了系統一次機會來修正導致調用失敗的錯誤,以回 到正常工作的狀態。在 Closed 狀態下,錯誤計數器是基於時間的。在特定的時間間隔內 會自動重置。這能夠防止由於某次的偶然錯誤導致熔斷器進入斷開狀態。也可以基於連續 失敗的次數。
斷開 (Open) 狀態:在該狀態下,對應用程式的請求會立即返回錯誤響應,而不調用後 端的服務。這樣也許比較粗暴,有些時候,我們可以 cache 住上次成功請求,直接返回 快取(當然,這個快取放在本地記憶體就好了),如果沒有快取再返回錯誤(快取的機制最 好用在全站一樣的數據,而不是用在不同的用戶間不同的數據,因為後者需要快取的數據 有可能會很多)。
半開(Half-Open)狀態:允許應用程式一定數量的請求去調用服務。如果這些請求對 服務的調用成功,那麼可以認為之前導致調用失敗的錯誤已經修正,此時熔斷器切換到閉 合狀態,同時將錯誤計數器重置。 如果這一定數量的請求有調用失敗的情況,則認為導致之前調用失敗的問題仍然存在,熔 斷器切回到斷開狀態,然後重置計時器來給系統一定的時間來修正錯誤。半斷開狀態能夠 有效防止正在恢復中的服務被突然而來的大量請求再次拖垮。
 

 

 

      實現熔斷器模式使得系統更加穩定和有彈性,在系統從錯誤中恢復的時候提供穩定性,並且 減少了錯誤對系統性能的影響。它快速地拒絕那些有可能導致錯誤的服務調用,而不會去等 待操作超時或者永遠不返回結果來提高系統的響應時間。 如果熔斷器設計模式在每次狀態切換的時候會發出一個事件,這種資訊可以用來監控服務的 運行狀態,能夠通知管理員在熔斷器切換到斷開狀態時進行處理。 下圖是 Netflix 的開源項目Hystrix中的熔斷的實現邏輯。

 

 

 
從這個流程圖中,可以看到

 1. 有請求來了,首先 allowRequest() 函數判斷是否在熔斷中,如果不是則放行,如果是的 話,還要看有沒有到達一個熔斷時間片,如果熔斷時間片到了,也放行,否則直接返回 出錯。

2. 每次調用都有兩個函數 markSuccess(duration) 和 markFailure(duration) 來統計一下 在一定的 duration 內有多少調用是成功還是失敗的。

3. 判斷是否熔斷的條件 isOpen(),是計算一下 failure/(success+failure) 當前的錯誤率, 如果高於一個閾值,那麼打開熔斷,否則關閉。

4. Hystrix 會在記憶體中維護一個數組,其中記錄著每一個周期的請求結果的統計。超過時長 長度的元素會被刪除掉。

 

熔斷設計的重點
 
 在實現熔斷器模式的時候,以下這些因素需可能需要考慮。
 
 錯誤的類型:
  需要注意的是請求失敗的原因會有很多種。你需要根據不同的錯誤情況來調 整相應的策略。所以,熔斷和重試一樣,需要對返回的錯誤進行識別。一些錯誤先走重試 的策略(比如限流,或是超時),重試幾次後再打開熔斷。一些錯誤是遠程服務掛掉,恢 復時間比較長;這種錯誤不必走重試,就可以直接打開熔斷策略。
 
日誌監控:
  熔斷器應該能夠記錄所有失敗的請求,以及一些可能會嘗試成功的請求,使得 管理員能夠監控使用熔斷器保護服務的執行情況。
 
測試服務是否可用:
  在斷開狀態下,熔斷器可以採用定期地 ping 一下遠程服務的健康檢 查介面,來判斷服務是否恢復,而不是使用計時器來自動切換到半開狀態。這樣做的一個 好處是,在服務恢復的情況下,不需要真實的用戶流量就可以把狀態從半開狀態切回關閉 狀態。否則在半開狀態下,即便服務已恢復了,也需要用戶真實的請求來恢復,這會影響 用戶的真實請求。
 
手動重置:
  在系統中對於失敗操作的恢復時間是很難確定的,提供一個手動重置功能能夠 使得管理員可以手動地強制將熔斷器切換到閉合狀態。同樣的,如果受熔斷器保護的服務 暫時不可用的話,管理員能夠強制將熔斷器設置為斷開狀態。
 
並發問題:
  相同的熔斷器有可能被大量並發請求同時訪問。熔斷器的實現不應該阻塞並發 的請求或者增加每次請求調用的負擔。尤其是其中對調用結果的統計,一般來說會成為一 個共享的數據結構,它會導致有鎖的情況。在這種情況下,最好使用一些無鎖的數據結 構,或是 atomic 的原子操作。這樣會帶來更好的性能。
 
資源分區:
  有時候,我們會把資源分布在不同的分區上。比如,資料庫的分庫分表,某個 分區可能出現問題,而其它分區還可用。在這種情況下,單一的熔斷器會把所有的分區訪 問給混為一談,從而,一旦開始熔斷,那麼所有的分區都會受到熔斷影響。或是出現一會 兒熔斷一會兒又好,來來回回的情況。所以,熔斷器需要考慮這樣的問題,只對有問題的 分區進行熔斷,而不是整體。
 
重試錯誤的請求:
  有時候,錯誤和請求的數據和參數有關係,所以,記錄下出錯的請求, 在半開狀態下重試能夠準確地知道服務是否真的恢復。當然,這需要被調用端支援冪等調 用,否則會出現一個操作被執行多次的副作用。
 
推薦