熔断与降级:故障的防火墙

23 阅读 1863 字 · 约 7 分钟

一个服务挂了,它自己不可用只是第一步。真正危险的,是调用它的上游也跟着挂,上游的上游也挂,雪崩一路传到最顶层。

熔断和降级就是挡雪崩的防火墙。熔断是发现下游不对劲就停止调用,降级是不可用时给个兜底。两个动作目的相同:不让一个故障传染整个系统。

级联故障是怎么发生的

订单服务调库存服务,库存服务调支付服务。支付服务挂了,库存服务调它会超时。超时不可怕,可怕的是库存服务的线程池在等超时——每个请求占一个线程,等 30 秒才释放。请求量不变,线程池很快被占满,库存服务也响应不了了。订单服务调库存服务同样超时,线程池也被占满。

一个底层服务挂了,像多米诺骨牌一样把上游逐个拖死。不是所有服务同时出问题,是被一个一个拖下水的。

这个过程中,真正杀死上游的不是下游的故障,是上游在等待一个永远不会回来的响应。线程、连接、内存,这些资源在等待中被耗尽,上游自己变成了下一个故障点。

熔断:别再调了

熔断器的思路很直接:调用失败率高了,就别调了。

三种状态。关闭(正常调用)、打开(停止调用)、半开(试探性恢复)。

正常时熔断器关闭,请求照常通过。每条请求的成败被记录,连续失败或一段时间内失败率超过阈值,熔断器打开——后续请求不再发往下游,直接返回错误或走降级逻辑。

打开之后不能永远开着。过一段时间,熔断器进入半开状态,放少量请求试探。这些请求成功了,说明下游恢复了,熔断器关上,恢复正常调用。又失败了,熔断器重新打开,继续等。

关键参数两个:触发阈值和恢复等待时间。阈值太敏感,偶发抖动就熔断,正常请求被拒;阈值太迟钝,等熔断打开时上游已经快挂了。等待时间太短,下游还没恢复就试探,半开失败又打开;太长,下游早就好了,还在空等。

没有万能参数。得根据下游服务的恢复特性和上游的容忍度调。

降级:给不了最好的,给个凑合的

熔断打开后请求怎么办?直接报错是一种,但不是唯一的。

降级是熔断的配套动作。下游不可用时,不给用户看报错,给一个兜底响应。

推荐列表服务挂了,首页不显示推荐,改成显示热门内容——热门是提前缓存的,不依赖实时计算。搜索服务挂了,返回上次搜索结果,或者返回"搜索功能暂时不可用"但页面其他部分正常。

降级的核心是 区分核心链路和非核心链路。下单、支付是核心,不能降级——挂了就是挂了,老老实实报错。推荐、评论、个性化排序是非核心,挂了用兜底数据撑着,别让用户看到白屏或 500。

什么能降级什么不能,不是技术决定的,是业务决定的。这个判断要在事前做,不是故障发生时现场想。故障时每秒都在消耗用户的耐心,没时间讨论优先级。

反面案例:熔断配了等于没配

某系统配了熔断器,阈值设成失败率 90%,恢复等待 5 秒。一次下游数据库主从切换,30 秒内所有请求超时。但失败率要到 90% 才触发熔断,而前 89% 的请求已经把线程池占满了。熔断打开时,上游自己已经挂了。

阈值设得太松,等于没设。熔断器不是事后报告,是事前拦截。如果等故障已经造成破坏才打开,就失去了意义。

另一个常见问题:熔断了但没降级。熔断器打开,请求直接抛异常。用户看到满屏报错。熔断拦住了调用,但用户体验和级联故障一样差。熔断和降级是配套的,光熔断不降级,只是把"慢死"变成了"快死"。

熔断和重试的关系

重试和熔断看起来矛盾:一个要再试,一个要停手。

其实互补。重试对付偶发失败——网络抖了一下,重试就好了。熔断对付持续失败——下游真挂了,重试再多次也没用,反而加重下游负担。

正确的配合是:先重试,重试几次还失败就认了。连续多次请求都重试失败,触发熔断,停止调用。等下游恢复,熔断半开,再开始正常调用。

光重试不熔断,下游挂了你还往人家坟头发请求,每秒几千次重试,下游刚想恢复就被打回去。光熔断不重试,偶发抖动就切断调用,可用性白白损失。

什么时候不用熔断

不是所有调用都需要熔断。

核心链路——下单、支付、扣库存——熔断打开后没有降级方案,报错和级联故障没区别。这些调用该做的是快速失败加排队重试,而不是熔断。熔断的价值在于"可以不调用",如果"必须调用",熔断只是把故障换个形式暴露给用户。

非核心调用才是熔断的主战场。推荐挂了可以不推荐,评论挂了可以不显示。这些"可以不调用"的场景,熔断加降级才能真挡住故障传染。

小结

级联故障的根因不是下游挂了,是上游在等待中耗尽资源。熔断器在失败率升高时停止调用,挡住资源消耗。降级在不可用时给兜底响应,保住用户体验。两者配套才有意义——光熔断不降级是快死,光降级不熔断挡不住资源耗尽。触发阈值和恢复等待时间没有万能值,得按业务调。核心链路没降级方案的,熔断的价值有限,该做的是快速失败。