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

熔断 的十种打开方式:工程实践笔记

之前一直觉得熔断这东西,面试背背“三态转换”就够了,实际项目里谁真配啊。直到上个月一个边缘的推荐服务抽风,RT 从 20ms 飙到 3s,把上游订单详情页的线程池打满,最后连网关一起躺下,整个站凉了二十分钟。复盘会上大家齐刷刷说“要有熔断就没这事儿”,然后发现熔断这玩意儿全公司没一处真正落地。于是花了两个周末把能用的姿势挨个摸了一遍,记录如下,省得下次事故再背锅。

一、先自己撸一个最小的,理解三态是怎么回事

框架先不谈,手写一个最简版,closed / open / half-open 一眼就能看明白:

import time

class Breaker:
    def __init__(self, fail_threshold=5, reset_timeout=30):
        self.fail_threshold = fail_threshold
        self.reset_timeout = reset_timeout
        self.failures = 0
        self.opened_at = 0

    def allow(self):
        if self.failures < self.fail_threshold:
            return True
        # 冷却时间过了,放一个请求出去试探(半开)
        return time.time() - self.opened_at > self.reset_timeout

    def on_success(self):
        self.failures = 0

    def on_failure(self):
        if self.failures == 0:
            self.opened_at = time.time()
        self.failures += 1

逻辑就这么点东西。真实实现要处理并发、滑动窗口、慢调用统计,所以这个别拿去生产,生产直接用下面现成的。

二、Spring 项目:Resilience4j 注解版,最省事

版本号不写死了,没准儿你看的时候已经是新的大版本,自己挑个最新的:

@CircuitBreaker(name = "orderService", fallbackMethod = "getOrderFallback")
public Order getOrder(String id) {
    return orderClient.get(id);
}

// 签名必须和原方法一致,末尾多一个 Throwable
public Order getOrderFallback(String id, Throwable t) {
    return orderCache.getOrDefault(id, Order.EMPTY);
}

对应的 application.yml:

resilience4j:
  circuitbreaker:
    instances:
      orderService:
        slidingWindowSize: 20
        failureRateThreshold: 50          # 失败率过 50% 就开闸
        waitDurationInOpenState: 30s
        permittedNumberOfCallsInHalfOpenState: 3

fallbackMethod 这个签名问题我第一次就踩了:参数对不上它不报错,只是静默不走 fallback,抛出来的异常看着跟没配熔断一样,查了好一会儿才发现是参数列表抄错了一个。

三、参数别写死,放到配置中心

熔断阈值这东西,上线第一天拍脑袋定的,基本一周内就得改。把上面那段 yml 挪到 Nacos,配置变更后 POST /actuator/refresh 一下就行,不用重启。调试的时候把 actuator 自带的 /actuator/circuitbreakers 打开,当前是 CLOSED 还是 OPEN 一目了然,别靠猜。

四、Feign 的 fallback,调用方的第一道防线

@FeignClient(name = "order", path = "/orders", fallback = OrderClientFallback.class)
public interface OrderClient {
    @GetMapping("/{id}")
    Order get(@PathVariable("id") String id);
}

@Component
public class OrderClientFallback implements OrderClient {
    @Override
    public Order get(String id) {
        return orderCache.getOrDefault(id, Order.EMPTY);
    }
}

两个前提:classpath 里要有 spring-cloud-starter-circuitbreaker-resilience4j,以及 feign.circuitbreaker.enabled: true。这个开关默认是关的,不打开 fallback 永远不会被调,而且没有任何提示,别问我怎么知道的。

五、要配的点太多时:Sentinel

项目里要熔断的点超过十几个、还想随时调,Sentinel 的 dashboard 值得上:

@SentinelResource(value = "getOrder", fallback = "getOrderFallback")

坑就一个:它的规则默认存在内存里,应用一重启全没。规则要落到 Nacos/Apollo 做数据源持久化,不然你会天天疑惑“昨天配的规则呢”。

六、Go 项目:sony/gobreaker

cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
    Name:     "order",
    Interval: time.Minute,        // 计数窗口
    Timeout:  30 * time.Second,   // open 状态持续多久
    ReadyToTrip: func(c gobreaker.Counts) bool {
        return c.Requests >= 10 &&
            float64(c.TotalFailures)/float64(c.Requests) > 0.5
    },
})

order, err := cb.Execute(func() (interface{}, error) {
    return getOrder(ctx, id)
})

吐槽一句:Execute 返回的是 interface{},外面还得自己断言一次类型,不太优雅,但胜在够小够稳,没别的毛病。

七、Istio / Envoy:让网格帮你摘坏实例

DestinationRule 里加一段:

trafficPolicy:
  outlierDetection:
    consecutive5xxErrors: 5
    interval: 10s
    baseEjectionTime: 30s
    maxEjectionPercent: 50

注意这和代码里的熔断器不是一回事:outlierDetection 是把个别坏实例踢出负载均衡池,代码里的熔断器是“下游整体不行了我就不调了”。一个是掐烂苹果,一个是整筐退货,两个都有才安心,别以为上了网格就完事了。

八、熔断之后的兜底,才是用户真正看到的东西

熔断只是把闸拉了,返回什么决定体验。按优先级排:

  1. 本地缓存里的旧数据,旧总比没有强
  2. 一个写死的默认值或空对象
  3. 静态骨架屏,前端配合在接口失败时读 localStorage 里上一次的响应

fallback 里千万不要再调用同一个下游,那就成套娃了;也别在 fallback 里干重活,它得在最坏情况下还跑得动。

九、验证熔断真的生效,这步别省

配完不测等于没配。用 toxiproxy 直接注入故障:

toxiproxy-cli create order --listen 127.0.0.1:9001 --upstream 127.0.0.1:8000
toxiproxy-cli toxic add order -t timeout -a timeout=0

把调用方的地址指到 9001,然后观察三件事:失败几次后日志里出现熔断打开、请求被快速失败(RT 掉到微秒级)、冷却时间到了之后半开放流量试探并恢复。懒的话 kill -9 下游进程也行,土但管用。

十、几个真金白银换来的坑

十种都列在这了,真用起来一个项目两三招就够:常规 Spring 项目二 + 四 + 九,Go 服务六 + 九,上了网格再叠个七。剩下的场景等真遇到了再回来翻。反正自打那次事故之后,我们组 review 任何调用外部服务的代码,第一句都是:它熔断了吗?

评论

还没有评论。

发表评论

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

未在播放