在Polly中,有一个重要的概念:Policy,策略有“故障定义”和“故障恢复”两部分组成。故障是指异常、非预期的返回值等情况,而动作则包括重试(Retry)、熔断/断路器(Circuit-Breaker)、Fallback(降级)等。
Polly
一、故障定义
1、指定希望策略处理的异常/错误
//单一异常类型
Policy
.Handle<HttpRequestException>();
//带条件的单一异常类型(条件过滤)
Policy
.Handle<SqlException>(ex => ex.Number == 1205);
//多个异常类型
Policy
.Handle<HttpRequestException>()
.Or<OperationCanceledException>();
//带有条件的多个异常类型
Policy
.Handle<SqlException>(ex => ex.Number == 1205)
.Or<ArgumentException>(ex => ex.ParamName == "example");
2、指定要处理的返回结果
//处理带条件的返回值
Policy
.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.NotFound);//当返回值为HttpResponseMessage,并且其StatusCode为NotFound时,才对其进行处理。
//处理多个返回值
Policy
.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.InternalServerError)
.OrResult(r => r.StatusCode == HttpStatusCode.BadGateway);
//处理原始返回值(隐含使用.Equals())
Policy
.HandleResult<HttpStatusCode>(HttpStatusCode.InternalServerError)
.OrResult(HttpStatusCode.BadGateway);
二、故障恢复
1、重试(Retry)(响应策略)
- 重试
//重试一次
Policy
.Handle<HttpRequestException>()
.Retry();
//多次重试
Policy
.Handle<HttpRequestException>()
.Retry(3);
//重试多次,在每次重试时调用一个操作
//使用当前异常和重试计数
Policy
.Handle<HttpRequestException>()
.Retry(3, onRetry: (exception, retryCount) =>
{
//在每次重试(如日志记录)之前添加要执行的逻辑
});
//重试多次,在每次重试时调用一个操作
//对于当前的异常,重试计数和上下文
//提供执行()
Policy
.Handle<HttpRequestException>()
.Retry(3, onRetry: (exception, retryCount, context) =>
{
//在每次重试(如日志记录)之前添加要执行的逻辑
});
- 永远重试(直到成功)
//永远重试
Policy
.Handle<HttpRequestException>()
.RetryForever();
//永远重试,在每次重试时调用一个动作
//当前异常
Policy
.Handle<HttpRequestException>()
.RetryForever(onRetry: exception =>
{
//在每次重试(如日志记录)之前添加要执行的逻辑
});
//永远重试,在每次重试时调用一个动作
//提供给Execute()的当前异常和上下文
Policy
.Handle<HttpRequestException>()
.RetryForever(onRetry: (exception, Context) =>
{
//在每次重试(如日志记录)之前添加要执行的逻辑
});
- 等待并重试
//重试,在每次重试之间等待指定的时间。
//(等待是用来抓住失败,然后再进行下一次尝试的。)
Policy
.Handle<HttpRequestException>()
.WaitAndRetry(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(3)
});
//重试,在每次重试之间等待指定的时间,
//使用当前异常在每次重试时调用一个操作
// 和持续时间
Policy
.Handle<HttpRequestException>()
.WaitAndRetry(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(3)
}, (exception, timeSpan) => {
//在每次重试(如日志记录)之前添加要执行的逻辑
});
//重试,在每次重试之间等待指定的时间,
//使用当前异常在每次重试时调用一个操作,
//为Execute()提供的持续时间和上下文
Policy
.Handle<HttpRequestException>()
.WaitAndRetry(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(3)
}, (exception, timeSpan, context) => {
//在每次重试(如日志记录)之前添加要执行的逻辑
});
//重试,在每次重试之间等待指定的时间,
//使用当前异常在每次重试时调用一个操作,
//持续时间、重试计数和提供给Execute()的上下文
Policy
.Handle<HttpRequestException>()
.WaitAndRetry(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(3)
}, (exception, timeSpan, retryCount, context) => {
//在每次重试(如日志记录)之前添加要执行的逻辑
});
//使用一个函数来重试指定的次数
//计算重试之间等待的时间
//当前的重试尝试(允许指数回退)
//这种情况要等一等
// 2 ^ 1 = 2 seconds then
// 2 ^ 2 = 4 seconds then
// 2 ^ 3 = 8 seconds then
// 2 ^ 4 = 16 seconds then
// 2 ^ 5 = 32 seconds
Policy
.Handle<HttpRequestException>()
.WaitAndRetry(5, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
);
//使用一个函数来重试指定的次数
//计算重试之间等待的时间
//当前的重试尝试,在每次重试时调用一个操作
//提供当前异常、持续时间和上下文
// 执行()
Policy
.Handle<HttpRequestException>()
.WaitAndRetry(
5,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
(exception, timeSpan, context) => {
//在每次重试(如日志记录)之前添加要执行的逻辑
}
);
//使用一个函数来重试指定的次数
//计算重试之间等待的时间
//当前的重试尝试,在每次重试时调用一个操作
//使用当前异常、持续时间、重试计数和上下文
//提供执行()
Policy
.Handle<HttpRequestException>()
.WaitAndRetry(
5,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
(exception, timeSpan, retryCount, context) => {
//在每次重试(如日志记录)之前添加要执行的逻辑
}
);
- 永远等待并重试(直到成功)
// 永远等待并重试
Policy
.Handle<HttpRequestException>()
.WaitAndRetryForever(retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
);
//永远等待并重试,在每次重试时调用一个动作
//当前异常和等待时间
Policy
.Handle<HttpRequestException>()
.WaitAndRetryForever(
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
(exception, timespan) =>
{
//在每次重试(如日志记录)之前添加要执行的逻辑
});
//永远等待并重试,在每次重试时调用一个动作
//当前异常、等待时间和提供给Execute()的上下文
Policy
.Handle<HttpRequestException>()
.WaitAndRetryForever(
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
(exception, timespan, context) =>
{
//在每次重试(如日志记录)之前添加要执行的逻辑
});
2、熔断/断路器(Circuit Breaker)(响应策略)
//在指定数目的连续异常后断开电路
//保持电路在规定的时间内断开。
Policy
.Handle<HttpRequestException>()
.CircuitBreaker(2, TimeSpan.FromMinutes(1));
//在指定数目的连续异常后断开电路
//保持电路在规定的时间内断开,
//调用改变电路状态的动作。
Action<Exception, TimeSpan> onBreak = (exception, timespan) => { };
Action onReset = () => { };
CircuitBreakerPolicy breaker = Policy
.Handle<HttpRequestException>()
.CircuitBreaker(2, TimeSpan.FromMinutes(1), onBreak, onReset);
//在指定数目的连续异常后断开电路
//保持电路在规定的时间内断开,
//要求改变电路状态,
//传递一个提供给Execute()的上下文。
Action<Exception, TimeSpan, Context> onBreak1 = (exception, timespan, context) => { };
Action<Context> onReset1 = context => { };
CircuitBreakerPolicy breaker1 = Policy
.Handle<HttpRequestException>()
.CircuitBreaker(2, TimeSpan.FromMinutes(1), onBreak1, onReset1);
// 监控电路状态,例如健康报告。
CircuitState state = breaker.CircuitState;
//手动开启(并保持开启状态)断路器-例如手动隔离下游服务。
breaker.Isolate();
//将断路器复位至关闭状态,重新开始接受动作。
breaker.Reset();
先进的断路器
// 如果在采样期间内,
//导致处理异常的操作超过故障阈值的比例,
//还规定了动作的次数通过电路的周期
//至少是最小吞吐量。
Policy
.Handle<HttpRequestException>()
.AdvancedCircuitBreaker(
failureThreshold: 0.5, // 分段上> = 50%的动作导致处理的异常...
samplingDuration: TimeSpan.FromSeconds(10), // ...在任何10秒的时间段内
minimumThroughput: 8, // ...在10秒的时间段内提供了至少8个动作。
durationOfBreak: TimeSpan.FromSeconds(30) // 中断30秒。
);//接受状态更改委托的配置重载//可按上述断路器的要求提供。//电路状态监测和手动控制//可按上述断路器的要求提供。
3、降级/回退(Fallback)(响应策略)
// 如果执行出错,提供一个替代值。
Policy
.Handle<HttpRequestException>()
.Fallback(() =>
{
//代替方法
}, (ex) =>
{
//拦截方法
}).Execute(() =>
{
});
4、超时(Timeout)(主动策略)
///乐观的超时
//如果执行的委托尚未完成,则超时并在30秒后返回给调用者。
Policy
.Timeout(30);
///悲观的超时
// 超时30秒后,如果执行的委托尚未完成。即使执行的代码没有取消机制,也会强制执行此超时。
Policy
.Timeout(30, TimeoutStrategy.Pessimistic);
5、策略包装(PolicyWrap)
// 定义一个组合策略,由以前定义的策略构建。
var policyWrap = Policy
.Wrap(Fallback, cache, retry, breaker, timeout, bulkhead);
// (将策略包装在任何已执行的委托周围:回退最外层…舱壁内层的)
policyWrap.Execute(() =>
{
});
三、实践
1、NuGet安装:Polly
2、添加引用
using Polly;
using Polly.CircuitBreaker;
using Polly.Timeout;
1、测试访问端口为3003和3004的服务
public class Test {
public List<string> services = new List<string> { "localhost:3003", "localhost:3004" };
public int serviceIndex = 0;
public HttpClient client = new HttpClient();
public Task<string> HttpInvokeAsync()
{
if (serviceIndex >= services.Count)
{
serviceIndex = 0;
}
var service = services[serviceIndex++];
Console.WriteLine(DateTime.Now.ToString() + "开始服务:" + service);
return client.GetStringAsync("http://" + service + "/api/weatherforecast");
}
}
2、定义超时策略
var timeoutPolicy = Policy.TimeoutAsync(1, (context, timespan, task) =>
{
Console.WriteLine("超时了,抛出TimeoutRejectedException异常。");
return Task.CompletedTask;
});
3、定义重试策略
var retryPolicy = Policy.Handle<HttpRequestException>().Or<TimeoutException>().Or<TimeoutRejectedException>()
.WaitAndRetryAsync(
retryCount: 2,
sleepDurationProvider: retryAttempt =>
{
var waitSeconds = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt - 1));
Console.WriteLine(DateTime.Now.ToString() + "----------------重试策略:[" + retryAttempt + "], 等待 " + waitSeconds + "秒!");
return waitSeconds;
});
4、定义熔断策略
var circuitBreakerPolicy = Policy.Handle<HttpRequestException>().Or<TimeoutException>().Or<TimeoutRejectedException>()
.CircuitBreakerAsync(
// 熔断前允许出现几次错误
exceptionsAllowedBeforeBreaking: 2,
// 熔断时间
durationOfBreak: TimeSpan.FromSeconds(3),
// 熔断时触发
onBreak: (ex, breakDelay) =>
{
Console.WriteLine(DateTime.Now.ToString() + "----------------断路器->断路 " + breakDelay.TotalMilliseconds + "毫秒! 异常: ", ex.Message);
},
// 熔断恢复时触发
onReset: () =>
{
Console.WriteLine(DateTime.Now.ToString() + "----------------断路器->好了! 再次闭合电路.");
},
// 在熔断时间到了之后触发
onHalfOpen: () =>
{
Console.WriteLine(DateTime.Now.ToString() + "----------------断路器->半开,下一个呼叫是测试.");
}
);
5、定义降级策略
var fallbackPolicy = Policy<string>.Handle<Exception>()
.FallbackAsync(
fallbackValue: "替代数据",
onFallbackAsync: (exception, context) =>
{
Console.WriteLine("降级策略, 异常->" + exception.Exception.Message + ", 返回替代数据.");
return Task.CompletedTask;
});
6、包装策略(以上策略一起异步使用)
//循环执行50次,策略的执行是非常简单的,唯一需要注意的就是调用的顺序:如下是依次从右到左进行调用,
//首先是进行超时的判断,一旦超时就触发TimeoutRejectedException异常,
//然后就进入到了重试策略中,如果重试了一次就成功了,那就直接返回,不再触发其他策略,
//否则就进入到熔断策略中:
Task.Run(async () =>
{
for (int i = 0; i < 50; i++)
{
Console.WriteLine(DateTime.Now.ToString() + "----------------开始第[" + i + "]-----------------------------");
var res = await fallbackPolicy
.WrapAsync(Policy.WrapAsync(circuitBreakerPolicy, retryPolicy, timeoutPolicy))
.ExecuteAsync(() => new Test().HttpInvokeAsync());
Console.WriteLine(DateTime.Now.ToString() + "--------------- 开始[" + i + "]->响应" + ": Ok->" + res);
await Task.Delay(1000);
Console.WriteLine("--------------------------------------------------------------------------------------------------------------------");
}
});
7、结果展示:
参考链接:
https://www.cnblogs.com/RainingNight/p/circuitbreaker-polly-in-asp-net-core.html
官网GitHub地址:https://github.com/App-vNext/Polly#retry