JAVA面试题分享三百四十八:来聊聊微服务的熔断保护机制

前言

在实现高并发的分布式系统中,服务间调用的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),否则触发熔断。

 

对于一些业务逻辑上就有所限制的接口,通过这种策略能够把相当一部分问题扼杀在萌芽阶段中。

- 错误比例

主要关注【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()方法

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),则触发熔断。

图片

对于一些比较低频的请求,是没办法通过错误比例来计算的(上面解释过了),就可以按这种策略来处理。

场景

把接口或者RPC调用的场景,按照访问频率和响应时间两个维度,相互交叉可以得到四种可能存在的场景:

1.高频+高耗时;

2.高频+低耗时;

3.低频+高耗时;

4.低频+低耗时;

下面再以一个电商系统为例,举例说明场景模拟。

1.高频+高耗时

场景: 搜索接口,ES或者CK相关的基础服务出现故障,导致搜索不可用。(想一下京东APP首页最上面的搜索栏不可用,妥妥的A级事故)

应对: 这种故障真出现了,上游调用端要即时发现并熔断,同时下游服务端所引用的基础设施ES/CK也要即时激活应对方案-异地多活/弹性伸缩/节点迁移重新选主/多副本...balabala,等下游重建完成后上游打开放流量进来并逐渐全量。但是细想这种关键核心接口一般也不会走到熔断,因为如果流量突发大量请求进来,前置的网关层或者Nginx应该要做一下保护拦截,或者加一个转发路由+限流的配置。

2.高频+低耗时

场景: sku接口(商品规格详情接口),相关sku缓存失效过期或者查询失败,导致每次都要去查询DB,RT升高。

应对: 转一级缓存为多级缓存,缓存时间打散错开。

3.低频+高耗时

场景: 供应商全量接口,调用耗时过大,导致超时或者失败。

应对: 这种可以考虑加一个中间调用方,中间层做是否真正下调判断,如果处于熔断状态,直接返回默认或者上次缓存过的供应商数据(通常情况下一般供应商资料一个月都更新不上一次)。

4.低频+低耗时

场景: 边缘服务接口,是无法持续触发熔断的(调用次数比较低,多次调用间的时间间隔过长,滑动窗口都已经滑过去,达不到阈值)。

应对: 不过这种情况也不需要熔断,想象一下线上出故障半天了都没有人反馈的那种极度边缘服务,还是低延时,流量峰期一过服务器的吞吐量就能重新上去了。

疑点

问1:如果已经触发了限流,是否还要记录进行请求的熔断计数的判定?

答1:要算,限流是服务端的规则判定,熔断是调用端的,两者没有相互干扰的关系,是独立进行的。

问2:在RPC框架中,在哪个步骤整合熔断器会比较合适?

答2:京东架构师何小锋在他的课程《RPC实战和核心原理》里面有一章讲过这个问题,他的原文描述是这样的:

熔断机制主要是保护调用端,调用端在发出请求的时候会先经过熔断器。我们可以回想下RPC的调用流程:

 

所以在哪个步骤整合熔断器是会比较合适呢?我的建议是动态代理,因为在RPC调用的流程中,动态代理是RPC调用的第一个关口。在发出请求时先经过熔断器,如果状态是闭合则正常发出请求,如果状态是打开则执行熔断器的失败策略。

上面何小锋的讲解,我理解是整合在动态代理上,如果真熔断了至少省去序列化到编码的那部分时间,但是按照大部分人的理解惯性以及一些框架的设计(我至少翻了两个),是在网络准备传输之前处加上的。两边都有考虑的点,具体如果抉择还是要看具体的使用场景吧。

延伸

参考原型

微服务中的熔断机制其实是参考了我们日常生活中保险丝的保护机制。

百度百科1:保险丝也被称为电流保险丝,IEC127标准将它定义为熔断体。其主要是起过载保护作用。电路中正确安置保险丝,保险丝就会在电流异常升高到一定的高度和热度的时候,自身熔断切断电流,保护了电路安全运行。

当然,其他领域还有一种名气更大的熔断机制。

百度百科2:股市交易熔断机制,也叫自动停盘机制,是指当股指波幅达到规定的熔断点时,交易所为控制风险采取的暂停交易措施。具体来说是对某一合约在达到涨跌停板之前,设置一个熔断价格,使合约买卖报价在一段时间内只能在这一价格范围内交易的机制。(梦回20年3月美股10天4熔断停盘,巴菲特都感叹系列)

很多时候,IT技术领域的创新都是来源于生活或者借鉴于其他领域。

熔断/限流/降级 对比关系

作为服务自理的三把大刀,三者的对比和关系也很容易被打乱。

熔断和限流除了上文所说的针对调用端和服务端的角度看之外,还可以这样区分:限流是持续性的过程,而熔断是非持续性的过程;熔断说的是服务之间的调用能实现自我恢复的状态,终有停止熔断的时刻,是非持续的;限流是从系统的流量入口考虑,对进入流量做限制从而保护系统,是能持续进行的;

降级是从整体资源使用的角度来考虑的。资源不够的时候,可以牺牲低优先级服务来保证高优先级服务的正常进行,考虑的是系统平级服务或者整个业务维度。

综上汇总三者关系:熔断和限流都可以认为是降级的一种方式。

 

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
垃圾回收机制Java中的一项重要特性,它负责自动管理内存的分配和释放,使得开发人员无需手动处理内存的分配和释放问题。Java的垃圾回收机制主要基于以下几个原则: 1. 引用计数:这是一种简单的垃圾回收算法,它通过计数对象被引用的次数来确定是否需要回收。当对象的引用计数为0时,即没有任何引用指向该对象时,垃圾回收器会将其标记为可回收。 2. 可达性分析:这是Java中主要采用的垃圾回收算法。它通过判断对象是否可达来确定是否需要回收。当一个对象不再被任何活动对象引用时,即无法通过任何路径访问到该对象时,垃圾回收器会将其标记为可回收。 3. 垃圾回收器:Java提供了不同类型的垃圾回收器,如Serial、Parallel、CMS、G1等。这些垃圾回收器使用不同的算法和策略来执行垃圾回收操作。例如,Serial垃圾回收器使用单线程进行垃圾回收,适用于小型应用;而Parallel垃圾回收器使用多线程进行垃圾回收,适用于大型应用。 4. 垃圾回收算法:Java的垃圾回收算法主要包括标记-清除、复制、标记-整理等。标记-清除算法首先标记所有活动对象,然后清除未标记的对象。复制算法将内存分为两个区域,每次只使用其中一个区域,将活动对象复制到另一个区域,并清除未复制的对象。标记-整理算法将活动对象向一端移动,然后清除未移动的对象。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值