Files
zhuangang/topics/backend/grpc-microservices/examples/resilience/resilience.go
T

120 lines
2.9 KiB
Go

// Package resilience 服务调用的三件套:超时(由调用方的 context 控制)、带退避的重试、熔断器。只用标准库。
package resilience
import (
"context"
"errors"
"math/rand/v2"
"sync"
"time"
)
// ErrOpen 熔断器打开时直接返回,不再调用下游
var ErrOpen = errors.New("熔断中:下游暂时不可用")
// Permanent 包装不应该重试的错误(参数错误、没有权限、资源不存在等)
type Permanent struct{ Err error }
func (p Permanent) Error() string { return p.Err.Error() }
func (p Permanent) Unwrap() error { return p.Err }
// Retry 最多尝试 attempts 次,间隔指数增长并加随机抖动;context 取消或超时立即停止
func Retry(ctx context.Context, attempts int, base time.Duration, fn func(context.Context) error) error {
var err error
for i := range attempts {
if err = fn(ctx); err == nil {
return nil
}
var p Permanent
if errors.As(err, &p) || errors.Is(err, ErrOpen) || i == attempts-1 {
return err
}
backoff := base << i // base, 2*base, 4*base ...
backoff += time.Duration(rand.Int64N(int64(backoff)/2 + 1)) // 抖动,避免所有客户端同时重试
select {
case <-ctx.Done():
return errors.Join(err, ctx.Err())
case <-time.After(backoff):
}
}
return err
}
type state int
const (
closed state = iota // 正常放行
open // 熔断:直接拒绝
halfOpen // 试探:放一个请求过去
)
// Breaker 连续失败 threshold 次后打开,cooldown 之后进入半开,试探成功则关闭、失败则重新打开
type Breaker struct {
mu sync.Mutex
state state
failures int
threshold int
cooldown time.Duration
openedAt time.Time
probing bool
now func() time.Time // 测试时可以替换
}
func NewBreaker(threshold int, cooldown time.Duration) *Breaker {
return &Breaker{threshold: threshold, cooldown: cooldown, now: time.Now}
}
func (b *Breaker) allow() bool {
b.mu.Lock()
defer b.mu.Unlock()
switch b.state {
case open:
if b.now().Sub(b.openedAt) < b.cooldown {
return false
}
b.state = halfOpen
fallthrough
case halfOpen:
if b.probing {
return false // 半开状态只放一个试探请求
}
b.probing = true
return true
}
return true
}
func (b *Breaker) record(err error) {
b.mu.Lock()
defer b.mu.Unlock()
var p Permanent
failed := err != nil && !errors.As(err, &p) // 业务错误不算下游故障
if b.state == halfOpen {
b.probing = false
if failed {
b.state, b.openedAt = open, b.now()
} else {
b.state, b.failures = closed, 0
}
return
}
if !failed {
b.failures = 0
return
}
b.failures++
if b.failures >= b.threshold {
b.state, b.openedAt = open, b.now()
}
}
// Do 通过熔断器调用 fn
func (b *Breaker) Do(ctx context.Context, fn func(context.Context) error) error {
if !b.allow() {
return ErrOpen
}
err := fn(ctx)
b.record(err)
return err
}