ASP.NET Core 微服务初探[2]:熔断降级之Polly

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

服务熔断

ASP.NET Core 微服务初探[2]:熔断降级之Polly

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

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

ASP.NET Core 微服务初探[2]:熔断降级之Polly

服务降级

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

降级的目的是当某个服务提供者发生故障的时候,向调用方返回一个错误响应或者替代响应。从整体负荷来考虑,某个服务熔断后,服务器将不再被调用,此时客户端可以自己准备一个本地的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异常,然后就进入到了重试策略中,如果重试了一次就成功了,那就直接返回,不再触发其他策略,否则就进入到熔断策略中:

ASP.NET Core 微服务初探[2]:熔断降级之Polly

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

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

ASP.NET Core 微服务初探[2]:熔断降级之Polly

总结

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

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

参考资料

上一篇:jmeter笔记2


下一篇:我的学习之路_第二十章_JDBC