在連線到遠端服務或資源時,處理可能需要不同時間來復原的錯誤。 這可以改善應用程式的穩定性和恢復功能。
內容和問題
在分散式環境中,對遠端資源和服務進行的呼叫可能會因為暫時性錯誤而失敗,例如低速網路連線、逾時、資源過度認可或資源暫時無法使用。 這些錯誤通常會在短時間內自行修正,而強大的雲端應用程式應會使用重試模式來準備處理這些錯誤。
不過,也可能發生因為非預期事件導致的錯誤,這可能需要更長的時間來進行修正。 這些錯誤的嚴重性範圍包含失去部分連線到整個服務無法運作。 在這些情況下,持續重試不太可能成功的作業是無意義的,相反地,應用程式應快速接受作業失敗,並且據以處理此失敗事件。
此外,如果服務非常忙碌,則系統中某一部分的失敗可能會導致階層式失敗。 例如,叫用服務的作業可設定為能實作逾時,並在該服務無法在此期間內回應時,回覆失敗訊息。 不過,此策略可能會導致同個作業的許多並行要求遭到封鎖,直到逾時期限到期。 這些已封鎖的要求可能會佔據重要的系統資源,例如記憶體、執行緒、資料庫連線等等。 因此,這些資源可能會被耗盡,導致其他必須使用相同資源但可能不相關的部分系統作業失敗。 在這些情況下,作業最好能立即失敗,並只嘗試叫用可能成功的服務。 請注意,設定較短的逾時可能有助於解決此問題,但逾時不應該過短而造成作業一直失敗,即使服務要求最終會成功。
解決方法
因 Michael Nygard 的著作《Release It!》而廣受歡迎的斷路器模式,可防止應用程式重複嘗試執行可能會失敗的作業。 一旦判斷該錯誤會持續較長時間時,該模式會讓應用程式繼續執行,而不用等候錯誤修正或浪費 CPU 循環。 斷路器模式也可讓應用程式偵測錯誤是否已解決。 如果此問題已獲得修正,應用程式可以嘗試叫用作業。
斷路器模式的用途與重試模式不同。 重試模式會在預期作業會成功的情況下,讓應用程式重試作業。 斷路器模式會防止應用程式執行很可能會失敗的作業。 應用程式可以結合這兩種模式,透過斷路器使用重試模式來叫用作業。 不過,重試邏輯應對斷路器傳回的任何例外狀況十分敏感,並在斷路器指出錯誤並非暫時性時,放棄重試。
針對可能失敗的作業,斷路器會充當 Proxy。 Proxy 應監視最近發生的失敗數量,並使用此資訊來決定是否允許作業繼續,或是立即傳回例外狀況。
Proxy 可以實作為具有下列狀態的狀態機,模擬電力斷路器的功能:
已關閉:來自應用程式的要求會路由至作業。 Proxy 會更新最近的失敗計數,如果作業呼叫不成功時,Proxy 就會增加此計數。 如果最近的失敗數量超出給定期間內的指定臨界值,則 Proxy 會置於開啟狀態。 此時的 Proxy 會啟動逾時計時器,當此計時器過期時,Proxy 會置於半開啟狀態。
逾時計時器的用途是讓系統有時間修正造成失敗的問題,然後才能允許應用程式嘗試再次執行作業。
開啟:來自應用程式的要求會立即失敗,並傳回例外狀況給應用程式。
半開啟:允許來自應用程式的有限要求數量通過,並叫用作業。 如果這些要求成功,則會假設先前導致失敗的錯誤已修正,而且斷路器會切換為已關閉狀態 (失敗計數器會重設)。 如果有任何要求失敗,斷路器就會假設該錯誤仍然存在,因此還原成開啟狀態,並重新啟動時逾時計時器,讓系統有更多時間從失敗中復原。
半開啟狀態有助於防止復原服務突然出現許多要求。 服務在復原時可以支援有限的要求量,直到復原完成,但當復原正在進行時,大量的工作會造成服務逾時或再次失敗。
圖中已關閉狀態使用的失敗計數器是以時間為基礎。 它會定期自動重設。 這有助於防止斷路器在經歷偶爾的失敗時進入開啟狀態。 只有在指定時間間隔內發生指定的失敗數量時,會達到造成斷路器進入開啟狀態的失敗臨界值。 半開啟狀態使用的計數器會記錄成功嘗試叫用作業的數目。 在指定數量的連續作業引動過程成功後,斷路器會還原為已關閉狀態。 如果有任何引動過程失敗,斷路器便會立即進入開啟狀態,而成功計數器將會在斷路器下次進入半開啟狀態時重設。
系統復原的方式會在外部處理,可能是透過還原或重新啟動失敗的元件,或是修復網路連線。
當系統從失敗中復原時,斷路器模式可以使其保持穩定,並盡可能不影響效能。 斷路器模式有助於保持系統的回應時間,因為其會快速拒絕作業可能會失敗的要求,而不是等候作業逾時或永遠不傳回。 如果斷路器在每次變更狀態時都會引發事件,則此資訊可以用來監視受到斷路器保護的部分系統健康情況,或在斷路器進入開啟狀態向系統管理員發出警示。
模式可自訂,且可根據可能發生的失敗類型進行調整。 例如,您可以將遞增的逾時計時器套用至斷路器。 一開始您可以將斷路器置於開啟狀態約幾秒鐘的時間,如果失敗事件尚未解決,則將逾時時間增加至幾分鐘,以此類推。 在某些情況下,與其讓開啟狀態傳回失敗並引發例外狀況,傳回對應用程式有意義的預設值可能會比較實用。
問題和考量
當您在決定如何實作此模式時,應考慮下列幾點:
例外狀況處理。 透過斷路器叫用作業的應用程式必須做好準備,以處理作業無法使用時引發的例外狀況。 每個應用程式都有專屬的例外狀況處理方式。 例如,應用程式可能會暫時降低功能、叫用替代作業來嘗試執行相同工作或取得相同資料,或提報例外狀況給使用者並要求他們稍後再試一次。
例外狀況類型。 要求失敗的原因可能有很多,其中有些原因所造成的失敗類型會比其他類型嚴重。 例如,要求可能會因為遠端服務當機而失敗,並將需要幾分鐘才能復原,或是因為服務暫時超載而發生逾時。 斷路器可能會檢查發生的例外狀況類型,並根據這些例外狀況的本質調整其策略。 例如,斷路器進入開啟狀態可能需要更大量的逾時例外狀況 (相較於服務完全無法使用造成的失敗數量)。
記錄。 斷路器應記錄所有失敗的要求 (和可能成功的要求),讓系統管理員可監視作業的健康情況。
可復原性。 您應針對斷路器保護的作業,將斷路器設定為符合適當的復原模式。 例如,如果斷路器長時間維持在開啟狀態,則它可能會引發例外狀況,即使失敗的原因已解決。 同樣地,如果斷路器從開啟狀態切換至半開啟狀態的速度太快,則斷路器可能會變動和減少應用程式的回應次數。
測試失敗的作業。 在開啟狀態下,斷路器會定期偵測遠端服務或資源,以判斷這些項目是否又可以使用了,而不是使用計時器來判斷何時要切換至半開啟狀態。 此偵測作業 可能會採取的方式是嘗試叫用先前失敗的作業,或使用遠端服務 (專門用於測試該服務的健康情況) 所提供的特殊作業,如健康情況端點監視模式中所述。
手動覆寫。 在失敗作業復原時間極為不同的系統中,十分適合提供手動重設選項,讓系統管理員可關閉斷路器 (和重設失敗計數器)。 同樣地,如果受斷路器保護的作業暫時無法使用,系統管理員也可以強制斷路器進入開啟狀態 (及重新啟動逾時計時器)。
並行。 大量的並行應用程式執行個體可存取同一個斷路器。 實作不應封鎖並行要求或將過多額外負荷新增至作業的每次呼叫中。
資源差異。 針對一種資源類型使用單一斷路器時請謹慎,因為其中可能有多個在根本上是獨立的提供者。 例如,在包含多個分區的資料存放區中,當一個分區發生暫時性問題時,另一個分區可能可以完整存取。 如果這些情況中的錯誤回應合併在一起,則應用程式可能會嘗試存取很可能發生失敗的部分分區,而存取其他可能成功的分區時可能會遭到封鎖。
加速斷路。 有時候失敗回應可能包含足以讓斷路器立即進行中斷的資訊,並在最短的時間內維持中斷狀態。 例如,分區資源若是因為負載過重而產生錯誤回應,則表示不建議立即重試,相反地,應用程式應稍待數分鐘後再試一次。
注意
如果服務正在對用戶端進行節流,則服務可能傳回 HTTP 429 (太多要求),如果服務目前無法使用,則傳回 HTTP 503 (服務無法使用)。 回應可以包含其他資訊,例如預期的延遲時間。
重新執行失敗的要求。 在開啟狀態下,不是只有簡單地讓要求快速失敗,斷路器也會將每個要求的詳細資料記錄到日誌,並在遠端資源或服務可用時,安排重新執行這些要求。
外部服務上不適當的逾時。 如果外部服務上設定的逾時時間過長,當外部服務中發生作業失敗時,斷路器可能無法完全保護應用程式。 如果逾時時間太長,執行斷路器的執行緒可能會遭到更長時間的封鎖,然後才讓斷路器指示作業已失敗。 在這段時間內,其他許多應用程式執行個體也可能嘗試透過斷路器叫用服務,並在集結大量執行緒後全部失敗。
使用此模式的時機
使用此模式:
- 防止應用程式嘗試叫用遠端服務或存取共用資源 (如果此作業非常有可能失敗)。
不建議使用此模式:
- 處理應用程式中的本機私人資源存取,例如記憶體內的資料結構。 在此環境中,使用斷路器會增加您系統的額外負荷。
- 作為替代方來處理您應用程式的商務邏輯例外狀況。
範例
在 Web 應用程式中,有數個頁面會填入從外部服務擷取的資料。 如果系統實作最小快取,這些頁面大部分的命中數會造成服務往返。 Web 應用程式至服務的連線通常會設定逾時時間 (通常是 60 秒),而且如果服務不在此時間內回應,每個網頁中的邏輯會假設服務無法使用,並擲回例外狀況。
不過,如果服務失敗且系統非常忙碌,則發生例外狀況前,系統會強制使用者等候 60 秒。 最後記憶體、連線和執行緒等資源可能會耗盡,並阻止其他使用者連線至系統,即使他們沒有存取從該服務擷取資料的頁面。
新增其他網頁伺服器和實作負載平衡來延展系統,可能會延遲資源耗盡的時間,但它不會解決此問題,因為使用者的要求仍然沒有回應,而且所有網頁伺服器最終仍會耗盡資源。
在斷路器中連線至服務並擷取資料的邏輯可能有助於解決此問題,並更從容地處理服務失敗。 使用者要求還是會失敗,但是這些要求會失敗地更快速,而且資源不會遭到封鎖。
CircuitBreaker
類別會在物件中維護斷路器的狀態資訊,該物件會實作 ICircuitBreakerStateStore
下列程式碼所示的介面。
interface ICircuitBreakerStateStore
{
CircuitBreakerStateEnum State { get; }
Exception LastException { get; }
DateTime LastStateChangedDateUtc { get; }
void Trip(Exception ex);
void Reset();
void HalfOpen();
bool IsClosed { get; }
}
State
屬性表示斷路器目前的狀態,可能是開啟、半開啟或已關閉,如同 CircuitBreakerStateEnum
列舉中所定義。 如果斷路器已關閉,IsClosed
屬性就是正確的,但如果是開啟或半開啟則不正確。 Trip
方法會將斷路器的狀態會切換成開啟狀態,並記錄導致狀態變更的例外狀況,包含發生例外狀況的日期和時間。 LastException
和 LastStateChangedDateUtc
屬性會傳回此資訊。 Reset
方法會關閉斷路器,而 HalfOpen
方法會將斷路器設定為半開啟。
範例中的 InMemoryCircuitBreakerStateStore
類別包含 ICircuitBreakerStateStore
介面的實作。 CircuitBreaker
類別會建立此類別的執行個體來保存斷路器的狀態。
CircuitBreaker
類別中的 ExecuteAction
方法會包裝作業,並指定為 Action
委派。 如果斷路器已關閉,ExecuteAction
會叫用Action
委派。 如果作業失敗,例外狀況處理常式會呼叫 TrackException
,它會將斷路器狀態設為開啟。 下列程式碼範例會重點說明此流程。
public class CircuitBreaker
{
private readonly ICircuitBreakerStateStore stateStore =
CircuitBreakerStateStoreFactory.GetCircuitBreakerStateStore();
private readonly object halfOpenSyncObject = new object ();
...
public bool IsClosed { get { return stateStore.IsClosed; } }
public bool IsOpen { get { return !IsClosed; } }
public void ExecuteAction(Action action)
{
...
if (IsOpen)
{
// The circuit breaker is Open.
... (see code sample below for details)
}
// The circuit breaker is Closed, execute the action.
try
{
action();
}
catch (Exception ex)
{
// If an exception still occurs here, simply
// retrip the breaker immediately.
this.TrackException(ex);
// Throw the exception so that the caller can tell
// the type of exception that was thrown.
throw;
}
}
private void TrackException(Exception ex)
{
// For simplicity in this example, open the circuit breaker on the first exception.
// In reality this would be more complex. A certain type of exception, such as one
// that indicates a service is offline, might trip the circuit breaker immediately.
// Alternatively it might count exceptions locally or across multiple instances and
// use this value over time, or the exception/success ratio based on the exception
// types, to open the circuit breaker.
this.stateStore.Trip(ex);
}
}
下列範例會示範斷路器未關閉時所執行的程式碼 (從上述範例作刪減)。 它會先檢查斷路器的開啟時間是否超過 CircuitBreaker
類別中本機 OpenToHalfOpenWaitTime
欄位所指定的時間。 如果是這種情況,ExecuteAction
方法會將斷路器設為半開啟,然後嘗試執行 Action
委派指定的作業。
如果作業成功,斷路器會重設為已關閉狀態。 如果作業失敗,斷路器會跳回開啟狀態,且發生例外狀況的時間會更新,如此一來,斷路器會在等待更長的時間之後,才嘗試再次執行作業。
如果斷路器只開啟一小段時間,小於 OpenToHalfOpenWaitTime
值,ExecuteAction
方法只會擲回 CircuitBreakerOpenException
例外狀況,並傳回造成斷路器轉換至開啟狀態的錯誤。
此外,它會使用鎖定來防止斷路器在半開啟時,對作業執行並行呼叫。 處理並行叫用作業嘗試的方式如同斷路器已開啟,作業會失敗並產生例外狀況,如稍後所述。
...
if (IsOpen)
{
// The circuit breaker is Open. Check if the Open timeout has expired.
// If it has, set the state to HalfOpen. Another approach might be to
// check for the HalfOpen state that had be set by some other operation.
if (stateStore.LastStateChangedDateUtc + OpenToHalfOpenWaitTime < DateTime.UtcNow)
{
// The Open timeout has expired. Allow one operation to execute. Note that, in
// this example, the circuit breaker is set to HalfOpen after being
// in the Open state for some period of time. An alternative would be to set
// this using some other approach such as a timer, test method, manually, and
// so on, and check the state here to determine how to handle execution
// of the action.
// Limit the number of threads to be executed when the breaker is HalfOpen.
// An alternative would be to use a more complex approach to determine which
// threads or how many are allowed to execute, or to execute a simple test
// method instead.
bool lockTaken = false;
try
{
Monitor.TryEnter(halfOpenSyncObject, ref lockTaken);
if (lockTaken)
{
// Set the circuit breaker state to HalfOpen.
stateStore.HalfOpen();
// Attempt the operation.
action();
// If this action succeeds, reset the state and allow other operations.
// In reality, instead of immediately returning to the Closed state, a counter
// here would record the number of successful operations and return the
// circuit breaker to the Closed state only after a specified number succeed.
this.stateStore.Reset();
return;
}
}
catch (Exception ex)
{
// If there's still an exception, trip the breaker again immediately.
this.stateStore.Trip(ex);
// Throw the exception so that the caller knows which exception occurred.
throw;
}
finally
{
if (lockTaken)
{
Monitor.Exit(halfOpenSyncObject);
}
}
}
// The Open timeout hasn't yet expired. Throw a CircuitBreakerOpen exception to
// inform the caller that the call was not actually attempted,
// and return the most recent exception received.
throw new CircuitBreakerOpenException(stateStore.LastException);
}
...
為使用 CircuitBreaker
物件保護作業,應用程式會建立 CircuitBreaker
類別的執行個體,並叫用 ExecuteAction
方法,指定要作為參數執行的作業。 應用程式應該準備好在作業失敗時攔截 CircuitBreakerOpenException
例外狀況,因為斷路器已開啟。 下列程式碼顯示一個範例:
var breaker = new CircuitBreaker();
try
{
breaker.ExecuteAction(() =>
{
// Operation protected by the circuit breaker.
...
});
}
catch (CircuitBreakerOpenException ex)
{
// Perform some different action when the breaker is open.
// Last exception details are in the inner exception.
...
}
catch (Exception ex)
{
...
}
相關資源
下列模式在實作此模式時也很有用:
.NET 的可靠 Web 應用程式模式 示範如何將斷路器模式套用至雲端上交集的 ASP.NET Web 應用程式。
重試模式。 說明應用程式為何可以在嘗試連線到服務或網路資源時,藉由明確地重試先前失敗的作業,處理預期的暫時性失敗。
健全狀況端點監視模式。 斷路器可以藉由將要求傳送至服務所公開的端點,來測試服務的健康情況。 此服務應傳回指出其狀態的資訊。