小Cの已经记不起来的博客

熔断:写给初学者的通俗解释

前段时间项目里有个服务半夜报警,爬起来一看日志,发现挂的原因特别憋屈:它自己代码一点毛病没有,是它依赖的下游服务卡住了。一个请求卡住不要紧,结果新请求源源不断地进来排队,最后把它也活活拖死了。修完之后我就去补了补熔断相关的知识,结果发现网上讲这东西的文章不是术语满天飞就是直接甩框架文档(也可能是我搜的关键词不对,反正当时没看到几篇讲人话的...),所以干脆自己写一篇大白话的。

先从家里的电闸说起

这个类比不新鲜,但真的好用。家里的空气开关大家都见过吧:一旦电路里电流过大,比如短路了,或者同时开太多大功率电器,它会“啪”地跳掉,把整条电路断开。看起来像把事情搞砸了,家里停电了嘛,但如果没有它,电线可能直接烧起来,烧的就是整个房子。

跳闸之后你会干嘛?你不会傻乎乎地立刻把闸合上去再试一次,你会先排查一下是哪个电器出了问题,处理完再合闸,合上之后可能还会留意一会儿,看会不会又跳。

微服务里的熔断器干的就是一模一样的事:发现下游服务不对劲,先跳闸断开调用,隔一会儿试探性放几个请求过去,没问题了再恢复正常。就这么点事,没什么玄乎的。

没有熔断会发生什么

举个最常见的场景。你有个订单服务,下单的时候要调库存服务查库存。某天库存服务因为一条慢查询变得特别慢,一个请求要卡 30 秒才返回。

这时候订单服务这边发生的事是:每个进来的下单请求都得干等库存服务返回,线程卡在那里动不了。用户一看页面没反应,就刷新,再点一次,请求更多了,线程占得更快。要不了几分钟,订单服务的线程池被占满,彻底没法处理任何请求,包括那些压根不需要查库存的请求。

更惨的是,调用订单服务的上游也会因为同样的逻辑被拖垮,一层一层传下去,最后整条链路全躺平。这就是常说的服务雪崩。你回头复盘会发现,罪魁祸首可能只是数据库里一个没加索引的查询,但代价是全线瘫痪。

熔断解决的就是这个问题:与其让每个请求都傻等 30 秒然后一起死,不如发现下游连续失败之后干脆不发了,直接快速失败。

熔断器的三种状态

熔断器内部就三个状态,理解了这三个,基本就理解了熔断的全部:

对应到跳闸的场景:打开就是跳闸了,半开就是“试着合一下闸看看”,关闭就是恢复正常用电。

撸一段伪代码感受一下

不看代码总觉得隔着一层,我写个极简的示意版本(别直接抄,看懂意思就行):

失败计数 = 0
状态 = "closed"

def 调用库存服务(sku):
    global 失败计数, 状态

    if 状态 == "open":
        return 兜底库存(sku)  # 注意:压根没发请求

    try:
        结果 = 请求库存服务(sku, timeout=1)
        失败计数 = 0
        return 结果
    except Exception:
        失败计数 += 1
        if 失败计数 >= 5:   # 连续挂 5 次
            状态 = "open"   # 跳闸
        return 兜底库存(sku)

就这么点逻辑。真实框架会复杂一些,比如用滑动窗口统计失败率、半开状态下只放行固定几个请求之类的,但核心思想没变。

真实项目里一般怎么用

自己手写没必要,现成轮子很多。老一辈用的是 Hystrix,不过官方早就不维护了,新项目别再选它。现在 Java 圈常见的是 Resilience4j 和阿里的 Sentinel。以 Resilience4j 为例,加个注解就行:

@CircuitBreaker(name = "stockService", fallbackMethod = "stockFallback")
public StockInfo getStock(String sku) {
    return stockClient.query(sku);
}

// 兜底方法的签名要在末尾多一个 Throwable 参数,不然框架找不到
public StockInfo stockFallback(String sku, Throwable t) {
    return StockInfo.unknown(sku); // 返回"库存查询中",而不是把异常抛给用户
}

阈值、冷却时间这些参数都可以在配置文件里调,不用改代码。

几个容易踩的坑

阈值别设太灵敏。 我一开始设了个“连续失败 2 次就熔断”,结果网络稍微抖一下就跳闸,恢复后又立刻被抖跳,在正常和熔断之间反复横跳,比不熔断还难受。实际项目里用“时间窗口内的失败率”会比“连续失败次数”稳得多。

兜底逻辑要认真写。 很多人熔断配置得明明白白,fallback 里却只是把异常重新抛出去,那熔断了和没熔断对用户来说没区别。想清楚下游挂了的时候用户该看到什么:缓存的旧数据?一个友好的提示?“库存查询中,稍后刷新”?这一步才是体验的关键。

分清楚熔断、降级、限流。 这仨初学者特别容易混。简单说:熔断是调用方的自我保护,发现下游不行了就不调了;降级是出问题时的备用方案,上面说的兜底逻辑就是降级的一种;限流是被调用方的自我保护,进来的请求太多时排队或者干脆拒绝一部分。三者经常一起出现,但不是一回事。

要有监控告警。 熔断器跳闸了你自己得第一个知道,不然用户反馈“页面怎么一直显示查询中”,你还一脸懵。Resilience4j 和 Sentinel 都有事件和指标可以接进监控系统,花不了多少时间,别省这一步。

最后

说到底,熔断的本质就一句话:牺牲个别调用的成功率,保住整个系统不被拖死。它不解决下游为什么会挂,那是另一个话题,但它能保证下游挂掉的时候,你的系统还有得救。

建议你在测试环境把下游服务直接 kill 掉,然后盯着日志看熔断器从 closed 到 open 再到 half-open 的状态流转,亲手走一遍比看十篇文章都直观。反正我当时就是这么弄懂的。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放