你的服务依赖数据库、Redis、别的 RPC。下游偶发抖一下,不该让上层跟着挂。重试兜瞬时错误,熔断在下游持续故障时快速失败,超时保证不无限等。三件合起来,下游挂了你的服务还能活。
1. 重试只兜瞬时错误
网络闪断、超时这类能自愈的才重试。业务错误(参数错、权限不足)重试没用,反而放大流量:
func withRetry(fn func() error, max int) error { var err error for i := 0; i < max; i++ { if err = fn(); err == nil { return nil } if errors.Is(err, ErrBusiness) { return err // 业务错不重试 } time.Sleep(backoff(i)) } return err }
2. 指数退避加抖动
每次等更久,避免重试风暴撞在一起:
func backoff(i int) time.Duration { base := 100 * time.Millisecond j := time.Duration(rand.Intn(100)) * time.Millisecond return base*time.Duration(1<<i) + j }
1<<i 是指数增长,随机 j 打散多个调用方的重试节奏。
3. 熔断挡持续故障
下游彻底挂了,重试只会堆更多超时请求、拖慢线程。用 sony/gobreaker 在连续失败到阈值时熔断,直接拒请求:
var cb = gobreaker.NewCircuitBreaker(gobreaker.Settings{ Name: "mysql-query", MaxRequests: 3, // 半开时放行的探测数 Timeout: 5 * time.Second, // open 持续多久 ReadyToTrip: func(c gobreaker.Counts) bool { return c.ConsecutiveFailures > 5 }, }) func callDB(ctx context.Context) (string, error) { return cb.Execute(func() (string, error) { return queryWithCtx(ctx) }) }
三态:closed 正常放行;连续失败超阈值转 open,请求直接失败不调下游;Timeout 后转 half-open,放几个探测,成功则恢复 closed。
4. 超时别无限等
每次下游调用带 context 超时,防止一个慢查询卡住整个请求:
func queryWithCtx(ctx context.Context) (string, error) { ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond) defer cancel() return db.QueryRowContext(ctx, "...").Text() }
超时返回 context.DeadlineExceeded,上层重试或走熔断。超时值按 P99 耗时定,留余量。
5. 在 service 层组合
handler 不直接碰这些,包在 service 的下游调用里:
func (s *OrderSvc) Pay(ctx context.Context, id string) error { return withRetry(func() error { _, err := callDownstream(ctx, id) // 内部带超时 + 熔断 return err }, 3) }
handler 只调 Pay,容错逻辑沉淀在 service,接口层干净。
6. 上线清单
重试只对幂等、瞬时错误,带退避和抖动,设最大次数。熔断阈值按正常错误率定,误判会误伤。超时值小于网关/客户端超时,避免上层已断你还在等。熔断和限流配合:限流防自己被冲,熔断防下游拖垮自己。监控熔断次数,频繁跳闸说明下游真有问题。
下一篇讲多租户,让一套服务服务多个客户而数据不串。