處理故障連接到遠程服務或資源時,可能需要耗費大量的時間。這種模式可以提高應用程序的穩(wěn)定性和靈活性。
在分布式環(huán)境中,如在云,其中,應用程序執(zhí)行訪問遠程資源和服務的操作,有可能對這些操作的失敗是由于瞬時故障,如慢的網絡連接,超時,或者被過度使用的資源或暫時不可用。這些故障一般之后的短時間內糾正自己,和一個強大的云應用應該準備使用的策略來處理它們,例如,通過重試模式進行說明。
但是,也可以是其中的故障是由于那些不容易預見的突發(fā)事件的情況下,這可能需要更長的時間來糾正。這些故障從連接的部分損失到服務的完整的故障范圍的嚴重程度。在這種情況下,它可能是毫無意義的應用,不斷重試執(zhí)行的操作是不太可能成功,而不是應用程序應該很快接受該操作已失敗,并相應地處理這個故障。
此外,如果一個服務是非常繁忙的,在系統中的一個部分出現故障可能會導致連鎖故障。例如,調用一個服務的操作可被配置成實現一個超時,如果該服務無法在這段時間內響應一個失敗消息答復。然而,這一策略可能導致許多并發(fā)請求的相同的操作,直到超時時段期滿被阻止。這些被禁止的請求可能會持有關鍵系統資源,如內存,線程,數據庫連接等。因此,這些資源可能會耗盡,從而導致該系統的其他可能無關的部件,需要使用相同的資源的失敗。在這些情況下,這將是優(yōu)選的操作立即失敗,并且只嘗試調用服務,如果它是可能成功。注意,設置一個較短的超時可能有助于解決此問題,但在超時不應該如此之短,以致操作失敗的大部分時間,即使該請求到服務最終會成功。
該斷路器圖案可以防止一個應用程序多次試圖執(zhí)行一個操作,即很可能失敗,允許它繼續(xù)而不等待故障恢復或者浪費 CPU 周期,而它確定該故障是持久的。斷路器模式也使應用程序能夠檢測故障是否已經解決。如果問題似乎已經得到糾正??,應用程序可以嘗試調用操作。
注意
斷路器圖案的目的是從該重試模式的不同。重試模式使應用程序可以重試操作以期望它會成功。斷路器圖案防止應用程序執(zhí)行一個操作,即很可能失敗。一個應用程序可以通過使用重試模式,通過一個斷路器調用操作結合這兩種模式。然而,在重試邏輯應該是由斷路器返回任何異常敏感和放棄重試次數,如果斷路器指示故障不是瞬時的。
斷路器充當可能失敗操作的代理。代理應監(jiān)測最近發(fā)生的故障數量,然后使用這個信息來決定是否允許該操作繼續(xù)進行,或簡單地立即返回一個異常。
代理可以被實現為狀態(tài)機與模擬的電路斷路器的功能如下狀態(tài):
關閉:從應用程序的請求是通過對操作進行路由。代理保持最近的失敗次數的計數,并且如果該呼叫到操作不成功,則代理遞增該計數。如果最近的失敗次數超過了一個給定時間周期內的規(guī)定的閾值時,該代理將被置于打開狀態(tài)。在這一點上的代理啟動一個超時定時器,當該定時器期滿的代理放置到半開放狀態(tài)。
注意
超時定時器的目的是為了給系統時間,糾正允許應用程序嘗試再次執(zhí)行該操作之前導致失敗的問題。
注意
半開的狀態(tài)是很有用的,以防止恢復服務,從突然被淹沒的請求。作為服務恢復,也可能是能夠支持請求的限制音量,直到恢復完成,但在恢復過程中,海量的工作可能會導致服務超時或再次失敗。
下圖展示出了用于一個可能的實現的電路斷路器的狀態(tài)。
圖 1 - 斷路器狀態(tài)
需要注意的是,在圖 1 中,所用的封閉狀態(tài)下的失敗計數器是基于時間的。它以定期自動復位。這有助于防止斷路器進入打開狀態(tài),如果它經受偶然的失敗;這使斷路器跳閘進入打開狀態(tài)的故障閾值時,故障的指定數量的指定的時間間隔期間發(fā)生的僅達到。所使用的半開狀態(tài)下的成功計數器記錄成功嘗試調用的操作的數量。斷路器恢復到封閉狀態(tài)后的連續(xù)操作調用中指定數量的已成功。如果任何調用失敗時,斷路器立即進入打開狀態(tài),并且成功的計數器將其進入半開狀態(tài)下一次復位。
如何將系統恢復從外部處理,可能通過恢復或重新啟動故障部件或修理的網絡連接。
執(zhí)行斷路器圖案增加了穩(wěn)定性和靈活性,以一個系統,提供穩(wěn)定性,而系統從故障中恢復,并盡量減少此故障的對性能的影響。它可以幫助快速地拒絕對一個操作,即很可能失敗,而不是等待操作超時(或者不返回)的請求,以保持系統的響應時間。如果斷路器提高每次改變狀態(tài)的時間的事件,該信息可以被用來監(jiān)測由斷路器保護系統的部件的健康狀況,或以提醒管理員當斷路器跳閘,以在打開狀態(tài)。
模式是可定制的,并且可以根據可能的故障的性質進行調整。例如,您可以申請增加的超時時間為一個斷路器??梢苑胖迷诖蜷_狀態(tài)的斷路器的幾秒鐘開始,然后,如果故障一直沒有解決增加超時到幾分鐘的時間,等等。在某些情況下,而不是打開狀態(tài)返回故障并提高了異常,也可能是有用的,返回一個缺省值,該值是有意義的應用。
在決定如何實現這個模式時,您應考慮以下幾點:
HTTP 協議定義的“HTTP503 服務不可用”,它可以如所請求的服務是當前不可用的特定的 Web 服務器上的被返回的響應。此響應可以包括附加信息,例如延遲的預期持續(xù)時間。
重播失敗的請求。在打開狀態(tài)下,而不是簡單的故障很快,斷路器也可以記錄每個請求的詳細信息,以軸頸和安排這些請求時,遠程資源或服務變得可用重放。
對外部服務不當超時。電路斷路器可能無法充分保護的應用程序,從失敗中配置有一個漫長的超時時間對外服務業(yè)務。如果超時太長,運行一個斷路器的螺紋可能被堵塞長時間之前斷路器指示操作已失敗。在這個時候,許多其他的應用程序實例也可以嘗試通過斷路器來調用服務,并占用一個顯著的線程數之前,他們都失敗。
使用這種模式:
這種模式可能不適合:
在 Web 應用中,幾個頁面的已填充了從外部服務中檢索數據。如果該系統實現了最小的緩存,點擊率最高的為每個頁面都會導致往返服務。從 Web 應用程序到服務的連接可以用一個超時時間段(通常為 60 秒)進行配置,并且如果該服務沒有在這個時間響應在每個網頁的邏輯將假設該服務不可用,并且拋出異常。
但是,如果服務失敗,系統非常繁忙,用戶可能會被迫等待異常發(fā)生時長達60秒前。最終的資源,如內存,連接和線程可能被耗盡,以防止其他用戶連接到系統,即使它們沒有訪問檢索業(yè)務數據的頁面。
通過添加更多的 Web 服務器和執(zhí)行負載均衡擴展,系統可能會延誤的點資源趨于枯竭,但它不會解決問題,因為用戶請求仍然會反應遲鈍,所有的 Web 服務器仍然可以最終耗盡資源。
包裹連接到服務,并檢索數據中的斷路器的邏輯可以幫助緩解這個問題的影響,并且更優(yōu)雅處理服務故障。用戶請求仍然會失敗的,但它們將更加迅速地失敗,并且資源不會被阻塞。
該CircuitBreaker
類維護有關的對象,它實現下面的代碼所示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)屬性指示斷路器的當前狀態(tài),以及由 CircuitBreakerStateEnum 枚舉所定義的將是這些值中的一個程序,HalfOpen,或者已關閉。如果電路斷路器閉合,但如果其打開或半開的 IsClosed 屬性應該是真實的。跳閘方法切換斷路器為打開狀態(tài)的狀態(tài),并記錄該引起狀態(tài)變化的異常,與所發(fā)生的異常的日期和時間一起。該 LastException 和 LastStateChangedDateUtc 屬性返回此信息。復位方法關閉斷路器和 HalfOpen 方法將斷路器半開。
在該實例中 InMemoryCircuitBreakerStateStore 類包含 ICircuitBreakerStateStore 接口的實現。該 CircuitBreaker 類創(chuàng)建這個類的一個實例來保存斷路器的狀態(tài)。
在 CircuitBreaker 類的 ExecuteAction 方法包裝的操作(在 Action 委托的形式)可能會失敗。當該方法運行時,它首先檢查斷路器的狀態(tài)。如果它被關閉(當地 IsOpen 屬性,如果斷路器處于打開狀態(tài)或半開,返回真,是假的)的 ExecuteAction 方法試圖調用 Action 委托。如果此操作失敗,異常處理程序執(zhí)行 TrackException 方法,用于設置該電路斷路器的狀態(tài)通過調用 InMemoryCircuitBreakerStateStore 物體的行程的方法打開。下面的代碼示例強調了這一流程。
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í)行,如果斷路器沒有關閉的代碼(從前面的例子中省略)。它如果斷路器已經開了一段時間長于當地 OpenToHalfOpenWaitTime 字段中 CircuitBreaker 類中指定的時間首先檢查。如果是這種情況,則 ExecuteAction 方法設置斷路器半開,然后嘗試執(zhí)行該行動代表所指定的操作。
如果操作成功,則斷路器復位到閉合狀態(tài)。如果操作失敗,則跳閘恢復到打開狀態(tài),并且在發(fā)生被更新,以使斷路器將等待進一步期間再次嘗試執(zhí)行該操作之前的異常所需的時間。
如果斷路器至今只有開放的時間很短,小于 OpenToHalfOpenWaitTime 值時,ExecuteAction 方法簡單地拋出 CircuitBreakerOpenException 異常和返回引發(fā)的斷路器轉換到打開狀態(tài)的誤差。
此外,為了防止斷路器試圖執(zhí)行并發(fā)呼叫的操作,同時它是半開的,它使用一個鎖。兼試圖調用該操作會如果斷路器是公開進行處理,如后所述,它會失敗并異常。
...
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 對象,以保護操作時,應用程序創(chuàng)建的 CircuitBreaker 類的一個實例,并調用 ExecuteAction 方法,指定的操作被作為參數來執(zhí)行。該應用程序應該準備,如果操作失敗,因為斷路器處于打開狀態(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)
{
...
}
更多建議: