熔断、降级和 AOP
1. 什么是熔断?
熔断常常出现在股市中,是指当股指波幅达到规定的熔断点时,交易所为控制风险采取的暂停交易措施。同样,在程序中也引入了熔断,程序中的熔断和股市中的熔断具有类似的意思,当下游服务因访问压力过大而响应变慢或失败,或者下游服务出现异常时,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。如果项目中不适用熔断将会影响到整个系统的使用,我们来看一个例子。
在一个系统中存在 A 、B 、C 这三个服务(子系统)。正常情况下 A 调用 B ,B 再调用 C ,返回的数据会按照 C 、B 、 A 的顺序来返回。这时当 C 因为某种原因导致数据处理量减少或者 B 发送了超出 C 处理能力的数据量,这时 C 就产生的阻塞。不仅大量的数据一直在等待 C 来处理,B 还会继续发送数据到 C 。但是因为 C 的处理能力有限,B 又无法得到响应,这时 B 也产生了阻塞,同样的 A 继续发送数据给 B ,一段时间后 A 无法得到响应 A 出现了阻塞。那么这个时候整个系统就都出现问题了,最后系统崩溃。这种链路故障被称为雪崩。
当我们引入了熔断机制这种情况就完全可以避免。在引入熔断之前,我们需要知道两个概念开启熔断和恢复熔断 。
开启熔断
在固定时间范围内接口的调用超时比率达到或超过一个阈值就会开启熔断。之后对该服务接口的调用将不再经过网络,而是自行本地的默认方法,这样就达到了服务降级的效果。
恢复熔断
熔断经过了设定的时间之后,服务将从熔断状态恢复位初始状态,之后再次接受调用方的远程调用。当调用服务超时比率再次达到或超过一个阈值就会重新开启熔断。好到现在未知我们已经了解了这两个概念,前面的例子只需要在每个调用初增加熔断机制即可,当出现超时比率到达所定义的阈值时,熔断机制将启动。经过一段时间后达到了重试的时间,服务的熔断状态消失,再次开始调用。
2. 什么是降级
降级是当某个服务出现故障启动熔断机制后,向调用方返回一个替代响应或者规范化的错误响应。例如我们的程序需要发送邮件,首先调用自建邮箱发送,当自建邮箱发送失败后调用网易邮箱发送邮件,如果网易邮箱发送失败,那么我们就调用 QQ 邮箱发送,如果 QQ 邮箱也发送失败,那么我们就返回发送失败响应。降级也可以看作服务的选择性放弃,例如在电商活动中为了应对大规模请求,都会将部分服务优先级降低或者暂停服务,等待峰值下降后再恢复被降级服务的优先级。
3. AOP
所谓的 AOP 就是面向切面编程,是指在运行时动态的将代码加入到指定的方法中或指定的位置中的编程思想。常见的 AOP 场景是日志记录,在面向对象的编程中类之间无法直接进行联系,因此无法将重复的日志代码封装起来。这时我们就可以利用 AOP 来解决,把多个类公用的代码抽取出来做为一个切片,在我们需要用到时再把它切入到指定的方法或位置中。我们一般把放入到指定方法或指定类中的代码称为切面,被切入的方法、类和位置被称为切入点。AOP 和 OOP 相辅相成,OOP 是从横向上规划处多个类,而 AOP 是从纵向上向对象中加入指定的代码, AOP 和 OOP 的结合使得代码变得更具立体感。
实现 ASP.NET Core 熔断与降级
前面说了那么多,现在我们就来看一下 ASP.NET Core 是怎么实现熔断和降级的。在这里我们需要用到一个第三方库 Polly 。它是被 .NET 基金会认客的弹性和瞬态故障处理库,可以让我们以顺畅并且线程安全的方式来执行重试、断路(熔断)、故障恢复等策略。Polly 包含 7 个功能:重试、断路(熔断)、降级、超时、隔离、缓存以及策略包集合 Polly 的核心是动作和故障,其中动作主要包含降级、熔断和重试等,策略则包含用来执行的代码。
下面我们来讲解一下 Polly 的这 7 个功能:
1. 重试
重试比较好理解,当出现故障时需要重新执行部分代码,代码如下:
class Program
{
static void Main(string[] args)
{
try
{
Policy.Handle<HttpRequestException>().Retry(5, ((exception, count, context) =>
{
Console.WriteLine($"第 {count} 次重试");
})).Execute(GetUser);
}
catch(Exception e)
{
Console.WriteLine("异常抛出");
}
Console.Read();
}
static void GetUser()
{
Console.WriteLine("开始");
Thread.Sleep(1000);
throw new HttpRequestException();
}
}
在上面的代码中我们定义了一个 GetUser 方法,这个方法用来模拟网络请求并在方法结尾制造了一个 Http 异常。在 Main 方法中我们设定了重试次数 5 次,并且规定了重试过程中要执行的业务逻辑,最后调用 Execute 方法指定针对哪段代码执行这个策略。我们运行代码可以发现当触发异常后,程序不断的重试调用指定的代码段,直到达到重试次数后不再重试并抛出异常。我们在使用重试策略需要注意的是,当达到重试次数后代码会抛出异常,因此我们必须在将充实策略代码包裹在 try_catch 代码段中。
2. 降级
当无法避免的错误发生时,需要要有一个合理的返回来代替失败。例如当一个用户使用手机号加验证码的形式注册的时候是没有密码的,这时我们需要给它设置一个默认的密码。
Policy.Handle<Whatever>()
.Fallback<User>(() => User.SetPassword())
3. 断路
当遇到严重问题时,我们要做的是马上将失败信息反馈给调用方而不是让调用方一直等待。例如当我们的程序调第三方开发的 API 时,这时第三方 API 很长时间都没有响应,那么这时我们可以认为第三方 API 有很大可能出现了问题(例如服务器宕机、 API bug等)。如果这时我们的程序还在一直重试,那么不仅会加重系统的负担,而且还有可能造成程序其它部分受影响,甚至整个程序崩溃。因此当系统出错的次数超过指定阈值,就必须中断所有的请求并等待一定时间后再继续。同样我们来看一下代码:
Policy.Handle<HttpRequestException>().CircuitBreaker(10, TimeSpan.FromSeconds(5),(exception, ts) =>
{
Console.WriteLine($"系统{exception.Message}在 {ts.Seconds}秒后重试");
},()=>
{
Console.WriteLine("重启!");
}).Execute(GetUser);
上述代码段定义了断路策略,当程序出现 Http 异常 10 次时就暂停 5 秒钟后继续执行,并且这段代码还定义了断路时中断所执行的代码和重启时所需要执行的代码。
4. 超时
当程序等待超过一定时间后我们就可以判断不可能会成功了。这种情况经常发生在网络请求中,例如正常情况下请求一次数据几乎是瞬间完成,如果在一次网络请求超过了 N 秒还没完成就说明本次请求出现异常,因此我们需要设置程序的超时时间 N 来避免程序长时间的等待。
Policy.Timeout(20, (context, timespan, task) =>
{
Console.WriteLine($"{context} {timespan} {task}");
}).Execute(GetUser);
上述代码中定义了当等待时间超过 20 秒后将触发回调函数。
5. 隔离
当程序某处出现故障可能会促发多个失败的调用,这样就很容易耗尽宿主机资源。因此要将可控的操作限制在一个固定大小的资源池中,以隔离有潜在可能相互影响的操作。例如利用 Polly 策略设置最多允许 5 个线程并发执行,如果执行被拒绝则执行回调。
Policy.Bulkhead(12, context =>
{
Console.WriteLine("回调代码")
});
6. 缓存
在开发时我们会把频繁使用并且很少变化的资源缓存起来,来提高系统的性能。Polly 提供了强大且易于上手的缓存策略的支持,使得我们不用再手动编写缓存策略。这里我不提供示例代码,是因为这一小部分需要根据不同项目和不同公司的框架来订。
7. 策略集合
当一个操作有不同的故障,并且不同的故障处理需要不同的策略时这些不同的策略必须作为一个集合放在在一起才能应用在同一种操作上。这时我们可以这么来写策略集合:
Policy.Wrap(fallback, cache, retry, breaker, timeout, bulkhead);
总结
本文开头主要介绍了软缎和降级的相关概念,然后用讲解了 Polly 的其中策略,这些都可以作为掌握 Polly 的基础。因为 Polly 是一个强大而丰富的 .NET 第三方库,无法通过一篇文章具体而又详细的讲解出来,因此对于 Polly 和 熔断以及降级有兴趣的同学可以上 Polly 官方文档中具体学习。
作者简介:朱钢,笔名喵叔,CSDN博客专家,.NET高级开发工程师,7年一线开发经验,参与过电子政务系统和AI客服系统的开发,以及互联网招聘网站的架构设计,目前就职于北京恒创融慧科技发展有限公司,从事企业级安全监控系统的开发。