Polly是一个.NET弹性和瞬时故障处理库,它允许开发人员以流畅且线程安全的方式表达策略,如失败重试、服务熔断、超时处理、舱壁隔离、缓存策略和失败降级。
在分布式的系统中,为了保障服务的高性能、高可用,我们需要做流量控制,如负载均衡、服务路由、熔断、降级、限流和流量相关调度。
自己的服务所依赖的某一个上游的底层服务挂了,当出现大量请求时,所有连接超时,Socket连接无法及时释放,导致内存溢出,出现OOM。
举例两个真实场景:
- 比如由于底层的权限服务挂了,导致所有业务线依赖于这个权限服务都出现不可用的情况。
- 最近我们组在使用Redis时,使用后未及时释放,由于连接池最大只有128,这时某同学封装的阻塞式Redis锁,不断循环去竞争锁,导致Redis服务一直处于不可用的状态。
背景:目前我们依赖一套标准的Restful 风格API,通过Http请求,我们来封装一套瞬时处理的策略组件。Http的请求库我用的是RestSharp。
重试策略
在Http请求中,比如网络的瞬时故障,或者对方服务由于发版上线导致的某一时刻的无法建立连接;另一种情况为,由于调用频率触发了对象服务的限频,需要我们Sleep(1s),这种小概率场景我们可以通过重试,来提高服务请求的可用性。 对于标准的Restful API我们会通过HttpStatus的状态码来判断异常处理重试策略。
var retryPolicy = Policy<IRestResponse>.HandleResult(restResponse =>
{
//http 网络瞬时故障
// ReSharper disable once ConvertIfStatementToReturnStatement
if (response.ResponseStatus == ResponseStatus.Error
|| response.ResponseStatus == ResponseStatus.TimedOut
|| response.StatusCode >= HttpStatusCode.InternalServerError;)
{
return true;
}
return false;
})
.WaitAndRetry(3, i => TimeSpan.FromSeconds(1));
return retryPolicy;
熔断、舱壁隔离策略
当某个上游无服务发生宕机或不可用时,为了避免我们自己的业务服务被拖垮,我们可以采用熔断策略。如实例代码:
- 第一个参数:failureThreshold = 0.8 表示故障阈值,比如100个请求,80个失败了,就会触发断路器。
- 第二个参数:samplingDuration ,表示根据30s内的数据样本做判断。可能某上游服务,由于网络瞬时故障5s内不可用,第6s网络恢复,如果我们设置的时间过短,立马触发断路器,而从熔断恢复正常,可能需要10s,这样设计就不太合理了。所以建议这个参数,既不要设置的太小,也不要设置的太大。
- 第三个参数: minimumThroughput = 50,触发熔断最小的请求次数。必须大于50次。 当前的熔断策略就是,在30s内,请求大于50次,失败的请求超过80%,则会触发熔断机制。
/// <summary>
/// 熔断策略
/// 会抛出:BrokenCircuitException
/// </summary>
/// <param name="onBreakAction"></param>
/// <param name="onResetAction"></param>
/// <param name="onHalfOpenAction"></param>
/// <param name="failureThreshold"></param>
/// <returns></returns>
public static CircuitBreakerPolicy<IRestResponse> CircuitBreakerPolicy(Action<DelegateResult<IRestResponse>, TimeSpan> onBreakAction, Action onResetAction, Action onHalfOpenAction,
double failureThreshold = 0.8)
{
var bulkhead = Policy<IRestResponse>.HandleResult(InvalidStatus)
.AdvancedCircuitBreaker(failureThreshold,
samplingDuration:TimeSpan.FromSeconds(30),
50,
TimeSpan.FromSeconds(30),
onBreakAction,
onResetAction,
onHalfOpenAction);
return bulkhead;
}
降级策略
当某个服务请求触发了熔断策略后,为了保障服务的可用性,我们一般会降级处理,比如零时读取缓存数据,或直接提示服务异常,及时返回异常信息。
- 参考polly的熔断策略,熔断后会触发BrokenCircuitException异常。我们针对这个异常做降级处理,无论是缓存还是直接异常返回,保证服务的快速响应,不会由于大量请求响应超时而宕机。
public static PolicyWrap<IRestResponse> FailBackPolicy(Action<DelegateResult<IRestResponse>, TimeSpan> onBreakAction, Action onResetAction, Action onHalfOpenAction)
{
//熔断后的降级策略,直接返回错误信息
var failBack = Policy<IRestResponse>
.Handle<BrokenCircuitException>().Fallback(() => new RestResponse()
{
ResponseStatus = ResponseStatus.Error,//需要注意,重试策略如果判断如果和该条件一致,则会导致,熔断降级后继续发生重试。
StatusCode = HttpStatusCode.ExpectationFailed,
ErrorMessage = "当前服务接口熔断不可用,已降级。"
});
return Policy.Wrap(failBack, BulkheadPolicy(onBreakAction, onResetAction, onHalfOpenAction));
}
组合所有策略
通过Castle或Autofac我们可以轻易的实现动态代理。以切面的形式实现控制层面的逻辑,不影响标准的业务。 策略组合的顺序是很重要的,最外层的最先触发。
需要注意的是,如果服务已经触发熔断并降级后,我们需要注意降级策略的返回值和重试的前置条件,如果降级返回的Response.HttpStatusCode == 500,就会继续触发重试的逻辑。导致熔断恢复周期可能会变长。
public class RestClientPolicyInterceptor : IInterceptor
{
public RestClientPolicyInterceptor()
{
}
/// <summary>
/// 北森RestClient 代理封装
/// </summary>
/// <param name="invocation"></param>
public void Intercept(IInvocation invocation)
{
//1.判断是否熔断,实现舱壁隔离
//2.判断网络瞬时故障的重试策略。
//3.Token失效的重试策略
string methodName = invocation.Method.Name;
PolicyWrap<IRestResponse> policy = Policy.Wrap(RequestExceptionRetryPolicy(),
BulkheadFailBackPolicy((result, span) => { Logger.Error($"{methodName}:触发熔断。"); },
() => { Logger.Error($"{methodName}:熔断, Rest。"); },
() => { Logger.Error($"{methodName}:熔断, Half Open。"); }));
policy.Execute(() =>
{
invocation.Proceed();
return invocation.ReturnValue as IRestResponse;
});
invocation.ReturnValue = restResponse;
}