Go 限流
Go 服务中的限流方案,涵盖令牌桶与漏桶算法、golang.org/x/time/rate、HTTP 中间件集成与分布式限流。
#type / concept
#status / growing
#tech / dev
#resource / go
[!info] related notes
- 所属 MOC: Go 服务工程
- 前置概念: Go Context
- 并列概念: Go 缓存策略, Go 认证与 JWT
Go 限流
一句话定义
Go 限流是通过令牌桶或漏桶算法控制请求速率,保护服务不被过载的防御机制,标准库扩展包 golang.org/x/time/rate 提供了开箱即用的实现。
核心机制 / 工作原理
令牌桶(Token Bucket)算法:
- 桶以固定速率
r产生令牌,桶容量为b(突发上限)。 - 每个请求消耗一个令牌,桶空则拒绝或等待。
- 允许突发流量(桶满时可瞬间消耗
b个请求),但长期速率不超过r。
漏桶(Leaky Bucket)算法:
- 请求进入桶(队列),桶以固定速率处理。
- 桶满则拒绝新请求。
- 输出速率恒定,完全平滑流量。
两者核心区别:令牌桶允许突发,漏桶强制平滑。多数场景推荐令牌桶。
golang.org/x/time/rate(标准实现):
import "golang.org/x/time/rate"
// 每秒 100 个请求,突发最多 200 个
limiter := rate.NewLimiter(100, 200)
// 等待直到有令牌(阻塞)
err := limiter.Wait(ctx)
// 尝试获取令牌(非阻塞)
if !limiter.Allow() {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
// 预留令牌(带超时)
ctx, cancel := context.WithTimeout(r.Context(), time.Second)
defer cancel()
if err := limiter.WaitN(ctx, 1); err != nil {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
HTTP 中间件集成:
func RateLimitMiddleware(limiter *rate.Limiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
w.Header().Set("Retry-After", "1")
http.Error(w, "too many requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
// 使用
limiter := rate.NewLimiter(100, 200)
mux := http.NewServeMux()
mux.Handle("/api/", RateLimitMiddleware(limiter)(apiHandler))
Per-User 限流:
type UserRateLimiter struct {
limiters map[string]*rate.Limiter
mu sync.Mutex
rate rate.Limit
burst int
}
func NewUserRateLimiter(r rate.Limit, burst int) *UserRateLimiter {
return &UserRateLimiter{
limiters: make(map[string]*rate.Limiter),
rate: r,
burst: burst,
}
}
func (l *UserRateLimiter) GetLimiter(userID string) *rate.Limiter {
l.mu.Lock()
defer l.mu.Unlock()
if lim, exists := l.limiters[userID]; exists {
return lim
}
lim := rate.NewLimiter(l.rate, l.burst)
l.limiters[userID] = lim
return lim
}
// 使用
userLimiter := NewUserRateLimiter(10, 20) // 每用户每秒 10 个,突发 20
limiter := userLimiter.GetLimiter(getUserID(r))
if !limiter.Allow() {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
}
注意:内存在增长,生产环境需要定期清理不活跃用户的 Limiter(LRU 或定时扫描)。
分布式限流(Redis):
// Redis 滑动窗口限流(Lua 脚本保证原子性)
const luaScript = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. math.random())
redis.call('EXPIRE', key, window / 1000)
return 1
end
return 0
`
script := redis.NewScript(luaScript)
allowed, _ := script.Run(ctx, rdb, []string{"rate:user:" + userID}, 100, 60000, time.Now().UnixMilli()).Int()
if allowed == 0 {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
}
最小例子 / 最小场景
// 全局限流:每秒 50 个请求,允许突发 100 个
limiter := rate.NewLimiter(50, 100)
mux := http.NewServeMux()
mux.HandleFunc("/api/resource", func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "too many requests", http.StatusTooManyRequests)
return
}
w.Write([]byte("OK"))
})
为什么重要
- 服务保护:防止恶意流量或突发流量压垮服务,维持可用性。
- 公平性:Per-User 限流防止单个用户占用全部资源。
- 成本控制:第三方 API(如支付网关)通常有调用频率限制,客户端限流可避免触发对方限制。
- 优雅降级:限流后返回 429 而非让服务崩溃或 OOM,是更好的用户体验。
边界与易混淆点
- 全局 vs Per-User:全局限流保护服务总容量,Per-User 限流保护公平性。两者应组合使用。
rate.Limiter是有状态的:内部维护令牌数量和最后更新时间,不能序列化到 Redis。分布式限流需要 Redis Lua 脚本或专用方案(如go-redis/redis_rate)。- Burst 的含义:
rate.NewLimiter(100, 200)中200是桶容量,不是”每秒允许 200 个”。它允许瞬间消耗 200 个令牌,之后恢复到每秒 100 个。 - 内存泄漏:Per-User 限流的 Map 会持续增长。必须加淘汰机制:LRU 缓存、TTL 过期、或定期清理。
- 429 响应头:应返回
Retry-After头告知客户端何时可以重试,配合指数退避(Exponential Backoff)效果最佳。 - 限流 vs 熔断:限流控制请求速率(输入端),熔断控制下游调用失败率(输出端)。两者解决不同问题,通常同时使用。