[分佈式設計] 熔斷設計

在我們的分佈式系統設計中,也應該有這樣的方式。前面說過重試機制,如果錯誤太多,或是在短時間內得不到修復,那麼我們重試也沒有意義了,此時應該開啟我們的熔斷操作,尤其是後端太忙的時候,使用熔斷設計可以保護後端不會過載。

熔斷設計

熔斷器模式可以防止應用程序不斷地嘗試執行可能會失敗的操作,使得應用程序繼續執行而不用等待修正錯誤,或者浪費 CPU 時間去等待長時間的超時產生。熔斷器模式也可以使應用程序能夠診斷錯誤是否已經修正。如果已經修正,應用程序會再次嘗試調用操作。

換句話來說,我覺得熔斷器模式就像是那些容易導致錯誤的操作的一種代理。這種代理能夠記錄最近調用發生錯誤的次數,然後決定允許操作繼續,或者立即返回錯誤。

[分佈式設計] 熔斷設計

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

閉合(Closed)狀態:我們需要一個調用失敗的計數器,如果調用失敗,則使失敗次數加 1。如果最近失敗次數超過了在給定時間內允許失敗的閾值,則切換到斷開 (Open) 狀態。此時開啟了一個超時時鐘,當該時鐘超過了該時間,則切換到半斷開(Half-Open)狀態。該超時時間的設定是給了系統一次機會來修正導致調用失敗的錯誤,以回到正常工作的狀態。在 Closed 狀態下,錯誤計數器是基於時間的。在特定的時間間隔內會自動重置。這能夠防止由於某次的偶然錯誤導致熔斷器進入斷開狀態。也可以基於連續失敗的次數。

斷開 (Open) 狀態:在該狀態下,對應用程序的請求會立即返回錯誤響應,而不調用後端的服務。這樣也許比較粗暴,有些時候,我們可以 cache 住上次成功請求,直接返回緩存(當然,這個緩存放在本地內存就好了),如果沒有緩存再返回錯誤(緩存的機制最好用在全站一樣的數據,而不是用在不同的用戶間不同的數據,因為後者需要緩存的數據有可能會很多)。

半開(Half-Open)狀態:允許應用程序一定數量的請求去調用服務。如果這些請求對服務的調用成功,那麼可以認為之前導致調用失敗的錯誤已經修正,此時熔斷器切換到閉合狀態,同時將錯誤計數器重置。

如果這一定數量的請求有調用失敗的情況,則認為導致之前調用失敗的問題仍然存在,熔斷器切回到斷開狀態,然後重置計時器來給系統一定的時間來修正錯誤。半斷開狀態能夠有效防止正在恢復中的服務被突然而來的大量請求再次拖垮。

[分佈式設計] 熔斷設計

實現熔斷器模式使得系統更加穩定和有彈性,在系統從錯誤中恢復的時候提供穩定性,並且減少了錯誤對系統性能的影響。它快速地拒絕那些有可能導致錯誤的服務調用,而不會去等待操作超時或者永遠不返回結果來提高系統的響應時間。

如果熔斷器設計模式在每次狀態切換的時候會發出一個事件,這種信息可以用來監控服務的運行狀態,能夠通知管理員在熔斷器切換到斷開狀態時進行處理。

下圖是 Netflix 的開源項目Hystrix中的熔斷的實現邏輯

[分佈式設計] 熔斷設計

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

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

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

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

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

熔斷設計的重點

在實現熔斷器模式的時候,以下這些因素需可能需要考慮。

錯誤的類型。需要注意的是請求失敗的原因會有很多種。你需要根據不同的錯誤情況來調整相應的策略。所以,熔斷和重試一樣,需要對返回的錯誤進行識別。一些錯誤先走重試的策略(比如限流,或是超時),重試幾次後再打開熔斷。一些錯誤是遠程服務掛掉,恢復時間比較長;這種錯誤不必走重試,就可以直接打開熔斷策略。

日誌監控。熔斷器應該能夠記錄所有失敗的請求,以及一些可能會嘗試成功的請求,使得管理員能夠監控使用熔斷器保護服務的執行情況。

測試服務是否可用。在斷開狀態下,熔斷器可以採用定期地 ping 一下遠程服務的健康檢查接口,來判斷服務是否恢復,而不是使用計時器來自動切換到半開狀態。這樣做的一個好處是,在服務恢復的情況下,不需要真實的用戶流量就可以把狀態從半開狀態切回關閉狀態。否則在半開狀態下,即便服務已恢復了,也需要用戶真實的請求來恢復,這會影響用戶的真實請求。

手動重置。在系統中對於失敗操作的恢復時間是很難確定的,提供一個手動重置功能能夠使得管理員可以手動地強制將熔斷器切換到閉合狀態。同樣的,如果受熔斷器保護的服務暫時不可用的話,管理員能夠強制將熔斷器設置為斷開狀態。

併發問題。相同的熔斷器有可能被大量併發請求同時訪問。熔斷器的實現不應該阻塞併發的請求或者增加每次請求調用的負擔。尤其是其中對調用結果的統計,一般來說會成為一個共享的數據結構,它會導致有鎖的情況。在這種情況下,最好使用一些無鎖的數據結構,或是 atomic 的原子操作。這樣會帶來更好的性能。

資源分區。有時候,我們會把資源分佈在不同的分區上。比如,數據庫的分庫分表,某個分區可能出現問題,而其它分區還可用。在這種情況下,單一的熔斷器會把所有的分區訪問給混為一談,從而,一旦開始熔斷,那麼所有的分區都會受到熔斷影響。或是出現一會兒熔斷一會兒又好,來來回回的情況。所以,熔斷器需要考慮這樣的問題,只對有問題的分區進行熔斷,而不是整體。

重試錯誤的請求。有時候,錯誤和請求的數據和參數有關係,所以,記錄下出錯的請求,在半開狀態下重試能夠準確地知道服務是否真的恢復。當然,這需要被調用端支持冪等調用,否則會出現一個操作被執行多次的副作用。


分享到:


相關文章: