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