前言
本節我們來介紹一款強大的庫Polly,Polly是一種.NET彈性和瞬态故障處理庫,允許我們以非常順暢和線程安全的方式來執諸如行重試,斷路,逾時,故障恢複等政策。 Polly針對對.NET 4.0,.NET 4.5和.NET Standard 1.1以及.NET Core實作,該項目作者現已成為.NET基金會一員,項目一直在不停疊代和更新,項目位址【https://github.com/App-vNext/Polly】,你值得擁有。接下來我們以.NET Framework 4.5來示範它的強大功能。
Introduce Polly
首先我們得下載下傳Polly包,最新版本為5.3.1,如下:

該庫實作了七種恢複政策,下面我一一為您來介紹。
重試政策(Retry)
重試政策針對的前置條件是短暫的故障延遲且在短暫的延遲之後能夠自我糾正。允許我們做的是能夠自動配置重試機制。
斷路器(Circuit-breaker)
斷路器政策針對的前置條件是當系統繁忙時,快速響應失敗總比讓使用者一直等待更好。保護系統故障免受過載,Polly可以幫其恢複。
逾時(Timeout)
逾時政策針對的前置條件是超過一定的等待時間,想要得到成功的結果是不可能的,保證調用者不必等待逾時。
隔闆隔離(Bulkhead Isolation)
隔闆隔離針對的前置條件是當程序出現故障時,多個失敗一直在主機中對資源(例如線程/ CPU)一直占用。下遊系統故障也可能導緻上遊失敗。這兩個風險都将造成嚴重的後果。都說一粒老鼠子屎攪渾一鍋粥,而Polly則将受管制的操作限制在固定的資源池中,免其他資源受其影響。
緩存(Cache)
緩存政策針對的前置條件是資料不會很頻繁的進行更新,為了避免系統過載,首次加載資料時将響應資料進行緩存,如果緩存中存在則直接從緩存中讀取。
回退(Fallback)
操作仍然會失敗,也就是說當發生這樣的事情時我們打算做什麼。也就是說定義失敗傳回操作。
政策包裝(PolicyWrap)
政策包裝針對的前置條件是不同的故障需要不同的政策,也就意味着彈性靈活使用組合。
幾種政策使用
一旦從事IT就得警惕異常并友好擁抱異常而非不聞不問,這個時候我們利用try{}catch{}來處理。
try
{
var a = 0;
var b = 1 / a;
}
catch (DivideByZeroException ex)
{
throw ex;
}
若我們想重試三次,此時我們隻能進行循環三次操作。我們隻能簡單進行處理,自從有了Polly,什麼重試機制,逾時都不在話下,下面我們來簡短介紹各種政策。Polly預設處理政策需要指定抛出的具體異常或者執行抛出異常傳回的結果。處理單個類型異常如下:
Policy
.Handle<DivideByZeroException>()
上述異常指嘗試除以0,下面我們示範下具體使用,我們嘗試除以0并用Polly指定該異常并重試三次。
static int Compute()
{
var a = 0;
return 1 / a;
}
try
{
var retryTwoTimesPolicy =
Policy
.Handle<DivideByZeroException>()
.Retry(3, (ex, count) =>
{
Console.WriteLine("執行失敗! 重試次數 {0}", count);
Console.WriteLine("異常來自 {0}", ex.GetType().Name);
});
retryTwoTimesPolicy.Execute(() =>
{
Compute();
});
}
catch (DivideByZeroException e)
{
Console.WriteLine($"Excuted Failed,Message: ({e.Message})");
}
如果我們想指定處理多個異常類型通過OR即可。
Policy
.Handle<DivideByZeroException>()
.Or<ArgumentException>()
當然還有更加強大的功能,比如在微信支付時,微信回調我們的應用程式時,此時若失敗,想必微信那邊也會做重試機制,例如隔一段時間重試調用一次,重複調用幾次後仍失敗則不再回調。我們利用Polly則可以示範等待重試機制。
/// <summary>
/// 抛出異常
/// </summary>
static void ZeroExcepcion()
{
throw new DivideByZeroException();
}
/// <summary>
/// 異常資訊
/// </summary>
/// <param name="e"></param>
/// <param name="tiempo"></param>
/// <param name="intento"></param>
/// <param name="contexto"></param>
static void ReportaError(Exception e, TimeSpan tiempo, int intento, Context contexto)
{
Console.WriteLine($"異常: {intento:00} (調用秒數: {tiempo.Seconds} 秒)\t執行時間: {DateTime.Now}");
}
try
{
var politicaWaitAndRetry = Policy
.Handle<DivideByZeroException>()
.WaitAndRetry(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(3),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(7)
}, ReportaError);
politicaWaitAndRetry.Execute(() =>
{
ZeroExcepcion();
});
}
catch (Exception e)
{
Console.WriteLine($"Executed Failed,Message:({e.Message})");
}
我們講完預設政策和重試政策,再來看看回報政策,翻譯的更通俗一點則是執行失敗後傳回的結果,此時要為Polly指定傳回類型,然後指定異常,最後調用Fallback方法。
static string ThrowException()
{
throw new Exception();
}
var fallBackPolicy =
Policy<string>
.Handle<Exception>()
.Fallback("執行失敗,傳回Fallback");
var fallBack = fallBackPolicy.Execute(() =>
{
return ThrowException();
});
Console.WriteLine(fallBack);
包裹政策說到底就是混合多種政策,并執行。
var fallBackPolicy =
Policy<string>
.Handle<Exception>()
.Fallback("執行失敗,傳回Fallback");
var fallBack = fallBackPolicy.Execute(() =>
{
return ThrowException();
});
Console.WriteLine(fallBack);
var politicaWaitAndRetry =
Policy<string>
.Handle<Exception>()
.Retry(3, (ex, count) =>
{
Console.WriteLine("執行失敗! 重試次數 {0}", count);
Console.WriteLine("異常來自 {0}", ex.GetType().Name);
});
var mixedPolicy = Policy.Wrap(fallBackPolicy, politicaWaitAndRetry);
var mixedResult = mixedPolicy.Execute(ThrowException);
Console.WriteLine($"執行結果: {mixedResult}");
至此關于Polly的基本介紹就已結束,該庫還是非常強大,更多特性請參考上述github例子,接下來我們來看看兩種具體場景。
ASP.NET Web APi使用Polly重試機制
在Polly v4.30中以上可以利用HandleResult指定傳回結果,如下:
Policy
.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.NotFound)
基于此我們完全可以利用執行Web APi中的響應政策,如下:
public readonly RetryPolicy<HttpResponseMessage> _httpRequestPolicy;
拿到響應中狀态碼,若為500則重試三次。
_httpRequestPolicy = Policy.HandleResult<HttpResponseMessage>(
r => r.StatusCode == HttpStatusCode.InternalServerError)
.WaitAndRetryAsync(3,
retryAttempt => TimeSpan.FromSeconds(retryAttempt));
上述擷取請求響應政策在構造函數中擷取。
public class PollyController : ApiController
{
public readonly RetryPolicy<HttpResponseMessage> _httpRequestPolicy;
public PollyController()
{
_httpRequestPolicy = Policy.HandleResult<HttpResponseMessage>(
r => r.StatusCode == HttpStatusCode.InternalServerError)
.WaitAndRetryAsync(3,
retryAttempt => TimeSpan.FromSeconds(retryAttempt));
}
}
此時調用接口時執行政策的Execute或者ExecuteAsync方法即可。
public async Task<IHttpActionResult> Get()
{
var httpClient = new HttpClient();
string requestEndpoint = "http://localhost:4096";
HttpResponseMessage httpResponse = await _httpRequestPolicy.ExecuteAsync(() => httpClient.GetAsync(requestEndpoint));
IEnumerable<string> numbers = await httpResponse.Content.ReadAsAsync<IEnumerable<string>>();
return Ok(numbers);
}
你以為僅限于在Web APi中使用嗎?在其他架構中也可以使用,例如EntityFramework 6.x中,在EntityFramework 6+上出現了執行政策,也就是執行重試機制,這個時候我們依然可以借助Polly輪子來實作。
EntityFramework 6.x使用Polly重試機制
在EntityFramework 6.x中有如下執行政策接口,看起來是不是和Polly中的Execute方法是不是很類似。
//
// 摘要:
// A strategy that is used to execute a command or query against the database, possibly
// with logic to retry when a failure occurs.
public interface IDbExecutionStrategy
{
//
// 摘要:
// Indicates whether this System.Data.Entity.Infrastructure.IDbExecutionStrategy
// might retry the execution after a failure.
bool RetriesOnFailure { get; }
//
// 摘要:
// Executes the specified operation.
//
// 參數:
// operation:
// A delegate representing an executable operation that doesn't return any results.
void Execute(Action operation);
//
// 摘要:
// Executes the specified operation and returns the result.
//
// 參數:
// operation:
// A delegate representing an executable operation that returns the result of type
// TResult.
//
// 類型參數:
// TResult:
// The return type of operation.
//
// 傳回結果:
// The result from the operation.
TResult Execute<TResult>(Func<TResult> operation);
//
// 摘要:
// Executes the specified asynchronous operation.
//
// 參數:
// operation:
// A function that returns a started task.
//
// cancellationToken:
// A cancellation token used to cancel the retry operation, but not operations that
// are already in flight or that already completed successfully.
//
// 傳回結果:
// A task that will run to completion if the original task completes successfully
// (either the first time or after retrying transient failures). If the task fails
// with a non-transient error or the retry limit is reached, the returned task will
// become faulted and the exception must be observed.
Task ExecuteAsync(Func<Task> operation, CancellationToken cancellationToken);
//
// 摘要:
// Executes the specified asynchronous operation and returns the result.
//
// 參數:
// operation:
// A function that returns a started task of type TResult.
//
// cancellationToken:
// A cancellation token used to cancel the retry operation, but not operations that
// are already in flight or that already completed successfully.
//
// 類型參數:
// TResult:
// The result type of the System.Threading.Tasks.Task`1 returned by operation.
//
// 傳回結果:
// A task that will run to completion if the original task completes successfully
// (either the first time or after retrying transient failures). If the task fails
// with a non-transient error or the retry limit is reached, the returned task will
// become faulted and the exception must be observed.
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> operation, CancellationToken cancellationToken);
}
EntityFramework 6.x中的執行政策說到底就是資料庫連接配接問題即彈性連接配接,若考慮到資料庫過渡負載問題,此時應用程式和資料庫之間存在網絡問題的話。可能資料庫連接配接在幾秒内才傳回,此時也沒有什麼很大的問題,我們完全可以再嘗試一次,此時或許過了連接配接頻繁期,保證連接配接立馬恢複。如果資料庫連接配接一會恢複不了呢?或許是五分鐘,又或者是半個小時。如果我們隻是一味盲目的進行重試,這顯然不可取。如果我們的應用程式連接配接逾時時間超過了20秒,若我們選擇繼續連接配接到資料庫,我們将很快用完我們應用程式池中的工作線程。一直等待資料庫的響應。此時網站将完全無響應,同時會給使用者頁面無響應的友好提醒。這是Polly庫中描述斷路器的很好例子,換句話說如果我們捕獲了m個數量的SqlExceptions,假設資料庫有其他問題存在,導緻我們不能在n秒内再嘗試連接配接資料庫。此時在資料庫連接配接上存在一個問題,那就是阻塞了我們的應用程式工作線程被挂起,我們試圖連接配接資料庫,我們假設不可用的話,但是我們要打破這種不可用,那就用Polly吧。
我們看到上述EntityFramework 6.x實作了IDbExecutionStrategy接口,但沒有實作如Polly中的斷路器模式,EntityFramework 6.x中的執行政策隻是重試機制而已。 比如SqlAzureExecutionStrategy将在指定的時間段内重試指定的次數,直到一段時間段過去,重試指數過後,接着就是失敗。 同時所有後續調用将執行相同操作,重試并失敗。 這是調用資料庫時最好的政策嗎? 不敢肯定,或許Polly中的斷路器模式值得我們借鑒。我們自己來實作上述執行政策接口。
public class CirtuitBreakerExecutionStrategy : IDbExecutionStrategy
{
private Policy _policy;
public CirtuitBreakerExecutionStrategy(Policy policy)
{
_policy = policy;
}
public void Execute(Action operation)
{
_policy.Execute(() =>
{
operation.Invoke();
});
}
public TResult Execute<TResult>(Func<TResult> operation)
{
return _policy.Execute(() =>
{
return operation.Invoke();
});
}
public async Task ExecuteAsync(Func<Task> operation, CancellationToken cancellationToken)
{
await _policy.ExecuteAsync(() =>
{
return operation.Invoke();
});
}
public async Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> operation, CancellationToken cancellationToken)
{
return await _policy.ExecuteAsync(() =>
{
return operation.Invoke();
});
}
public bool RetriesOnFailure { get { return true; } }
}
接下來在基于代碼配置檔案中設定我們上述自定義實作的斷路器模式。
public class EFConfiguration : DbConfiguration
{
public Policy _policy;
public EFConfiguration()
{
_policy = Policy.Handle<Exception>().CircuitBreaker(3, TimeSpan.FromSeconds(60));
SetExecutionStrategy("System.Data.SqlClient", () => new CirtuitBreakerExecutionStrategy(_policy));
}
}
上述自定義實作執行政策不保證一定有用或許也是一種解決方案呢。
總結
本節我們介紹了強大的Polly庫和其對應使用的兩種實際場景,有此輪子我們何不用起,将其進行封裝可以用于一切重試、緩存、異常等處理。
我的部落格即将入駐“雲栖社群”,誠邀技術同仁一同入駐。
你所看到的并非事物本身,而是經過诠釋後所賦予的意義