限流逻辑必须放在反向代理的请求入口层,即自定义http.Handler的ServeHTTP方法中,在调用ReverseProxy.ServeHTTP前完成拦截与判断,不可置于Transport或RoundTrip层面。

限流逻辑该放在反向代理的哪一层?
Go 的 net/http/httputil 提供的 ReverseProxy 本身不处理限流,必须在请求进入代理前拦截——也就是在 http.Handler 的 ServeHTTP 中做。绕过这层直接包装 transport 或 roundtripper,会导致限流统计失效(比如重试、连接复用会绕过计数)。
- 正确位置:自定义
http.Handler,先调用限流器Allow()或Reserve(),再交给ReverseProxy.ServeHTTP() - 错误做法:在
http.Transport的RoundTrip方法里加限流——无法区分客户端 IP 或 API 路径,且对失败重试无感知 - 注意
ReverseProxy默认会修改请求头(如删除Connection),限流判断需在它之前完成,否则原始X-Forwarded-For可能已被覆盖
用 golang.org/x/time/rate 还是第三方限流库?
golang.org/x/time/rate 足够轻量且线程安全,适合单机 QPS 限流;但若需分布式限流(如按用户 ID 全局限频)、滑动窗口或令牌桶动态配置,就得换方案。别一上来就引入 redis 或 sentinel——多数内部微服务压根不需要跨实例共享状态。
- 单机限流:用
rate.Limiter,每秒 100 请求就写rate.NewLimiter(100, 100)(burst 设为和 rate 相同可应对突发) - 按路径限流:用
sync.Map存不同rate.Limiter实例,key 是r.URL.Path,避免所有路径共用一个桶 - 按 IP 限流:从
r.Header.Get("X-Real-IP")或r.RemoteAddr提取客户端地址,注意 Nginx 转发时要配proxy_set_header X-Real-IP $remote_addr; - 别用
time.Sleep模拟限流——会阻塞 goroutine,吞吐暴跌
如何返回标准的限流响应而不中断代理流程?
限流触发时不能 panic 或直接 writeHeader(429),因为 ReverseProxy 的 responseWriter 已被封装,需用 httputil.NewSingleHostReverseProxy 的 ErrorHandler 钩子接管错误流,或更稳妥地——自己构造 response 并提前 return。
- 推荐方式:在 handler 开头检查限流,失败则调用
w.WriteHeader(429)+w.Write([]byte("Too Many Requests")),然后 return,不执行后续 proxy - 必须设置
Retry-After头:用limiter.Reserve().Delay()获取等待时间,转成秒级整数填入 header - 别依赖
ReverseProxy.ErrorHandler处理限流错误——它只捕获 transport 层错误(如后端不可达),不捕获业务逻辑拒绝 - 如果用了 gin 或 echo 等框架,确保限流中间件注册顺序在 proxy handler 之前
为什么本地测试限流总不准?
本地用 ab 或 hey 压测时,QPS 显示远超设定值,大概率是限流器没绑定到真实请求维度,或者 burst 参数设得过大。
立即学习“go语言免费学习笔记(深入)”;
- 检查是否误用全局单例
rate.Limiter:所有请求共用一个桶,实际是“全站总 QPS”而非“每 IP 每秒” - burst 过大(比如设成 1000)会让短时峰值完全绕过限流,建议 burst ≤ rate × 2
- 用
curl -H "X-Real-IP: 192.168.1.100"手动指定 IP 测试,避免 localhost 被识别为同一来源 - Go 的
rate.Limiter是基于时间的,系统时间跳变(如 NTP 同步)会导致短暂失效,生产环境用容器需挂载/etc/timezone并禁用 chrony 跳变
限流不是加个中间件就完事——关键在粒度选择、状态隔离和错误透传。漏掉任意一环,都可能让保护机制变成摆设。


















