Appearance
27|限流与降级
网关是流量入口,后端服务的容量有限。突发流量、爬虫攻击或缓存击穿都可能导致后端被打满,响应变慢甚至崩溃。限流控制进入系统的请求速率,降级在资源不足时关闭非核心功能,保证核心链路可用。go-gateway 在代理层实现令牌桶限流,在管理 API 实现接口级限流。
一、限流算法
| 算法 | 原理 | 特点 |
|---|---|---|
| 固定窗口 | 每秒允许 N 个请求 | 简单,但窗口边界可能突发 2N 请求 |
| 滑动窗口 | 按时间滑动统计 | 更平滑,实现稍复杂 |
| 令牌桶 | 以固定速率生成令牌,请求消耗令牌 | 允许一定突发,最常用 |
| 漏桶 | 请求进入桶,以固定速率漏出 | 强制平滑,不支持突发 |
令牌桶是生产环境最常用的算法,它允许短时间内消耗积累的令牌(突发),但长期速率不超过生成速率。
二、单机限流
Go 标准库扩展 golang.org/x/time/rate 提供了令牌桶实现:
bash
go get golang.org/x/time/ratego
package main
import (
"context"
"fmt"
"net/http"
"time"
"golang.org/x/time/rate"
)
var limiter = rate.NewLimiter(rate.Limit(10), 20) // 每秒10个,桶容量20
func rateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}rate.NewLimiter(10, 20) 表示每秒生成 10 个令牌,桶最多存 20 个。突发时最多一次处理 20 个请求,之后按每秒 10 个的速率放行。
等待令牌(而不是直接拒绝):
go
ctx, cancel := context.WithTimeout(r.Context(), 100*time.Millisecond)
defer cancel()
if err := limiter.Wait(ctx); err != nil {
http.Error(w, "timeout", http.StatusTooManyRequests)
return
}Wait 会阻塞直到拿到令牌或 context 超时。适合对延迟不敏感、希望尽量处理请求的场景。
三、接口级限流
不同接口的容量不同。/api/users 可能能扛 1000 QPS,/api/report 只能扛 10 QPS。按路径分别限流:
go
type RateLimiter struct {
limiters map[string]*rate.Limiter
mu sync.RWMutex
}
func (rl *RateLimiter) GetLimiter(path string, r rate.Limit, burst int) *rate.Limiter {
rl.mu.RLock()
lim, ok := rl.limiters[path]
rl.mu.RUnlock()
if ok {
return lim
}
rl.mu.Lock()
defer rl.mu.Unlock()
lim, ok = rl.limiters[path]
if ok {
return lim
}
lim = rate.NewLimiter(r, burst)
rl.limiters[path] = lim
return lim
}Gin 路由组中间件按路径限流:
go
admin.Use(func(c *gin.Context) {
path := c.Request.URL.Path
lim := limiterManager.GetLimiter(path, 10, 20)
if !lim.Allow() {
c.AbortWithStatusJSON(429, gin.H{"error": "rate limit exceeded"})
return
}
c.Next()
})四、熔断与降级
限流是从入口控制流量。熔断是从出口保护——后端服务连续出错时,停止向其转发请求,避免把后端拖垮。
熔断器有三种状态:
| 状态 | 行为 |
|---|---|
| Closed | 正常转发请求 |
| Open | 后端异常,直接返回错误,不再转发 |
| Half-Open | 等待一段时间后,放少量请求试探后端是否恢复 |
go-gateway 的简易熔断实现:
go
type CircuitBreaker struct {
failures int
threshold int
lastFailure time.Time
cooldown time.Duration
state string // closed, open, half-open
mu sync.Mutex
}
func (cb *CircuitBreaker) Call(fn func() error) error {
cb.mu.Lock()
defer cb.mu.Unlock()
if cb.state == "open" {
if time.Since(cb.lastFailure) > cb.cooldown {
cb.state = "half-open"
} else {
return fmt.Errorf("circuit breaker open")
}
}
err := fn()
if err != nil {
cb.failures++
cb.lastFailure = time.Now()
if cb.failures >= cb.threshold {
cb.state = "open"
}
return err
}
cb.failures = 0
cb.state = "closed"
return nil
}生产环境用成熟的库如 github.com/sony/gobreaker 或 github.com/afex/hystrix-go。
五、降级响应
后端不可用时,网关返回预定义的降级响应,而不是把错误透传给客户端:
go
func fallbackResponse(w http.ResponseWriter, path string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "service temporarily unavailable",
"path": path,
})
}六、常见错误
限流阈值设置过高
限流阈值设成后端容量的 10 倍,等于没限流。阈值应该通过压测确定,留 20%-30% 余量。
全局一个限流器
所有接口共用一个限流器,某个接口被刷时,其他接口也受影响。应该按接口、按用户、按 IP 多维度限流。
不处理限流器的并发安全
go
var limiter = rate.NewLimiter(10, 20) // 这个本身是并发安全的rate.Limiter 内部有锁,是并发安全的。但自定义的限流管理器(如上面的 RateLimiter)需要自己加锁保护 map。