Go 微服务熔断:别等下游挂死才开始保护自己
Go 微服务熔断:别等下游挂死才开始保护自己
一、熔断不是失败后的装饰动作
Go 微服务调用下游时,最常见的防护是超时和重试。但下游持续错误时,只靠超时会让请求排队,只靠重试会把压力放大。熔断的价值,是在下游明显不可用时,暂时停止继续打它,让系统有喘气机会。
熔断不是为了让错误消失,而是让错误范围可控。该失败的请求快速失败,该降级的请求走备用路径。别等下游彻底挂死,才想起来保护自己。
一个典型的连环故障:服务 A 调用服务 B,服务 B 变慢到 5 秒超时。服务 A 的请求线程全在等 B 的响应,队列越堆越长。运维看到 A 响应慢,加机器扩容 A。新机器上的 A 继续发起请求,给 B 施加更大压力,B 直接 OOM 挂掉。然后 A 也 OOM,接着上游 C 也被拖垮。如果在 A 检测到 B 的错误率超过阈值时,立刻熔断对 B 的调用,允许快速失败或降级,以上所有连锁反应都不会发生。扩容加机器解决的是容量问题,不是下游不可用问题——这两者完全不同。
二、熔断器要有关闭、打开和半开三种状态
关闭状态正常请求;错误率过高进入打开状态,直接失败;等待一段时间后进入半开,放少量探测请求。
stateDiagram-v2 [*] --> Closed Closed --> Open: 错误率超过阈值 Open --> HalfOpen: 冷却时间结束 HalfOpen --> Closed: 探测成功 HalfOpen --> Open: 探测失败半开很关键。没有半开,系统不知道下游是否恢复;半开放量太大,又会把刚恢复的下游打回去。
半开状态的设计细节值得展开。半开时只放少量探测请求,通常是 1-3 个。如果探测成功(如连续 N 次成功),切回关闭状态;如果探测失败,立即回到打开状态,重新冷却。探测请求应该是正常业务请求而不是虚构的 health check——健康检查可能返回 200,但业务请求可能超时。用真实流量探测才能验证下游是否真的恢复。
三、Go 中把熔断判断放在调用前
type Breaker interface { Allow() error MarkSuccess() MarkFailure(error) } func CallWithBreaker(ctx context.Context, b Breaker, call func(context.Context) error) error { if err := b.Allow(); err != nil { return fmt.Errorf("circuit open: %w", err) } err := call(ctx) if err != nil { b.MarkFailure(err) return err } b.MarkSuccess() return nil }熔断判断必须在调用前。否则请求已经打到下游,再说熔断就晚了。生产里可以选用成熟的熔断库(hystrix-go、sony/gobreaker 等),但核心语义要理解:统计窗口(滑动窗口比固定窗口更灵敏)、错误率阈值、冷却时间、半开放量,这四个参数决定了熔断器是保护系统还是添乱。
四、熔断要区分错误类型
不是所有错误都应该计入熔断。参数错误、权限错误、业务拒绝,不代表下游不可用。网络错误、超时、5xx、连接池耗尽,才更像下游或链路问题。
还要给熔断配置按接口区分。下游一个低优先级接口错误,不应该熔断整个服务。粒度太粗会误伤,粒度太细又难管理。通常按下游服务加关键接口分组比较稳。
举个例子:订单服务有"创建订单"和"查询订单状态"两个接口。如果创建订单失败 30 次但查询一直正常,应该是只熔断创建订单接口而不是整个订单服务。但如果订单服务的数据库连接池耗尽,两个接口都会报错,此时熔断整个服务才是合理的。粒度选择依赖于你对故障模式的理解,不是一个全局常量。
最后,熔断要有告警和看板。打开次数、半开成功率、快速失败数都要能看到。熔断是止血,不是治病。它打开了,说明背后还有问题要修。
熔断还要和降级策略联动。读接口可以返回缓存,非核心写接口可以提示稍后重试,核心写接口则要谨慎失败。不同业务的降级方式不同,不要让熔断器只会返回一个通用错误。
配置也要灰度。错误率阈值、冷却时间、半开探测数都需要压测和线上观察。阈值太低会频繁误伤,阈值太高又起不到保护作用。熔断参数和限流参数一样,都是生产策略,不是随便填的常量。
最后,熔断打开时要带上原因。日志里记录最近错误类型、下游地址、请求量和触发阈值。排障时能少猜很多。熔断告警除了通知"熔断打开"外,还要附带触发统计数据,让值班的人一眼看到是 "5xx" 还是 "timeout" 导致熔断。
熔断还要避免全实例同步打开。所有实例同时检测到错误,同时打开,再同时半开探测,可能造成流量波峰。可以给冷却时间加一点随机抖动,让恢复更平滑。
如果服务有多个机房或多个下游副本,熔断状态最好按目标地址维护。某个区域故障,不应该影响其他健康区域。熔断粒度要和流量路由粒度一致。
五、总结
Go 微服务熔断要在下游持续异常前主动保护系统。关闭、打开、半开三个状态缺一不可,错误类型和接口粒度要分清。熔断不是把错误藏起来,而是让错误快速、可控、可观测地暴露出来。一个好的熔断器让系统"失败得优雅",而不是"死得难堪"。