Go 限流中间件:令牌桶之外还要看排队语义
Go 限流中间件:令牌桶之外还要看排队语义
一、限流不是简单拒绝请求
Go 服务常用令牌桶做限流。它实现简单、性能好,能控制瞬时流量。但在真实后端系统里,限流不只是“超过阈值就拒绝”。有些请求可以排队等一会,有些请求必须快速失败,有些请求应该按用户或租户隔离。
如果所有请求共用一个全局限流器,大客户的突发流量可能挤掉普通用户,低价值接口也可能影响核心接口。
二、限流要分层
flowchart TD A[请求] --> B[全局限流] B --> C[租户限流] C --> D[接口限流] D --> E[下游资源限流]全局限流保护进程,租户限流保护公平性,接口限流保护热点 API,下游资源限流保护数据库、缓存和第三方服务。不同层次的目标不同,不能用一个阈值解决所有问题。
排队语义也要明确。查询接口可以等待几十毫秒,支付或提交接口可能更适合快速失败并提示重试。后台任务可以进入队列,但队列长度必须有限。
三、代码里要表达超时
func Limit(next http.Handler, limiter *rate.Limiter) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 80*time.Millisecond) defer cancel() if err := limiter.Wait(ctx); err != nil { http.Error(w, "rate limited", http.StatusTooManyRequests) return } next.ServeHTTP(w, r) }) }Wait比Allow更温和,但必须带超时。没有超时的等待会把请求堆在服务内部,最终表现为延迟雪崩。
rate_limit: global_rps: 8000 tenant_rps: 300 wait_timeout_ms: 80 reject_status: 429配置要能按环境调整。压测环境、灰度环境和生产环境的阈值不同,不能把限流数字写死在代码里。
四、限流结果要进入观测
限流触发不是坏事,但限流持续触发就是容量信号。指标里应区分全局拒绝、租户拒绝、接口拒绝和下游拒绝。只有知道是哪一层在拒绝,才能决定扩容、优化接口还是调整套餐。
响应也要友好。对 API 用户返回稳定错误码和重试建议;对前端页面,可以展示稍后重试或降级数据。限流策略如果没有产品表达,用户只会觉得系统不稳定。
限流还要和幂等设计配合。被拒绝或超时的请求,用户可能会重试。如果接口没有幂等键,重试可能造成重复提交、重复扣费或重复创建任务。限流中间件负责控制进入系统的速度,业务层仍然要保证重复请求不会破坏数据一致性。
灰度上线时,可以先只记录不拦截,观察不同租户、接口和时间段的命中情况。确认阈值合理后,再逐步开启拦截。这样能避免一上线就把正常高峰当异常流量挡掉。
还要为内部调用单独建策略。很多服务雪崩不是外部流量打进来,而是内部定时任务、批处理或补偿任务突然放大。外部网关限流挡不住内部流量,服务间调用也要有配额和优先级。
五、总结
Go 限流中间件不能只实现一个令牌桶,还要定义分层限流、排队超时、公平性和观测指标。
限流的目标不是粗暴挡流量,而是在压力变大时让系统按业务优先级保持可控。
