熔断与降级
问题
微服务中如何防止雪崩?Go 有哪些熔断限流方案?
答案
雪崩效应
服务 C 故障 → B 等待超时 → B 的 goroutine 堆积 → B 也不可用 → A 级联失败。
熔断器模式
| 状态 | 说明 |
|---|---|
| Closed(关闭) | 正常放行,统计错误率 |
| Open(打开) | 直接拒绝,快速失败 |
| HalfOpen(半开) | 放一个请求探测,决定恢复或继续熔断 |
Go 熔断实现
import "github.com/sony/gobreaker"
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "user-service",
MaxRequests: 3, // 半开状态最多放 3 个请求
Interval: 10 * time.Second, // 统计窗口
Timeout: 30 * time.Second, // Open → HalfOpen 等待时间
ReadyToTrip: func(counts gobreaker.Counts) bool {
// 错误率 > 60% 触发熔断
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 10 && failureRatio >= 0.6
},
OnStateChange: func(name string, from, to gobreaker.State) {
log.Printf("熔断器 %s: %s → %s", name, from, to)
},
})
// 使用
result, err := cb.Execute(func() (interface{}, error) {
return callUserService()
})
if err == gobreaker.ErrOpenState {
// 熔断中,走降级逻辑
return fallbackResponse()
}
限流
令牌桶(golang.org/x/time/rate):
import "golang.org/x/time/rate"
// 每秒 100 个请求,最多突发 200 个
limiter := rate.NewLimiter(100, 200)
func RateLimitMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if !limiter.Allow() {
c.AbortWithStatusJSON(429, gin.H{"error": "too many requests"})
return
}
c.Next()
}
}
滑动窗口限流:
// 基于 Redis 的滑动窗口
func SlidingWindowLimit(ctx context.Context, rdb *redis.Client, key string, limit int64, window time.Duration) bool {
now := time.Now().UnixMilli()
pipe := rdb.Pipeline()
// 移除窗口外的记录
pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(now-window.Milliseconds(), 10))
// 添加当前请求
pipe.ZAdd(ctx, key, redis.Z{Score: float64(now), Member: now})
// 统计窗口内请求数
countCmd := pipe.ZCard(ctx, key)
pipe.Expire(ctx, key, window)
pipe.Exec(ctx)
return countCmd.Val() <= limit
}
降级策略
func getUserProfile(userID string) (*UserProfile, error) {
// 1. 尝试调用远程服务
profile, err := userService.GetProfile(userID)
if err == nil {
// 存入缓存
cache.Set(userID, profile)
return profile, nil
}
// 2. 降级:从缓存读取
if cached, ok := cache.Get(userID); ok {
log.Warn("user-service 降级,使用缓存")
return cached, nil
}
// 3. 兜底:返回默认值
return &UserProfile{Name: "用户", Avatar: defaultAvatar}, nil
}
常见面试问题
Q1: 熔断和限流的区别?
答案:
| 维度 | 熔断 | 限流 |
|---|---|---|
| 目的 | 保护下游(不调崩别人) | 保护自身(不被流量打垮) |
| 触发条件 | 下游错误率高 | 请求超过阈值 |
| 作用位置 | 调用方 | 被调用方 |
| 响应 | 快速失败 + 降级 | 429 Too Many Requests |
Q2: sentinel-go vs gobreaker?
答案:
- gobreaker:轻量,只做熔断,API 简单,适合需求简单的场景
- sentinel-go:阿里开源,功能全面(限流 + 熔断 + 热点 + 系统保护),有 Dashboard
小项目用 gobreaker + x/time/rate,大型微服务用 sentinel-go。