雲計算模型- 斷路器模式

斷路器模式

當鏈接到遠程服務或資源到時候,處理那些須要一段時間才能修復的系統缺陷。這能優化應用對穩定性和可靠性。web

上下文和問題

在分佈式環境中,對遠端服務或資源的請求可能會因爲諸如如下臨時性錯誤而失敗:緩慢的網絡請求,鏈接超時,資源被過分使用,或服務臨時不可用。一般狀況下,這些錯誤可以在短暫的中斷後自我修復。一個健壯的雲端應用應該可以經過重試模式等策略來處理這些問題。數據庫

然而,有的時候這些錯誤緣於一些未知的事件,從而須要更長的時間修復。這些錯誤多是系統一部分沒法鏈接,或是整個服務都響應失敗。在這些狀況下,盲目的去重試以前的操做可能並無意義,並且也不太可能會成功,取而代之系統應該快速識別出操做失敗而後去處理這些失敗。緩存

另外,若是一個服務很是繁忙,系統中的一部分出錯將致使級連的錯誤。例如,一個調用其餘服務的操做能夠設定一個超時,而後在超時後返回錯誤。然而,這個策略可能致使不少訪問這個服務的併發請求阻塞,直到超時。這些阻塞的請求可能佔用了重要的系統資源,諸如內存,線程,數據庫連接等。所以可能致使這些資源被耗盡,進而致使其餘不相干的模塊由於資源競爭而失敗。在這些狀況下,直接讓這些操做失敗,而後在合適的時候再去嘗試調用這些服務,彷佛是更合理的選擇。設定一個短一些的超時時長可能會有助於解決這個問題,可是又不能設定的過短而中斷那些最終可能成功的請求。服務器

解決方案

由 Michael Nygard 在其[書中](https://pragprog.com/book/mnee/release-it)普及的斷路器模式,可以阻止應用重複的嘗試執行可能失敗的請求。這容許系統繼續運行,而不用等待那些錯誤被修復,也不用浪費 CPU 循環,由於它已經識別到該錯誤是持續性的。斷路器模式也使系統可以檢測出錯誤是否已被修復。若是問題已經被修復,系統可以從新調用該操做。網絡

斷路器模式的目的和重試模式有所不一樣。重試模式使應用可以重試指望成功的操做。斷路器模式阻止應用去調用極可能失敗的操做。應用能夠聯合使用兩種模式。然而,重試邏輯應該可以處理斷路器模式拋出的異常,並在斷路器指示該錯誤非短時間可修復的錯誤時,中止重試。數據結構

斷路器爲可能會失敗的操做充當代理的角色。這個代理監視最近發生的失敗的數量,而後用這些信息判斷是否繼續執行該操做,仍是直接返回異常。併發

該代理能夠經過一個狀態機來實現,該狀態機應模擬電子斷路器來實現如下狀態:app

  • 關閉:來自應用的請求直接路由到對應的操做。代理維護一個計數器來記錄最近失敗的次數。若是一個操做失敗,該計數器加一。若是最近失敗的次數在指定時間段內超過一個閾值,代理被設定到 開啓 狀態。同時,代理啓動一個計時器,當計時器超時後,代理被設定到 半開狀態。負載均衡

    設定計時器的目的是在應用重試該操做前,給系統留出時間修復致使該錯誤的問題。less

  • 開啓:從應用發送給該服務的請求直接失敗,並返回異常。

  • 半開:容許少許的請求經過代理調用該操做。若是請求成功,系統假定以前引發操做失敗的錯誤已被修復,斷路器設定到 關閉狀態(且將失敗計數器重置)。若是任何請求失敗,斷路器便假定以前的錯誤依舊存在,而後把狀態從新置爲打開,重啓超時計時器,併爲系統恢復該錯誤設定更長的恢復時間。

    半開 狀態有助於使恢復中的系統避免遭受突發的大量請求。在服務恢復過程當中,它可能只能支撐有限數量的請求,直至恢復徹底完成。在恢復過程當中接收大量請求,可能會使服務超時,甚至再次失敗。

斷路器狀態機

在上圖中,關閉狀態下使用的計數器是基於時間的,它會自動按期重置。這可以使斷路器避免因偶發性失敗而切換到失敗狀態。失敗閾值設定使斷路器只有在指定的時間內失敗的次數達到了指定值後才切換到失敗狀態。半開狀態下使用的計數器用來記錄請求成功的次數。當連續成功的請求數量超過一個指定值後,斷路器將切換到 關閉狀態。若是任一調用失敗,斷路器將直接進入打開狀態,下次進入半開狀態的時候,成功計數器將被清零。

系統如何修復是屬於本模式之外的內容,可能經過從新加載數據,重啓失敗的組件,或是修復網絡問題。

斷路器模式爲在從錯誤中恢復的系統提供穩定性,同時下降對性能的影響。它經過快速駁回可能失敗的請求來下降系統響應時間。若是每次斷路器切換狀態時都觸發一個時間,則能夠用來監視斷路器保護部分的系統狀態,或在斷路器切換到 打開狀態時爲管理員提供報警。

這個模式是可定製的,並且可適配不一樣類型的錯誤。例如,你能夠將超時計數器的值調高,你能夠將斷路器處在狀態的初始值設爲幾秒,而後若是到時後失敗未解決將超時器設爲幾分鐘等。在一些狀況下,除了讓處在狀態的斷路器返回失敗和異常,也能夠將其配置爲返回一個對應用有意義的默認值。

問題和注意事項

當考慮如何實現該模式時,須要考慮以下問題:

異常處理。應用經過斷路器調用服務需準備好如何處理因服務沒法訪問而產生的異常。處理異常的方式因應用不一樣而不一樣。例如,應用應能臨時降級它對應的功能,調用候選的能得到一樣數據的應用,或向用戶報告錯誤,請其事後重試。

異常的類型。請求可能因爲各類緣由而失敗,其中一些致使的問題可能比其餘更嚴重。例如,請求可能因爲外部服務宕機而失敗從而中斷數分鐘,或者因爲服務過載而致使超時。斷路器可能可以檢測異常的類型從而使用不一樣的策略。例如,若是要把斷路器設定到 狀態,超時類型到錯誤次數的閾值要比系統徹底不可用的閾值要高很懂。

日誌。斷路器應該記錄全部失敗的請求(如何能夠,也能夠記錄成功的)來容許管理員來監控操做的健康情況。

可恢復性。你應該爲斷路器配置其保護的操做可能的恢復模型。例如,若是斷路器在打開狀態維持了很長時間,可能致使即便錯誤已經修復,斷路器仍拋出異常。相似的,若是斷路器從半開的時間過短,可能致使它上下波動,減小應用的響應時間(??沒懂)。

測試失敗的操做。在的狀態下,除了用計數器來決定什麼時候切換到半開狀態,斷路器還能夠啓用一個定時任務來週期性 ping 遠端服務來判斷該服務是否已能夠訪問。能夠採用嘗試調用以前失敗的服務的形式,或調用遠端服務提供的專門用來測試服務狀態的操做,如健康情況健康模式 所描述的那樣。

手動重載。對於系統恢復時間波動很是大的系統,提供一個手動重置選項來方便管理員關閉斷路器(同時重置失敗計數器)是頗有用的。相似的,若是斷路器所保護的服務臨時不可用,管理員能夠強制打開斷路器將其置爲狀態(同時重置計時器)。

併發。斷路器可能同時被大量客戶端訪問。其實現不用阻塞併發的請求,也不能給操做添加過多的額外負載。

資源區分。當咱們爲一個由多個獨立的提供者提供的同一個資源使用斷路器時,咱們須要額外注意。例如,在一個由多個分片的數據存儲資源中,即使其餘分片遇到臨時錯誤,單個分片也能夠接受徹底的訪問。若是在這種場景中,這些錯誤被合併成同一錯誤,應用可能會在某些分片錯誤時嘗試去訪問其餘分片,但因爲斷路器的存在,對其餘分片的訪問也可能會被阻塞,即便它們可能成功。

加速熔斷。有時候返回的錯誤信息包含足夠信息令斷路器斷路。例如,一個共享資源過載,可直接另斷路器斷路而避免應用立刻重試。

[!注意事項]
一個服務可能在限流時返回 HTTP 429(太多的請求),或者在服務當前不可用時返回 HTTP 503(服務不可用)。HTTP 返回信息中可能包含了額外信息,好比下次重試的間隔時間等。

重放失敗的請求。在打開的狀態下,除了直接返回失敗,斷路器也能夠將每一個請求的詳細信息記錄到日誌中,而後而後在遠程資源可訪問後,重放該請求。
外部服務不適合的超時。斷路器不適合用來保護那些設置了過長超時時長的外部服務。若是超時時間過長,斷路器的線程可能阻塞,在這段時間內,其餘應用可能耶嘗試調用這個服務,從而致使斷路器消耗大量的線程。

何時使用該模式

在如下場景可使用該模式:

  • 阻止應用訪問一個極可能失敗的共享的遠程服務或資源。

如下場景不該該用該模式:

  • 訪問本地資源,好比內存中的數據結構。在這個場景中,使用斷路器將爲你的系統帶來額外的開銷。
  • 用來替代業務邏輯中的異常處理。

例子

在 web 應用中,頁面是根據外部服務得到的數據計算生成的。若是系統設定較少的緩存策略,大多數頁面點擊都會調用一次服務。從 web 應用到服務的請求能夠設定超時時間(一般是60秒),若是服務在這段時間內未響應,頁面的邏輯將認爲服務不可用並拋出異常。

然而,若是服務失敗而且系統很是繁忙,用戶可能須要等60秒纔會被提示異常。最終內存,連接,線程等資源可能會用盡,阻止其餘用戶鏈接系統,即便它們並非訪問失敗的那個服務。

經過添加更多的網絡服務器和實現負載均衡來爲系統擴容可以延緩資源耗盡的時間,但這並不會解決這個問題由於用戶的請求仍會未響應而且最終因此網絡服務器的資源終會耗盡。

爲訪問該服務查詢數據的鏈接包裹一層斷路器可以解決該問題,而且能更優雅地解決服務失敗。用戶的請求仍會失敗,但失敗將會更迅速而且不會阻塞資源。

The CircuitBreaker class maintains state information about a circuit breaker in an object that implements the ICircuitBreakerStateStore interface shown in the following code.

interface ICircuitBreakerStateStore
{
  CircuitBreakerStateEnum State { get; }

  Exception LastException { get; }

  DateTime LastStateChangedDateUtc { get; }

  void Trip(Exception ex);

  void Reset();

  void HalfOpen();

  bool IsClosed { get; }
}

The State property indicates the current state of the circuit breaker, and will be either Open, HalfOpen, or Closed as defined by the CircuitBreakerStateEnum enumeration. The IsClosed property should be true if the circuit breaker is closed, but false if it's open or half open. The Trip method switches the state of the circuit breaker to the open state and records the exception that caused the change in state, together with the date and time that the exception occurred. The LastException and the LastStateChangedDateUtc properties return this information. The Reset method closes the circuit breaker, and the HalfOpen method sets the circuit breaker to half open.

The InMemoryCircuitBreakerStateStore class in the example contains an implementation of the ICircuitBreakerStateStore interface. The CircuitBreaker class creates an instance of this class to hold the state of the circuit breaker.

The ExecuteAction method in the CircuitBreaker class wraps an operation, specified as an Action delegate. If the circuit breaker is closed, ExecuteAction invokes the Action delegate. If the operation fails, an exception handler calls TrackException, which sets the circuit breaker state to open. The following code example highlights this flow.

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);
  }
}

The following example shows the code (omitted from the previous example) that is executed if the circuit breaker isn't closed. It first checks if the circuit breaker has been open for a period longer than the time specified by the local OpenToHalfOpenWaitTime field in the CircuitBreaker class. If this is the case, the ExecuteAction method sets the circuit breaker to half open, then tries to perform the operation specified by the Action delegate.

If the operation is successful, the circuit breaker is reset to the closed state. If the operation fails, it is tripped back to the open state and the time the exception occurred is updated so that the circuit breaker will wait for a further period before trying to perform the operation again.

If the circuit breaker has only been open for a short time, less than the OpenToHalfOpenWaitTime value, the ExecuteAction method simply throws a CircuitBreakerOpenException exception and returns the error that caused the circuit breaker to transition to the open state.

Additionally, it uses a lock to prevent the circuit breaker from trying to perform concurrent calls to the operation while it's half open. A concurrent attempt to invoke the operation will be handled as if the circuit breaker was open, and it'll fail with an exception as described later.

...
    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);
    }
    ...

To use a CircuitBreaker object to protect an operation, an application creates an instance of the CircuitBreaker class and invokes the ExecuteAction method, specifying the operation to be performed as the parameter. The application should be prepared to catch the CircuitBreakerOpenException exception if the operation fails because the circuit breaker is open. The following code shows an example:

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)
{
  ...
}

相關模式與指導

在實現該模式時,如下模式也會有幫助:

  • 重試模式
    當應用嘗試鏈接服務或網絡資源時遇到臨時性錯誤時,簡單的重試以前失敗的操做。

  • 健康狀態監控模式
    斷路器應該可以經過給服務端點發送請求來測試服務的健康情況。服務應該可以返回信息來代表自身情況。

翻譯自 Azure Cloud Design Patterns

相關文章
相關標籤/搜索