熔断 的十种打开方式:工程实践笔记
之前一直觉得熔断这东西,面试背背“三态转换”就够了,实际项目里谁真配啊。直到上个月一个边缘的推荐服务抽风,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 是把个别坏实例踢出负载均衡池,代码里的熔断器是“下游整体不行了我就不调了”。一个是掐烂苹果,一个是整筐退货,两个都有才安心,别以为上了网格就完事了。
八、熔断之后的兜底,才是用户真正看到的东西
熔断只是把闸拉了,返回什么决定体验。按优先级排:
- 本地缓存里的旧数据,旧总比没有强
- 一个写死的默认值或空对象
- 静态骨架屏,前端配合在接口失败时读 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 下游进程也行,土但管用。
十、几个真金白银换来的坑
- 失败的定义要想清楚。 订单不存在这种业务 404 不该计入失败率,Resilience4j 里用
recordExceptions圈定范围,否则下游数据干净也能把你熔断开。 - 慢调用也是失败。
slowCallDurationThreshold记得配,只算异常不算慢的话,下游慢到 3s 你也熔不了,直接回到事故起点。 - 半开别放太多。
permittedNumberOfCallsInHalfOpenState设 1~3 就够,一窝蜂放进去等于拿半死的下游再压一轮测。 - 熔断保护的是自己,限流保护的才是下游。 这俩别指望一个顶俩。
- 指标要暴露出去。 Micrometer 接 Prometheus,看板放上状态和失败率。不看的熔断和没配一样,出事的时候你甚至不知道它在工作。
十种都列在这了,真用起来一个项目两三招就够:常规 Spring 项目二 + 四 + 九,Go 服务六 + 九,上了网格再叠个七。剩下的场景等真遇到了再回来翻。反正自打那次事故之后,我们组 review 任何调用外部服务的代码,第一句都是:它熔断了吗?
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。