开源组件-Polly

511 阅读4分钟

Polly是一个.NET弹性和瞬时故障处理库,它允许开发人员以流畅且线程安全的方式表达策略,如失败重试、服务熔断、超时处理、舱壁隔离、缓存策略和失败降级。

在分布式的系统中,为了保障服务的高性能、高可用,我们需要做流量控制,如负载均衡、服务路由、熔断、降级、限流和流量相关调度。

自己的服务所依赖的某一个上游的底层服务挂了,当出现大量请求时,所有连接超时,Socket连接无法及时释放,导致内存溢出,出现OOM。

举例两个真实场景:

  1. 比如由于底层的权限服务挂了,导致所有业务线依赖于这个权限服务都出现不可用的情况。
  2. 最近我们组在使用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;
    }