【大话微服务】漫谈服务熔断、服务降级相关理论

来自:编程新说,作者:编程新说李新杰

生活是个大宝库



“隔行如隔山”这句话相信每个人都知道,在我看来这句话也对也不对。说它对是因为站在专业的角度上看确实是术业有专攻。说它不对是因为站在哲学或人性的角度上看很多事情又是相通的。

“服务熔断、服务降级”对于从来没听说过的人来说绝对是一脸懵逼,而且站在专业的角度去解释的话,别人真不一定能弄明白,不妨先从生活中的事情入手来看看。

马上又要五一了,好多人都计划着出去旅游了吧。假设某个景区里有一个稀世珍宝,好多人都知道了这个消息,且都准备前往一看。

平日里大家都要上班,只有那些自由工作者或无业游民来景区参观,人非常少,所以可以随意驻足、观看、拍照,没有任何限制。

五一前夕,好多人无心工作,干脆请假提前开启假期模式,来景区参观的人多了起来,所以景区规定,每个人只有2个小时的时间,时间一到必须离开。

五一当前,全员放假,开启拥堵模式,一大早景区就人满为患,为了让更多人进去,景区把时间压缩到1小时。但是人还是越来越多,已经达到临界状态了,景区只能关上大门,不让游客再进去。

但是游客肯定不爽啊,大老远一路龟速过来,不让进,肯定不愿意啊。工作人员只好苦口婆心的解释,人太多了,真不能再进去了,否则会出现安全事故。

但也不能让你们白跑一趟啊,作为补偿,每人发一张门票5折优惠券,等人少的时候你们再来吧。

没有时间限制时,就是正常的服务。

限制为2小时,就是服务进行了轻微(或小范围)降级。

限制为1小时,就是服务再次(或大范围)降级。

最后关上大门,就是服务熔断了。

熔断着实有些暴力,所以一般都会给个补偿方案。

再举一个应时应景的例子,保证每个人都能看懂:

服务正常,发4~6个月年终奖。

服务降级,不发年终奖而发996福报。

服务熔断,性价比低的员工直接开除。

补偿方案,多发1个月工资。


必然会产生的结果



通过网络去调用一个远程机器上的服务早已司空见惯,进入微服务时代后,更是成了家常便饭。网络并不是什么时候都是稳定的,所以你的请求可能迟迟得不到响应而直至超时。

更糟糕的是,如果有许多人一起调用这个无法响应的服务,必然会导致某些关键性的资源被用完,无异于是雪上加霜,最终直接崩溃了。

比如一个数据库表数据量很大但又没有索引,直接全表扫描,查询很慢,一直无法返回结果,且CPU很高。此时如果请求很多的话,连接池中的数据库链接立马被用完,许多慢查询叠加在一起,CPU直接100%,虽然机器不一定崩溃,但是整体无法响应。

现实中有很多问题是无法解决的,我们能做的就是尽量避免它的发生。对于此类问题,一定要在如何避免上下功夫。当然这种事情也只能根据情况而定了。

比如食品安全问题,我们能做的只能是少在外面吃饭或努力选择安全食材。比如空气雾霾问题,我们能做的就是尽量减少户外活动或出门戴口罩。

当然也会有一些强制措施,比如给汽车装上限速软件,无论怎么踩油门,速度都无法超过阈值。

回到计算机里面,可以为每一个服务配置一个计数器,当同时调用该服务的人数达到阈值时,对于后续的调用则不再去执行服务而是直接返回。

也可以为每一个请求都配置一个计时器,当执行时间达到阈值时,即使还没有拿到服务的结果,也会主动断开,返回其它结果而不是无限期的耗下去。

只要是有些经验的人,都能想到这些方法,接下来就看看大牛(Martin Fowler)给的方案吧。


断路器(CircuitBreaker)



断路器就是一种可以使调用链路断开的装置。可以把它想象成家庭电路中的保险丝,在电流过大时会自动熔断而切断电路起到保护整个电路的作用。软件中的断路器的行为语义将会更加丰富一些。

就是把一个受保护的服务调用包装进一个断路器对象里,断路器对象会对失败进行监控。(可以认为断路器对象就是个小代理)。

一旦失败的次数达到一个确定的阈值,断路器启动。所有后续的对断路器的调用都会直接得到一个错误,而不会再去调用受保护的服务。

因此断路器对象需要有一些必要的参数,如阈值、超时时间等,还需要一个计数器记录失败次数,还需要有一个状态字段标识断路器当前的状态。

一开始断路器状态是闭合的,都会去调用受保护的服务,当失败或超时时,计数器加1,当成功时重新将它置为0。如果出现连续失败导致计数器达到了阈值,则断路器断开,变为断开状态,后续请求将直接返回错误。

可见断路器内部的原理并不复杂,只不过还有一个问题需要考虑,那就是服务可能过了一会儿恢复了,此时断路器应该重新闭合上才对,那这个闭合的动作应该由谁去发起呢?

我们知道,保险丝在熔断后,它自己不可能再重新接上,必须由相关人员去更换,也就是说需要外部力量介入。

那软件中的断路器在断开后,当再次闭合时,也需要外部力量介入吗,它自己不会自动闭合吗?


软件的行为语义更加丰富



对于软件断路器,我们可以让它自己去检测底层服务是否再次可用。实现起来也非常简单,那就是在一段合适的时间间隔之后,再尝试去调一次受保护的服务,如果成功的话就把断路器闭合上。

此时需要一个变量来存储最后一次失败的时间,还需要一个变量来存储时间间隔。假设自最后一次失败以来已经经过了这个指定的时间间隔,此时断路器会被设置为一个特殊的新状态,即半开状态

它表明断路器已经做好准备去执行一次真正的服务调用试验,来看看问题是否已经被修复。之所以把它叫做试验性调用,就是因为如果调用成功就闭合断路器,如果仍然失败则记录下当前时间,进入下一轮时间间隔的等待。

在实际中断路器的实现可能会复杂些,毕竟导致错误的情况很多,每种情况的处理逻辑都不一样,还有就是不同错误类型对应的阈值也是不一样的,如超时错误可以多尝试几次,但是连接失败的情况尝试3次就足够了。

而且断路器断开的策略不一定按失败的次数,可以按一段时间窗口内失败量所占的百分比,比如连续10分钟内有50%的失败就断开断路器。

还可以使用一个线程池,为每一个请求分配一个线程,当线程池里的线程用完的话,断路器断开。当线程池里又有线程时,断路器闭合。

还可以使用一个队列,把所有的请求都放入队列中,服务提供者按照固定的速度去消费,当队列被填满时,断路器断开。当队列里又有空间时,断路器闭合。


事物的两面性



断路器的好处是显而易见的。比如避免由于下游服务出故障而导致上游服务也挂掉。再比如避免由于部分服务出故障而导致整个机器崩溃,所有服务都不可用。

但是这样会给客户端带来一些麻烦,客户端必须要对断路器的失败做出反应,而且不同种类的失败可能反应都不一样。这会增加一些工作量的。

推荐↓↓↓
Java编程
上一篇:如果你这样回答“什么是线程安全”,面试官都会对你刮目相看