zoukankan      html  css  js  c++  java
  • ASP.NET Core 微服务初探[2]:熔断降级之Polly

    当我们从单体架构迁移到微服务模式时,其中一个比较大的变化就是模块(业务,服务等)间的调用方式。在以前,一个业务流程的执行在一个进程中就完成了,但是在微服务模式下可能会分散到2到10个,甚至更多的机器(微服务)上,这必然就要使用网络进行通信。而网络本身就是不可靠的,并随着每个服务都根据自身的情况进行的动态扩容,以及机器漂移等等。可以说,在微服务中,网络连接缓慢,资源繁忙,暂时不可用,服务脱机等异常情况已然变成了一种常态。因此我们必须要有一种机制来保证服务整体的稳定性,而本文要介绍的熔断降级就是一种很好的应对方案。

    服务熔断

    avalanche

    在介绍熔断之前,我们先来谈谈微服务中的雪崩效应。在微服务中,服务A调用服务B,服务B可能会调用服务C,服务C又可能调用服务D等等,这种情况非常常见。如果服务D出现不可用或响应时间过长,就会导致服务C原来越多的线程处于网络调用等待状态,进而影响到服务B,再到服务A等,最后会耗尽整个系统的资源,导致整体的崩溃,这就是微服务中的“雪崩效应”。

    而熔断机制就是应对雪崩效应的一种链路保护机制。其实,对于熔断这个词我们并不陌生,在日常生活中经常会接触到,比如:家用电力过载保护器,一旦电压过高(发生漏电等),就会立即断电,有些还会自动重试,以便在电压正常时恢复供电。再比如:股票交易中,如果股票指数过高,也会采用熔断机制,暂停股票的交易。同样,在微服务中,熔断机制就是对超时的服务进行短路,直接返回错误的响应信息,而不再浪费时间去等待不可用的服务,防止故障扩展到整个系统,并在检测到该服务正常时恢复调用链路。

    IM1L漏电断路器

    服务降级

    当我们谈到服务熔断时,经常会提到服务降级,它可以看成是熔断器的一部分,因为在熔断器框架中,通常也会包含服务降级功能。

    降级的目的是当某个服务提供者发生故障的时候,向调用方返回一个错误响应或者替代响应。从整体负荷来考虑,某个服务熔断后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,这样,虽然服务水平下降,但总比直接挂掉的要好。比如:调用联通接口服务器发送短信失败之后,改用移动短信服务器发送,如果移动短信服务器也失败,则改用电信短信服务器,如果还失败,则返回“失败”响应;再比如:在从推荐商品服务器加载数据的时候,如果失败,则改用从缓存中加载,如果缓存也加载失败,则返回一些本地替代数据。

    在某些情况下,我们也会采取主动降级的机制,比如双十一活动等,由于资源的有限,我们也可以把少部分不重要的服务进行降级,以保证重要服务的稳定,待度过难关,再重新开启。

    Polly基本使用

    在.Net Core中有一个被.Net基金会认可的库Polly,它一种弹性和瞬态故障处理库,可以用来简化对服务熔断降级的处理。主要包含以下功能:重试(Retry),断路器(Circuit-breaker),超时检测(Timeout),舱壁隔离(Bulkhead Isolation), 缓存(Cache),回退(FallBack)。

    该项目作者现已成为.NET基金会一员,一直在不停的迭代和更新,项目地址: https://github.com/App-vNext/Polly

    策略

    在Polly中,有一个重要的概念:Policy,策略有“故障定义”和“故障恢复”两部分组成。故障是指异常、非预期的返回值等情况,而动作则包括重试(Retry)、熔断(Circuit-Breaker)、Fallback(降级)等。

    故障定义

    故障也可以说是触发条件,它使用Handle<T>来定义,表示在什么情况下,才对其进行处理(熔断,降级,重试等)。

    一个简单的异常故障定义如下:

    Policy.Handle<HttpRequestException>()
    

    如上,表示当我们的代码触发HttpRequestException异常时,才进行处理。

    我们也可以对异常的信息进行过滤:

    Policy.Handle<SqlException>(ex => ex.Number == 1205)
    

    如上,只有触发SqlException异常,并且其异常号为1205的时候才进行处理。

    如果我们希望同时处理多种异常,可以使用Or<T>来实现:

    Policy.Handle<HttpRequestException>().Or<OperationCanceledException>()
    
    Policy.Handle<SqlException>(ex => ex.Number == 1205).Or<ArgumentException>(ex => ex.ParamName == "example")
    

    除此之外,我们还可以根据返回结果进行故障定义:

    Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.NotFound)
    

    如上,当返回值为HttpResponseMessage,并且其StatusCodeNotFound时,才对其进行处理。更多用法参考:usage--fault-handling-policies

    故障恢复

    当定义了故障后,要考虑便是如何对故障进行恢复了,Polly中常用的有以下几种恢复策略:

    重试(Retry)策略

    重试就是指Polly在调用失败时捕获我们指定的异常,并重新发起调用,如果重试成功,那么对于调用者来说,就像没有发生过异常一样。在网络调用中经常出现瞬时故障,那么重试机制就非常重要。

    一个简单的重试策略定义如下:

    // 当发生HttpRequestException异常时,重试3次
    var retryPolicy = Policy.Handle<HttpRequestException>().Retry(3);
    

    有些情况下,如果故障恢复的太慢,我们重试的过快是没有任何任何意义的,这时可以指定重试的时间间隔:

    Policy.Handle<HttpRequestException>().WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt - 1)));
    

    如上,重试五次,并且重试时间指数级增加。

    超时(Timeout)策略

    超时是我们比较常见的,比如HttpClient就可以设置超时时间,如果在指定的时间内还没有返回,就触发一个TimeoutException异常,而Polly的超时机制与其类似,只不过超时时触发的是一个TimeoutRejectedException异常。

    // 如果30秒种内没有执行完成,就触发`TimeoutRejectedException`异常
    Policy.TimeoutAsync(30);
    
    // 设置超时回调
    Policy.TimeoutAsync(30, onTimeout: (context, timespan, task) =>
    {
        // do something 
    });
    

    由于超时策略本身就是抛出一个超时异常,所以不需要设置触发条件。

    回退(FallBack)策略

    回退也称服务降级,用来指定发生故障时的备用方案。

    Policy<string>.Handle<HttpRequestException>().FallbackAsync("substitute data", (exception, context) =>
    {
        // do something 
    });
    

    如上,如果触发HttpRequestException异常时,就返回固定的substitute data

    熔断(Circuit-breaker)策略

    断路器用于在服务多次不可用时,快速响应失败,保护系统故障免受过载。

    Policy.Handle<HttpRequestException>().Or<TimeoutException>()
        .CircuitBreakerAsync(
            // 熔断前允许出现几次错误
            exceptionsAllowedBeforeBreaking: 3,
            // 熔断时间
            durationOfBreak: TimeSpan.FromSeconds(100),
            // 熔断时触发
            onBreak: (ex, breakDelay) =>
            {
                // do something 
            },
            // 熔断恢复时触发
            onReset: () =>
            {
                // do something 
            },
            // 在熔断时间到了之后触发
            onHalfOpen: () =>
            {
                // do something 
            }
        );
    

    如上,如果我们的业务代码连续失败3次,就触发熔断(onBreak),就不会再调用我们的业务代码,而是直接抛出BrokenCircuitException异常。当熔断时间(100s)过后,切换为HalfOpen状态,触发onHalfOpen事件,此时会再调用一次我们的业务代码,如果调用成功,则触发onReset事件,并解除熔断,恢复初始状态,否则立即切回熔断状态。

    更多策略的用法查看:usage--general-resilience-policie

    执行

    在上面的示例中,我们熟悉了各种策略的定义,那么接下来就是执行它。也就是使用Polly包裹我们的业务代码,Polly会拦截业务代码中的故障,并根据指定的策略进行恢复。

    最简单的策略执行方式如下:

    var policy = /*策略定义*/;
    var res = await policy.ExecuteAsync(/*业务代码*/);
    

    如果需要同时指定多个策略,可以使用Policy.Wrap来完成:

    Policy.Wrap(retry, breaker, timeout).ExecuteAsync(/*业务代码*/);
    

    其实Warp本质就是多个策略的嵌套执行,使用如下写法效果是一样的:

    fallback.Execute(() => waitAndRetry.Execute(() => breaker.Execute(action)));
    

    关于Polly更详细的用法可以查看Polly Github上的https://github.com/App-vNext/Polly/wiki,本文就不再过多介绍。

    Polly熔断降级实战

    场景:轮询调用服务A和服务B,单次调用时间不得超过1s,调用失败时自动切换到另外一个服务重试一次,如果都失败,进行优雅的降级,返回模拟数据,并在2个服务都多次失败后进行熔断。

    首先创建一个ASP.NET Core Console程序,命名为PollyDemo。

    然后引入Polly的官方Nuge包:

    dotnet add package Polly
    

    在我们首先定义一个超时策略:

    var timeoutPolicy = Policy.TimeoutAsync(1, (context, timespan, task) =>
    {
        Console.WriteLine("It's Timeout, throw TimeoutRejectedException.");
        return Task.CompletedTask;
    });
    

    可以根据实际情况来设置超时时间,我这里为了方便测试,就设置为1s。

    然后定义重试策略:

    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() + "-Retry:[" + retryAttempt + "], wait " + waitSeconds + "s!");
                return waitSeconds;
            });
    

    再定义一个熔断策略:

    var circuitBreakerPolicy = Policy.Handle<HttpRequestException>().Or<TimeoutException>().Or<TimeoutRejectedException>()
        .CircuitBreakerAsync(
            // 熔断前允许出现几次错误
            exceptionsAllowedBeforeBreaking: 2,
            // 熔断时间
            durationOfBreak: TimeSpan.FromSeconds(3),
            // 熔断时触发
            onBreak: (ex, breakDelay) =>
            {
                Console.WriteLine(DateTime.Now.ToString() + "Breaker->Breaking the circuit for " + breakDelay.TotalMilliseconds + "ms! Exception: ", ex.Message);
            },
            // 熔断恢复时触发
            onReset: () =>
            {
                Console.WriteLine(DateTime.Now.ToString() + "Breaker->Call ok! Closed the circuit again.");
            },
            // 在熔断时间到了之后触发
            onHalfOpen: () =>
            {
                Console.WriteLine(DateTime.Now.ToString() + "Breaker->Half-open, next call is a trial.");
            }
        );
    

    如上,连续错误2次就熔断3秒。

    最后,再定义一个回退策略:

    var fallbackPolicy = Policy<string>.Handle<Exception>()
        .FallbackAsync(
            fallbackValue: "substitute data",
            onFallbackAsync: (exception, context) =>
            {
                Console.WriteLine("It's Fallback,  Exception->" + exception.Exception.Message + ", return substitute data.");
                return Task.CompletedTask;
            });
    

    我们的业务代码如下:

    private List<string> services = new List<string> { "localhost:5001", "localhost:5002" };
    private int serviceIndex = 0;
    private HttpClient client = new HttpClient();
    
    private Task<string> HttpInvokeAsync()
    {
        if (serviceIndex >= services.Count)
        {
            serviceIndex = 0;
        }
        var service = services[serviceIndex++];
        Console.WriteLine(DateTime.Now.ToString() + "-Begin Http Invoke->" + service);
        return client.GetStringAsync("http://" + service + "/api/values");
    }
    

    这里方便测试,直接写死了两个服务,对其轮询调用,在生产环境中可以参考上一篇《服务发现之Consul》来实现服务发现和负载均衡。

    现在,我们组合这些策略来调用我们的业务代码:

    for (int i = 0; i < 100; i++)
    {
        Console.WriteLine(DateTime.Now.ToString() + "-Run[" + i + "]-----------------------------");
        var res = await fallbackPolicy.WrapAsync(Policy.WrapAsync(circuitBreakerPolicy, retryPolicy, timeoutPolicy)).ExecuteAsync(HttpInvokeAsync);
        Console.WriteLine(DateTime.Now.ToString() + "-Run[" + i + "]->Response" + ": Ok->" + res);
        await Task.Delay(1000);
        Console.WriteLine("--------------------------------------------------------------------------------------------------------------------");
    }
    

    如上,循环执行100次,策略的执行是非常简单的,唯一需要注意的就是调用的顺序:如上是依次从右到左进行调用,首先是进行超时的判断,一旦超时就触发TimeoutRejectedException异常,然后就进入到了重试策略中,如果重试了一次就成功了,那就直接返回,不再触发其他策略,否则就进入到熔断策略中:

    breaking

    如上图,服务A(localhost:5001)和服务B(localhost:5001),都没有启动,所以会一直调用失败,最后熔断器开启,并最终被降级策略拦截,返回substitute data

    现在我们启动服务A,可以看到服务会自动恢复,解除熔断状态:

    reset

    总结

    本篇首先讲解了一下微服务中熔断、降级的基本概念,然后对.Net Core中的Polly框架做了一个基本介绍,最后基于Polly演示了如何在.NET Core中实现熔断降级来提高服务质量。而熔断本质上只是一个保护壳,在周围出现异常的时候保全自身,从长远来看,平时定期做好压力测试才能防范于未然,降低触发熔断的次数。如果清楚的知道每个服务的承载量,并做好服务限流的控制,就能将“高压”下触发熔断的概率降到最低了。那下一篇就来介绍一下速率限制(Rate Limiting),敬请期待!

    附本篇示例源码地址:https://github.com/RainingNight/AspNetCoreSample/tree/master/src/Microservice/CircuitBreaker/PollyDemo

    参考资料

  • 相关阅读:
    性能优化随笔
    Linux文件类型及如何查看,修改文件读写权限
    ngx_pagespeed
    用U盘安装Linux系统的简单方法
    Maven 3 入门 安装与配置
    CentOS 6.2 安装教程
    各种代码文件中的注释格式
    Linux下的WebLogic安装部署
    Win8常用快捷键
    Jenkins入门总结
  • 原文地址:https://www.cnblogs.com/RainingNight/p/circuitbreaker-polly-in-asp-net-core.html
Copyright © 2011-2022 走看看