前言
在实现高并发的分布式系统中,服务间调用的RPC也会面临高并发的场景。在这样的情况下,我们提供服务的每个服务节点就都可能由于访问量过大而引起一系列的问题(业务处理耗时过长/CPU飘高/频繁GC/服务进程直接宕机等等)。但是在生产环境中,我们要保证稳定性和高可用性,就要对微服务系统进行自我保护,从而保证在高访问量、高并发。
RPC调用中的服务端自我保护最通用的做法就是限流,限流本身也有很多种做法,令牌桶、漏桶(单机),redis+lua脚本(分布式)。
而本文要分析的是调用端是如何进行自我保护的,这里就引出了文章的主角 -- 熔断机制。
定义
服务治理中的熔断机制,指的是在发起服务调用的时候,如果被调用方返回的错误率超过一定的闻值,那么后续的请求将不会真正发起请求,而是在调用方直接返回错误。
需要熔断的几种场景:
1.服务依赖的资源出现大量错误。
2.某个用户超过资源配额时,后端任务会快速拒绝请求,返回"配额不足"的错误,但是拒绝回复仍然会消耗一定资源,有可能后端忙着不停发送拒绝请求,导致过载。
设计
熔断器工作机制主要是关闭、打开和半打开这三个状态之间的切换 。在正常情况下,熔断器是关闭的;
当调用端调用下游服务出现异常时,熔断器会收集异常指标信息进行计算,当达到熔断条件时熔断器打开,这时调用端再发起请求是会直接被熔断器拦截,并快速地执行失败逻辑;
当熔断器打开一段时间后,会转为半打开状态,这时熔断器允许调用端发送一个请求给服务端,如果这次请求能够正常地得到服务端的响应,则将状态置为关闭状态,否则设置为打开。
注意:熔断器打开有两个条件-QPS超过多少以及错误率达到多少。 如果QPS特别低,只有2,错了1个请求错误率就50%了,这个时候肯定是不能打开熔断器的。所以要同时判断错误率和QPS。
用户通过设置熔断规则(Rule)来给资源添加熔断器,修改配置文件时会将每一个熔断规则转换成对应的熔断器,熔断器对用户是不可见的。最终实现的每个熔断器都会有自己独立的统计结构。
熔断器的整体检查逻辑可以用几点来精简概括
1.基于熔断器的状态机来判断对资源是否可以访问;
2.对不可访问的资源会有探测机制,探测机制保障了对资源访问的弹性恢复;
3.熔断器会在对资源访问的完成态去更新统计,然后基于熔断规则更新熔断器状态机;
本质
最简单的概括就是:通过计数判断被调用接口是否健康,若不健康则直接返回错误。那么问题来了,如何定义是否健康?
我们衡量下游服务质量时候,场景的指标就是RT(response time)、异常数量以及异常比例 等。 So,与之对应的溶断器支持三种熔断策略:慢调用比例熔断,错误比例熔断,错误计数熔断。
慢调用比例
请求响应时间(RT): 调用目标的响应时间,单位秒
最大响应时间(MaxAllowedRt): 调用目标最大的响应时间
持续时间(T): 统计持续时间
熔断器不在静默期,并且慢调用的比例大于设置的闻值,则接下来的熔断周期内对资源的访问会自动地被熔断。该策略下需要设置允许的调用RT临界值(即最大的响应时间),对该资源访问的响应时间大于该闻值则统计为慢调用。
持续时间(T)的请求响应时间(RT)的均值必须小于最大响应时间(MaxAllowedRt),否则触发熔断。
对于一些业务逻辑上就有所限制的接口,通过这种策略能够把相当一部分问题扼杀在萌芽阶段中。
错误比例
参考大名鼎鼎的Google Sre的论文,主要关注【Client request rejection probability】这小节,即下图
先把公式展开来看下
请求数量(requests): 调用方发起请求的数量总和
请求接受数量(accepts): 被调用方正常处理的请求数量总和
在正常情况下,这两个值是相等的,随着被调用方服务出现异常开始拒绝请求,请求接受数量(accepts)的值开始逐渐小于请求数量(requests),这个时候调用方可以继续发送请求。
直到requests=K*accepts,一旦超过这个限制,熔断器就会打开,新的请求会在本地以一定的概率被抛弃直接返回错误。
可见K的取值对于熔断器的作用效果是非常明显的 ,重点关注一下"We generally prefer the 2x multiplier......"的那三段,啃下英文,大概意思总结就是:
我们倾向于使用2x,能让比实际允许的更多的请求到达后端,如果后端停止接受流量,客户端检测延迟时间会更短;自适应节流工作很好,稳定请求率;而且客户端是基于本地信息做出决策,没有额外依赖关系;但是也有弊端,客户端不能很好地处理偶尔向后端发送的请求,而这种情况下可能会额外造成代价。
这个计算公式的好处在于,不会直接一刀切的丢弃所有请求,而是计算出一个概率来进行判断。
论文如此,来看看实际的代码实现,以go-zero为例:
源码文件:go-zero/core/breaker/googlebreaker.go 的accept()方法
googlebreaker.go
func (b *googleBreaker) accept() error {
accepts, total := b.history()
weightedAccepts := b.k * float64(accepts)
dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1))
if dropRatio <= 0 {
return nil
}
if b.proba.TrueOnProba(dropRatio) {
return ErrServiceUnavailable
}
return nil
}
可以说就是论文中计算公式的代码最简实例化。
错误计数
请求数量(requests): 调用方发起请求的数量总和
请求接受数量(accepts): 被调用方正常处理的请求数量
最大失败次数(maxtimes): 最大失败次数
持续时间(T): 统计持续时间
在正常情况下,大于号左边是0。一旦差值超过最大次数,则会熔断器就会打开。
持续时间(T)内,请求数量(requests)-请求接受数量(accepts)小于最大失败次数(maxtimes),则触发熔断。
对于一些比较低频的请求,是没办法通过错误比例来计算的(上面解释过了),就可以按这种策略来处理。
上面是3种策略的理论介绍,下面3篇是与之对应的实操验证文章,贴出来大家参考一下。
场景
把接口或者RPC调用的场景,按照访问频率和响应时间两个维度,相互交叉可以得到四种可能存在的场景:
1.高频+高耗时;
2.高频+低耗时;
3.低频+高耗时;
4.低频+低耗时;
下面再以一个电商系统为例,举例说明场景模拟:
1.高频+高耗时
场景: 搜索接口,ES或者CK相关的基础服务出现故障,导致搜索不可用。(想一下京东APP首页最上面的搜索栏不可用,妥妥的A级事故)
应对: 这种故障真出现了,上游调用端要即时发现并熔断,同时下游服务端所引用的基础设施ES/CK也要即时激活应对方案-异地多活/弹性伸缩/节点迁移重新选主/多副本...balabala,等下游重建完成后上游打开放流量进来并逐渐全量。但是细想这种关键核心接口一般也不会走到熔断,因为如果流量突发大量请求进来,前置的网关层或者Nginx应该要做一下保护拦截,或者加一个转发路由+限流的配置。
2.高频+低耗时
场景: sku接口(商品规格详情接口),相关sku缓存失效过期或者查询失败,导致每次都要去查询DB,RT升高。
应对: 转一级缓存为多级缓存,缓存时间打散错开。
3.低频+高耗时
场景: 供应商全量接口,调用耗时过大,导致超时或者失败。
应对: 这种可以考虑加一个中间调用方,中间层做是否真正下调判断,如果处于熔断状态,直接返回默认或者上次缓存过的供应商数据(通常情况下一般供应商资料一个月都更新不上一次)。
4.低频+低耗时
场景: 边缘服务接口,是无法持续触发熔断的(调用次数比较低,多次调用间的时间间隔过长,滑动窗口都已经滑过去,达不到阈值)。
应对: 不过这种情况也不需要熔断,想象一下线上出故障半天了都没有人反馈的那种极度边缘服务,还是低延时,流量峰期一过服务器的吞吐量就能重新上去了。
疑点
问:如果已经触发了限流,是否还要记录进行请求的熔断计数的判定?
答:要算,限流是服务端的规则判定,熔断是调用端的,两者没有相互干扰的关系,是独立进行的。
问:在RPC框架中,在哪个步骤整合熔断器会比较合适?
答:京东架构师何小锋在他的课程《RPC实战和核心原理》里面有一章讲过这个问题,他的原文描述是这样的:
熔断机制主要是保护调用端,调用端在发出请求的时候会先经过熔断器,我们可以回想下RPC的调用流程。
所以在哪个步骤整合熔断器是会比较合适呢?我的建议是动态代理,因为在RPC调用的流程中,动态代理是RPC调用的第一个关口。在发出请求时先经过熔断器,如果状态是闭合则正常发出请求,如果状态是打开则执行熔断器的失败策略。
上面何小锋的讲解,我理解是整合在动态代理上,如果真熔断了至少省去序列化到编码的那部分时间,但是按照大部分人的理解惯性以及一些框架的设计(我至少翻了两个),是在网络准备传输之前处加上的。两边都有考虑的点,具体如何抉择还是要看具体的使用场景吧。
延伸
参考原型
微服务中的熔断机制其实是参考了我们日常生活中保险丝的保护机制。
百度百科1:保险丝也被称为电流保险丝,IEC127标准将它定义为熔断体。其主要是起过载保护作用。电路中正确安置保险丝,保险丝就会在电流异常升高到一定的高度和热度的时候,自身熔断切断电流,保护了电路安全运行。
当然,其他领域还有一种名气更大的熔断机制。
百度百科2:股市交易熔断机制,也叫自动停盘机制,是指当股指波幅达到规定的熔断点时,交易所为控制风险采取的暂停交易措施。具体来说是对某一合约在达到涨跌停板之前,设置一个熔断价格,使合约买卖报价在一段时间内只能在这一价格范围内交易的机制。
(梦回20年3月美股10天4熔断停盘,巴菲特都感叹系列)
很多时候,IT技术领域的创新都是来源于生活或者借鉴于其他领域。
熔断/限流/降级 对比关系
作为服务治理的三把大刀,三者的对比和关系也很容易被弄乱。
熔断和限流除了上文所说的针对调用端和服务端的角度看之外,还可以这样区分:限流是持续性的过程,而熔断是非持续性的过程 ;熔断说的是服务之间的调用能实现自我恢复的状态,终有停止熔断的时刻,是非持续;限流是从系统的流量入口考虑,对进入流量做限制从而保护系统,是能持续进行的;
降级是从整体资源使用的角度来考虑的。资源不够的时候,可以牺牲低优先级服务来保证高优先级服务的正常进行,考虑的是系统服务或者整个业务维度。
综上汇总三者关系:熔断和限流都可以认为是降级的一种方式。
参考资料
除了文中已经贴上的链接之外,本文还参考了下述文章
过载保护、限流与熔断
10张图带你彻底搞懂限流、熔断、服务降级
最后的最后,希望读到这里的你给作者一点正能量反馈,点赞收藏评论一下,毕竟原创不易,谢谢。