(二)—— 斷路器模式

2018-02-24 15:44 更新

云計(jì)算設(shè)計(jì)模式(二)——斷路器模式

處理故障連接到遠(yuǎn)程服務(wù)或資源時,可能需要耗費(fèi)大量的時間。這種模式可以提高應(yīng)用程序的穩(wěn)定性和靈活性。

背景和問題

在分布式環(huán)境中,如在云,其中,應(yīng)用程序執(zhí)行訪問遠(yuǎn)程資源和服務(wù)的操作,有可能對這些操作的失敗是由于瞬時故障,如慢的網(wǎng)絡(luò)連接,超時,或者被過度使用的資源或暫時不可用。這些故障一般之后的短時間內(nèi)糾正自己,和一個強(qiáng)大的云應(yīng)用應(yīng)該準(zhǔn)備使用的策略來處理它們,例如,通過重試模式進(jìn)行說明。

但是,也可以是其中的故障是由于那些不容易預(yù)見的突發(fā)事件的情況下,這可能需要更長的時間來糾正。這些故障從連接的部分損失到服務(wù)的完整的故障范圍的嚴(yán)重程度。在這種情況下,它可能是毫無意義的應(yīng)用,不斷重試執(zhí)行的操作是不太可能成功,而不是應(yīng)用程序應(yīng)該很快接受該操作已失敗,并相應(yīng)地處理這個故障。

此外,如果一個服務(wù)是非常繁忙的,在系統(tǒng)中的一個部分出現(xiàn)故障可能會導(dǎo)致連鎖故障。例如,調(diào)用一個服務(wù)的操作可被配置成實(shí)現(xiàn)一個超時,如果該服務(wù)無法在這段時間內(nèi)響應(yīng)一個失敗消息答復(fù)。然而,這一策略可能導(dǎo)致許多并發(fā)請求的相同的操作,直到超時時段期滿被阻止。這些被禁止的請求可能會持有關(guān)鍵系統(tǒng)資源,如內(nèi)存,線程,數(shù)據(jù)庫連接等。因此,這些資源可能會耗盡,從而導(dǎo)致該系統(tǒng)的其他可能無關(guān)的部件,需要使用相同的資源的失敗。在這些情況下,這將是優(yōu)選的操作立即失敗,并且只嘗試調(diào)用服務(wù),如果它是可能成功。注意,設(shè)置一個較短的超時可能有助于解決此問題,但在超時不應(yīng)該如此之短,以致操作失敗的大部分時間,即使該請求到服務(wù)最終會成功。

解決方案

該斷路器圖案可以防止一個應(yīng)用程序多次試圖執(zhí)行一個操作,即很可能失敗,允許它繼續(xù)而不等待故障恢復(fù)或者浪費(fèi) CPU 周期,而它確定該故障是持久的。斷路器模式也使應(yīng)用程序能夠檢測故障是否已經(jīng)解決。如果問題似乎已經(jīng)得到糾正??,應(yīng)用程序可以嘗試調(diào)用操作。

注意

斷路器圖案的目的是從該重試模式的不同。重試模式使應(yīng)用程序可以重試操作以期望它會成功。斷路器圖案防止應(yīng)用程序執(zhí)行一個操作,即很可能失敗。一個應(yīng)用程序可以通過使用重試模式,通過一個斷路器調(diào)用操作結(jié)合這兩種模式。然而,在重試邏輯應(yīng)該是由斷路器返回任何異常敏感和放棄重試次數(shù),如果斷路器指示故障不是瞬時的。

斷路器充當(dāng)可能失敗操作的代理。代理應(yīng)監(jiān)測最近發(fā)生的故障數(shù)量,然后使用這個信息來決定是否允許該操作繼續(xù)進(jìn)行,或簡單地立即返回一個異常。

代理可以被實(shí)現(xiàn)為狀態(tài)機(jī)與模擬的電路斷路器的功能如下狀態(tài):

關(guān)閉:從應(yīng)用程序的請求是通過對操作進(jìn)行路由。代理保持最近的失敗次數(shù)的計(jì)數(shù),并且如果該呼叫到操作不成功,則代理遞增該計(jì)數(shù)。如果最近的失敗次數(shù)超過了一個給定時間周期內(nèi)的規(guī)定的閾值時,該代理將被置于打開狀態(tài)。在這一點(diǎn)上的代理啟動一個超時定時器,當(dāng)該定時器期滿的代理放置到半開放狀態(tài)。

注意

超時定時器的目的是為了給系統(tǒng)時間,糾正允許應(yīng)用程序嘗試再次執(zhí)行該操作之前導(dǎo)致失敗的問題。

  • 打開:從應(yīng)用程序請求立即失敗和異常返回給應(yīng)用程序。
  • 半開放:從應(yīng)用程序請求的數(shù)量有限允許通過并調(diào)用運(yùn)行。如果這些請求是成功的,則假定先前導(dǎo)致故障的故障已修復(fù)和斷路器切換到閉合狀態(tài)(故障計(jì)數(shù)器被復(fù)位)。如果任何請求失敗,斷路器假設(shè)故障仍然存在,因此恢復(fù)到打開狀態(tài),并重新啟動超時定時器,系統(tǒng)的時間再延長,從故障中恢復(fù)。

注意

半開的狀態(tài)是很有用的,以防止恢復(fù)服務(wù),從突然被淹沒的請求。作為服務(wù)恢復(fù),也可能是能夠支持請求的限制音量,直到恢復(fù)完成,但在恢復(fù)過程中,海量的工作可能會導(dǎo)致服務(wù)超時或再次失敗。

下圖展示出了用于一個可能的實(shí)現(xiàn)的電路斷路器的狀態(tài)。

圖 1 - 斷路器狀態(tài)
需要注意的是,在圖 1 中,所用的封閉狀態(tài)下的失敗計(jì)數(shù)器是基于時間的。它以定期自動復(fù)位。這有助于防止斷路器進(jìn)入打開狀態(tài),如果它經(jīng)受偶然的失敗;這使斷路器跳閘進(jìn)入打開狀態(tài)的故障閾值時,故障的指定數(shù)量的指定的時間間隔期間發(fā)生的僅達(dá)到。所使用的半開狀態(tài)下的成功計(jì)數(shù)器記錄成功嘗試調(diào)用的操作的數(shù)量。斷路器恢復(fù)到封閉狀態(tài)后的連續(xù)操作調(diào)用中指定數(shù)量的已成功。如果任何調(diào)用失敗時,斷路器立即進(jìn)入打開狀態(tài),并且成功的計(jì)數(shù)器將其進(jìn)入半開狀態(tài)下一次復(fù)位。

Note

如何將系統(tǒng)恢復(fù)從外部處理,可能通過恢復(fù)或重新啟動故障部件或修理的網(wǎng)絡(luò)連接。

執(zhí)行斷路器圖案增加了穩(wěn)定性和靈活性,以一個系統(tǒng),提供穩(wěn)定性,而系統(tǒng)從故障中恢復(fù),并盡量減少此故障的對性能的影響。它可以幫助快速地拒絕對一個操作,即很可能失敗,而不是等待操作超時(或者不返回)的請求,以保持系統(tǒng)的響應(yīng)時間。如果斷路器提高每次改變狀態(tài)的時間的事件,該信息可以被用來監(jiān)測由斷路器保護(hù)系統(tǒng)的部件的健康狀況,或以提醒管理員當(dāng)斷路器跳閘,以在打開狀態(tài)。

模式是可定制的,并且可以根據(jù)可能的故障的性質(zhì)進(jìn)行調(diào)整。例如,您可以申請?jiān)黾拥某瑫r時間為一個斷路器??梢苑胖迷诖蜷_狀態(tài)的斷路器的幾秒鐘開始,然后,如果故障一直沒有解決增加超時到幾分鐘的時間,等等。在某些情況下,而不是打開狀態(tài)返回故障并提高了異常,也可能是有用的,返回一個缺省值,該值是有意義的應(yīng)用。

問題和注意事項(xiàng)

在決定如何實(shí)現(xiàn)這個模式時,您應(yīng)考慮以下幾點(diǎn):

  • 異常處理。通過斷路器調(diào)用操作的應(yīng)用程序必須準(zhǔn)備好處理,如果該操作是不可用的,可以被拋出的異常。在這樣的異常處理將特定應(yīng)用程序的方式。例如,一個應(yīng)用程序可以暫時降低其功能,調(diào)用替換操作來嘗試執(zhí)行相同的任務(wù)或獲得相同的數(shù)據(jù),或者報(bào)告該異常給用戶,并要求他們稍后再試。
  • 例外的類型。一個請求可能失敗的原因有多種,其中有一些可能指示更嚴(yán)重的類型的失效比其他。例如,一個請求可能會失敗,因?yàn)檫h(yuǎn)程服務(wù)已經(jīng)崩潰了,可能需要幾分鐘才能恢復(fù),或失敗可能是由于該服務(wù)被暫時超載造成的超時時間。一種斷路器可能能夠檢查發(fā)生的異常的類型,并根據(jù)這些異常的性質(zhì)調(diào)整策略。例如,它可能需要的超時異常更大數(shù)目的斷路器的開狀態(tài)相比失敗次數(shù)跳閘由于服務(wù)是完全不可用。
  • 日志記錄。一個斷路器應(yīng)記錄所有失敗的請求(也可能是成功的請求),以使管理員能夠監(jiān)視它封裝了操作的健康。
  • 可恢復(fù)性。您應(yīng)該配置斷路器與之相匹配的是保護(hù)的操作可能恢復(fù)模式。例如,如果斷路器保持在打開狀態(tài)下很長一段時間,也可能產(chǎn)生異常,即使對于失敗的原因早已得到了解決。類似地,一個斷路器可以振蕩并降低應(yīng)用程序的響應(yīng)時間,如果它從打開狀態(tài)到半開狀態(tài)太快切換。
  • 測試失敗的操作。在打開狀態(tài)下,而不是使用一個計(jì)時器來確定何時切換到半開放狀態(tài)下,斷路器可代替周期性地查驗(yàn)遠(yuǎn)程服務(wù)或資源,以確定它是否已經(jīng)再次變得可用。這個平可以采取的企圖的形式援引了以前失敗的操作,也可以使用由遠(yuǎn)程服務(wù)提供的特殊操作專門用于測試服務(wù)的健康狀況,所描述的衛(wèi)生端點(diǎn)監(jiān)測圖案。
  • 手動覆蓋。在一個系統(tǒng)中,如果恢復(fù)時間為一個失敗的操作是非??勺兊?,它可能是有利的,以提供一個手動復(fù)位選項(xiàng),使管理員能夠強(qiáng)行關(guān)閉斷路器(和復(fù)位的故障計(jì)數(shù)器)。同樣,管理員可以強(qiáng)制斷路器進(jìn)入開放狀態(tài)(并重新啟動超時定時器),如果由斷路器保護(hù)動作暫時不可用。
  • 并發(fā)。相同的電路斷路器可以通過大量的應(yīng)用程序的并行實(shí)例來訪問。實(shí)施不應(yīng)該阻塞并發(fā)請求或添加過多的開銷,以每次調(diào)用操作。
  • 資源分化。使用單個斷路器時,一個類型的資源,如果??可能有多個潛在的獨(dú)立供應(yīng)商要小心。例如,在數(shù)據(jù)存儲器,其包括多個碎片,1分片可以是完全可訪問的,而另一個是經(jīng)歷一個暫時的問題。如果在這些情況下的錯誤響應(yīng)被合二為一,應(yīng)用程序可能試圖訪問一些碎片,即使發(fā)生故障的可能性高,同時獲得其他碎片,即使它是可能成功的可能被堵塞。
  • 加速斷路。有時失敗響應(yīng)可以包含足夠的信息用于斷路器的實(shí)施知道它應(yīng)當(dāng)立即跳閘并保持處于跳閘狀態(tài)的最小時間量。例如,從該過載的共享資源的錯誤響應(yīng)可以指示立即重試時不推薦使用,并且該應(yīng)用程序應(yīng)代替再次嘗試在幾分鐘時間。

Note

HTTP 協(xié)議定義的“HTTP503 服務(wù)不可用”,它可以如所請求的服務(wù)是當(dāng)前不可用的特定的 Web 服務(wù)器上的被返回的響應(yīng)。此響應(yīng)可以包括附加信息,例如延遲的預(yù)期持續(xù)時間。

  • 重播失敗的請求。在打開狀態(tài)下,而不是簡單的故障很快,斷路器也可以記錄每個請求的詳細(xì)信息,以軸頸和安排這些請求時,遠(yuǎn)程資源或服務(wù)變得可用重放。

  • 對外部服務(wù)不當(dāng)超時。電路斷路器可能無法充分保護(hù)的應(yīng)用程序,從失敗中配置有一個漫長的超時時間對外服務(wù)業(yè)務(wù)。如果超時太長,運(yùn)行一個斷路器的螺紋可能被堵塞長時間之前斷路器指示操作已失敗。在這個時候,許多其他的應(yīng)用程序?qū)嵗部梢試L試通過斷路器來調(diào)用服務(wù),并占用一個顯著的線程數(shù)之前,他們都失敗。

當(dāng)使用這個模式

使用這種模式:

  • 為了防止一個應(yīng)用程序試圖調(diào)用一個遠(yuǎn)程服務(wù)或訪問共享資源,如果??該操作是極有可能失敗。

這種模式可能不適合:

  • 對于處理中的應(yīng)用程序訪問本地專用資源,例如在存儲器內(nèi)數(shù)據(jù)結(jié)構(gòu)。在這種環(huán)境下,使用斷路器只會增加開銷到您的系統(tǒng)。
  • 作為一個替代品來處理異常在應(yīng)用程序的業(yè)務(wù)邏輯。

例子

在 Web 應(yīng)用中,幾個頁面的已填充了從外部服務(wù)中檢索數(shù)據(jù)。如果該系統(tǒng)實(shí)現(xiàn)了最小的緩存,點(diǎn)擊率最高的為每個頁面都會導(dǎo)致往返服務(wù)。從 Web 應(yīng)用程序到服務(wù)的連接可以用一個超時時間段(通常為 60 秒)進(jìn)行配置,并且如果該服務(wù)沒有在這個時間響應(yīng)在每個網(wǎng)頁的邏輯將假設(shè)該服務(wù)不可用,并且拋出異常。

但是,如果服務(wù)失敗,系統(tǒng)非常繁忙,用戶可能會被迫等待異常發(fā)生時長達(dá)60秒前。最終的資源,如內(nèi)存,連接和線程可能被耗盡,以防止其他用戶連接到系統(tǒng),即使它們沒有訪問檢索業(yè)務(wù)數(shù)據(jù)的頁面。

通過添加更多的 Web 服務(wù)器和執(zhí)行負(fù)載均衡擴(kuò)展,系統(tǒng)可能會延誤的點(diǎn)資源趨于枯竭,但它不會解決問題,因?yàn)橛脩粽埱笕匀粫磻?yīng)遲鈍,所有的 Web 服務(wù)器仍然可以最終耗盡資源。

包裹連接到服務(wù),并檢索數(shù)據(jù)中的斷路器的邏輯可以幫助緩解這個問題的影響,并且更優(yōu)雅處理服務(wù)故障。用戶請求仍然會失敗的,但它們將更加迅速地失敗,并且資源不會被阻塞。

CircuitBreaker類維護(hù)有關(guān)的對象,它實(shí)現(xiàn)下面的代碼所示ICircuitBreakerStateStore接口電路斷路器的狀態(tài)信息。

interface ICircuitBreakerStateStore  
{  
  CircuitBreakerStateEnum State { get; }  
?
  Exception LastException { get; }  
?
  DateTime LastStateChangedDateUtc { get; }  
?
  void Trip(Exception ex);  
?
  void Reset();  
?
  void HalfOpen();  
?
  bool IsClosed { get; }  
}

狀態(tài)屬性指示斷路器的當(dāng)前狀態(tài),以及由 CircuitBreakerStateEnum 枚舉所定義的將是這些值中的一個程序,HalfOpen,或者已關(guān)閉。如果電路斷路器閉合,但如果其打開或半開的 IsClosed 屬性應(yīng)該是真實(shí)的。跳閘方法切換斷路器為打開狀態(tài)的狀態(tài),并記錄該引起狀態(tài)變化的異常,與所發(fā)生的異常的日期和時間一起。該 LastException 和 LastStateChangedDateUtc 屬性返回此信息。復(fù)位方法關(guān)閉斷路器和 HalfOpen 方法將斷路器半開。

在該實(shí)例中 InMemoryCircuitBreakerStateStore 類包含 ICircuitBreakerStateStore 接口的實(shí)現(xiàn)。該 CircuitBreaker 類創(chuàng)建這個類的一個實(shí)例來保存斷路器的狀態(tài)。

在 CircuitBreaker 類的 ExecuteAction 方法包裝的操作(在 Action 委托的形式)可能會失敗。當(dāng)該方法運(yùn)行時,它首先檢查斷路器的狀態(tài)。如果它被關(guān)閉(當(dāng)?shù)?IsOpen 屬性,如果斷路器處于打開狀態(tài)或半開,返回真,是假的)的 ExecuteAction 方法試圖調(diào)用 Action 委托。如果此操作失敗,異常處理程序執(zhí)行 TrackException 方法,用于設(shè)置該電路斷路器的狀態(tài)通過調(diào)用 InMemoryCircuitBreakerStateStore 物體的行程的方法打開。下面的代碼示例強(qiáng)調(diào)了這一流程。

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   
      // re-trip 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 may 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);  
  }  
}

下面的例子顯示了執(zhí)行,如果斷路器沒有關(guān)閉的代碼(從前面的例子中省略)。它如果斷路器已經(jīng)開了一段時間長于當(dāng)?shù)?OpenToHalfOpenWaitTime 字段中 CircuitBreaker 類中指定的時間首先檢查。如果是這種情況,則 ExecuteAction 方法設(shè)置斷路器半開,然后嘗試執(zhí)行該行動代表所指定的操作。

如果操作成功,則斷路器復(fù)位到閉合狀態(tài)。如果操作失敗,則跳閘恢復(fù)到打開狀態(tài),并且在發(fā)生被更新,以使斷路器將等待進(jìn)一步期間再次嘗試執(zhí)行該操作之前的異常所需的時間。

如果斷路器至今只有開放的時間很短,小于 OpenToHalfOpenWaitTime 值時,ExecuteAction 方法簡單地拋出 CircuitBreakerOpenException 異常和返回引發(fā)的斷路器轉(zhuǎn)換到打開狀態(tài)的誤差。

此外,為了防止斷路器試圖執(zhí)行并發(fā)呼叫的操作,同時它是半開的,它使用一個鎖。兼試圖調(diào)用該操作會如果斷路器是公開進(jìn)行處理,如后所述,它會失敗并異常。

...  
  if (IsOpen)  
  {  
    // The circuit breaker is Open. Check if the Open timeout has expired.  
    // If it has, set the state to HalfOpen. Another approach may be to simply   
    // 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 simply 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 simply 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 Open state, a counter  
          // here would record the number of successful operations and return the  
          // circuit breaker to the Open state only after a specified number succeed.  
          this.stateStore.Reset();  
          return;  
        }  
        catch (Exception ex)  
        {  
          // If there is 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 has not yet expired. Throw a CircuitBreakerOpen exception to  
    // inform the caller that the caller that the call was not actually attempted,   
    // and return the most recent exception received.  
    throw new CircuitBreakerOpenException(stateStore.LastException);  
  }  
  ...

使用 CircuitBreaker 對象,以保護(hù)操作時,應(yīng)用程序創(chuàng)建的 CircuitBreaker 類的一個實(shí)例,并調(diào)用 ExecuteAction 方法,指定的操作被作為參數(shù)來執(zhí)行。該應(yīng)用程序應(yīng)該準(zhǔn)備,如果操作失敗,因?yàn)閿嗦菲魈幱诖蜷_狀態(tài),以趕上 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)  
{  
  ...  
}
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號