斷路器模式

Azure

處理在連線到遠端服務或資源時,可能需要一段時間才能復原的錯誤。 這可以改善應用程式的穩定性和復原能力。

內容和問題

在分散式環境中,遠端資源和服務的呼叫可能會因為暫時性錯誤而失敗,例如網路連線速度緩慢、逾時或資源已過度認可或暫時無法使用。 這些錯誤通常會在短時間內自行更正,而強固的雲端應用程式應該使用重試模式策略來處理這些錯誤。

不過,也可能有錯誤是由於非預期的事件,而且可能需要更長的時間才能修正。 這些錯誤的嚴重性可能從失去部分連線到服務完全失敗。 在這些情況下,應用程式可能會毫無意義地持續重試不太可能成功的作業,而應用程式應該快速接受作業失敗並據以處理此失敗。

此外,如果服務非常忙碌,系統某個部分中的失敗可能會導致串聯失敗。 例如,叫用服務的作業可以設定為實作逾時,並在服務在此期間內無法回應時回復失敗訊息。 不過,此策略可能會導致對相同作業的許多並行要求遭到封鎖,直到逾時期間到期為止。 這些封鎖的要求可能會保存重要的系統資源,例如記憶體、線程、資料庫連線等等。 因此,這些資源可能會耗盡,導致系統其他可能需要使用相同的資源之不相關的部分失敗。 在這些情況下,最好讓作業立即失敗,而且只有在服務可能成功時才會嘗試叫用服務。 請注意,設定較短的逾時可能有助於解決此問題,但逾時不應太短,因為作業大部分時間都會失敗,即使服務的要求最終會成功也一樣。

解決方案

斷路器模式,由邁克爾·尼加德在他的書《 發行它!》中普及,可以防止應用程式重複嘗試執行可能失敗的作業。 允許它繼續,而不等待錯誤被修正或浪費 CPU 週期,同時判斷該錯誤是持久的。 斷路器模式也可讓應用程式偵測是否已解決錯誤。 如果問題似乎已修正,應用程式可以嘗試叫用作業。

斷路器模式的目的與重試模式不同。 重試模式可讓應用程式在預期作業成功時重試作業。 斷路器模式可防止應用程式執行可能失敗的作業。 應用程式可以結合這兩種模式,使用重試模式透過斷路器來叫用作業。 不過,重試邏輯應該會受到斷路器所傳回之任何例外狀況的影響,而且如果斷路器指出錯誤不是暫時的,就應該放棄重試嘗試。

斷路器可作為可能失敗之作業的 Proxy。 Proxy 應該監視最近發生的失敗數目,並使用這項資訊來決定是否允許作業繼續,或只是立即傳回例外狀況。

Proxy 可以實作為狀態機器,並具有下列模擬斷路器功能的狀態機器:

  • 已關閉:從應用程式的要求會路由傳送至作業。 Proxy 會維護最近失敗次數的計數,如果對作業的呼叫失敗失敗,Proxy 就會遞增此計數。 如果最近失敗的數目超過指定時段內的指定臨界值,Proxy 就會進入 Open 狀態。 此時 Proxy 會啟動逾時定時器,而且當此定時器過期時,Proxy 會進入 半開啟 狀態。

    逾時定時器的目的是讓系統有時間修正導致失敗的問題,再讓應用程式嘗試再次執行作業。

  • 開啟:來自應用程式的要求會立即失敗,並傳回例外狀況給應用程式。

  • 半開啟:允許來自應用程式的有限數目要求通過並叫用作業。 如果這些要求成功,則假設先前造成失敗的錯誤已修正,而斷路器會切換至 [已關閉 ] 狀態(失敗計數器已重設)。 如果有任何要求失敗,斷路器會假設錯誤仍然存在,因此它會還原為 Open 狀態,並重新啟動逾時定時器,讓系統有一段進一步的時間從失敗中復原。

    半開啟狀態有助於防止復原服務突然被要求淹沒。 當服務復原時,在復原完成之前,它可能會支援有限的要求量,但復原正在進行時,大量工作可能會導致服務逾時或再次失敗。

斷路器狀態

在此圖中,關閉狀態所使用的失敗計數器是以時間為基礎。 它會定期自動重設。 這有助於防止斷路器在偶爾發生失敗時進入 開啟 狀態。 只有在指定的間隔期間發生指定的失敗數目時,才會達到將斷路器 進入 Open 狀態的失敗臨界值。 Half-Open 狀態所使用的計數器會記錄成功叫用作業的嘗試次數。 斷路器會在指定數目的連續作業調用成功之後,還原為 已關閉 狀態。 如果有任何調用失敗,斷路器會立即進入 Open 狀態,而成功計數器會在下次進入 半開啟 狀態時重設。

系統在外部復原的方式,可能是藉由還原或重新啟動失敗的元件或修復網路連線來處理。

斷路器模式在系統從失敗中復原時提供穩定性,並將效能的影響降到最低。 它可藉由快速拒絕可能失敗的作業要求,而不是等待作業逾時或永不傳回,來協助維護系統的回應時間。 如果斷路器在每次變更狀態時引發事件,這項資訊可用來監視受斷路器保護之系統部分的健康情況,或在斷路器前往 開啟 狀態時警示系統管理員。

模式是可自定義的,而且可以根據可能的失敗類型進行調整。 例如,您可以將增加的逾時定時器套用至斷路器。 您一開始可以將斷路器置於 開啟 狀態幾秒鐘,然後如果失敗尚未解決,請將逾時增加為幾分鐘,依故。 在某些情況下,相較於 Open 狀態傳回失敗並引發例外狀況,傳回對應用程式有意義的預設值可能很有用。

問題和考慮

決定如何實作此模式時,您應該考慮下列幾點:

例外狀況處理。 透過斷路器叫用作業的應用程式必須準備好,才能處理作業無法使用時所引發的例外狀況。 處理例外狀況的方式會是應用程式特定的。 例如,應用程式可能會暫時降低其功能、叫用替代作業以嘗試執行相同的工作或取得相同的數據,或向使用者報告例外狀況,並要求他們稍後再試一次。

例外狀況的類型。 要求可能會因為許多原因而失敗,其中有些可能表示比其他失敗類型更嚴重。 例如,要求可能會因為遠端服務損毀而失敗,而且需要幾分鐘的時間才能復原,或因為服務暫時超載而逾時。 斷路器可能會檢查發生的例外狀況類型,並根據這些例外狀況的性質來調整其策略。 例如,相較於因為服務完全無法使用而失敗的數目,可能需要大量的逾時例外狀況,才能將斷路器 移至 Open 狀態。

記錄。 斷路器應該記錄所有失敗的要求(以及可能成功的要求),讓系統管理員能夠監視作業的健康情況。

可復原性。 您應該將斷路器設定為符合它所保護之作業的可能復原模式。 例如,如果斷路器長時間維持在 Open 狀態,即使失敗的原因已解決,也可能引發例外狀況。 同樣地,如果斷路器從 Open 狀態切換至半開啟狀態太快,斷路器可能會波動並降低應用程式的回應時間。

測試失敗的作業。 在 [ 開啟 ] 狀態中,而不是使用定時器來判斷何時切換至 半開啟 狀態,斷路器可以改為定期 Ping 遠端服務或資源,以判斷它是否再次可供使用。 此 Ping 可能採用嘗試叫用先前失敗的作業的形式,或者可以使用遠端服務特別提供的特殊作業來測試服務的健康情況,如健康情況端點監視模式所述

手動覆寫。 在失敗作業的復原時間極可變的系統中,提供手動重設選項,可讓系統管理員關閉斷路器(並重設失敗計數器)。 同樣地,如果斷路器保護的作業暫時無法使用,系統管理員可能會強制斷路器進入 開啟 狀態(並重新啟動逾時定時器)。

並行。 應用程式的大量並行實例可以存取相同的斷路器。 實作不應該封鎖並行要求,或對作業的每個呼叫增加過多的額外負荷。

資源差異。 如果有多個基礎獨立提供者,請在針對某個類型的資源使用單一斷路器時,請小心。 例如,在包含多個分區的數據存放區中,當另一個分區遇到暫時性問題時,可能會完全存取一個分區。 如果合併這些案例中的錯誤回應,應用程式可能會嘗試存取某些分區,即使失敗的可能性很高,但存取其他分區可能會遭到封鎖,即使可能成功也一樣。

加速斷路器。 有時候,失敗回應可能會包含足夠的資訊,讓斷路器立即旅行,並保持中斷最少的時間。 例如,多載共用資源的錯誤回應可能表示不建議立即重試,而且應用程式應該在幾分鐘內再試一次。

注意

如果服務正在節流用戶端,則服務可以傳回 HTTP 429 (太多要求),如果服務目前無法使用,則傳回 HTTP 503 (服務無法使用)。 回應可以包含其他資訊,例如預期的延遲持續時間。

重新執行失敗的要求。 在 Open 狀態中,斷路器也可以記錄每個要求的詳細數據給日誌,並安排在遠端資源或服務可供使用時重新執行這些要求。

外部服務的不當逾時。 斷路器可能無法完全保護應用程式免於在設定長時間逾時期間的外部服務中失敗的作業。 如果逾時時間過長,執行斷路器的線程可能會在斷路器指出作業失敗之前封鎖一段很長的期間。 在這一次,許多其他應用程式實例可能也會嘗試透過斷路器叫用服務,並在線程全部失敗之前系結大量的線程。

使用此模式的時機

使用此模式:

  • 若要防止應用程式嘗試叫用遠端服務,或如果這項作業極有可能失敗,請存取共用資源。

不建議使用此模式:

  • 用於處理應用程式中本機私人資源的存取,例如記憶體內部數據結構。 在此環境中,使用斷路器會增加系統的額外負荷。
  • 替代在應用程式的商業規則中處理例外狀況。

工作負載設計

架構設計人員應該評估斷路器模式在工作負載的設計中如何使用,以解決 Azure 架構良好架構支柱涵蓋的目標和原則。 例如:

要素 此模式如何支援支柱目標
可靠性設計決策可協助工作負載復原到故障,並確保它會在發生失敗后復原到完全正常運作的狀態。 此模式可防止多載錯誤相依性。 您也可以使用此模式來觸發工作負載中的正常降低。 斷路器通常結合自動復原,以提供自我保護和自我修復。

- RE:03 失敗模式分析
- RE:07 暫時性錯誤
- RE:07 自我保護
效能效率 可透過調整、數據、程式代碼的優化,有效率地協助您的工作負載 符合需求 此模式可避免重試錯誤方法,這可能會導致在相依性復原期間過度使用資源,也可以在嘗試復原的相依性上多載效能。

- PE:07 程式代碼和基礎結構
- PE:11 實時問題回應

如同任何設計決策,請考慮對其他可能以此模式導入之目標的任何取捨。

範例

在 Web 應用程式中,數個頁面會填入從外部服務擷取的數據。 如果系統實作最少的快取,這些頁面的大部分點擊都會造成服務的來回行程。 從 Web 應用程式到服務的 連線 可以設定逾時期間(通常是 60 秒),如果服務在這段時間內沒有回應,則每個網頁中的邏輯會假設服務無法使用並擲回例外狀況。

不過,如果服務失敗且系統非常忙碌,使用者可能會被迫等候最多 60 秒,再發生例外狀況。 記憶體、連線和線程等資源最終可能會用盡,以防止其他用戶連線到系統,即使他們無法存取從服務擷取數據的頁面也一樣。

藉由新增進一步的 Web 伺服器並實作負載平衡來調整系統,可能會在資源耗盡時延遲,但無法解決問題,因為使用者要求仍然沒有回應,而且所有網頁伺服器最終仍可能用盡資源。

包裝連線至服務的邏輯,並在斷路器中擷取數據有助於解決此問題,並更優雅地處理服務失敗。 使用者要求仍然會失敗,但會更快失敗,而且不會封鎖資源。

類別 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 表示斷路器的目前狀態,而且會是 OpenHalfOpenClosed ,如列舉所 CircuitBreakerStateEnum 定義。 IsClosed如果斷路器已關閉,則屬性應該是 true,但如果已開啟或半開啟,則為 false。 方法會將 Trip 斷路器的狀態切換為開啟狀態,並記錄造成狀態變更的例外狀況,以及發生例外狀況的日期和時間。 LastException和屬性會傳LastStateChangedDateUtc回這項資訊。 方法 Reset 會關閉斷路器,而方法會將 HalfOpen 斷路器設定為半開啟。

範例 InMemoryCircuitBreakerStateStore 中的 類別包含 介面的實作 ICircuitBreakerStateStore 。 類別 CircuitBreaker 會建立這個類別的實例,以保存斷路器的狀態。

類別 ExecuteAction 中的 CircuitBreaker 方法會包裝指定為委派的 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)
{
  ...
}

實作此模式時,下列模式可能也很有用: