Go限流首选rate.Limiter,需全局复用并按IP/path分桶;熔断须先限流后熔断,配合精准失败判定与指标监控。

Go 限流用 golang.org/x/time/rate 就够,别自己写令牌桶
标准库 rate.Limiter 是最轻量、最可靠的选择,它基于滑动窗口+令牌桶混合逻辑,线程安全,且不依赖外部存储。自己手撸计数器或 Redis 计数容易漏掉并发竞争、时钟漂移、服务重启丢失状态等问题。
常见错误是把 rate.NewLimiter 放在 handler 内部每次新建——这会让限流完全失效,因为每个请求都拿到全新 limiter。必须全局复用一个实例,按 IP 或用户 ID 做 key 分桶。
- 限流粒度建议用
net.ParseIP(r.RemoteAddr)提取真实 IP,但要注意反向代理场景下需读X-Forwarded-For并校验可信跳数 -
rate.Every(100 * time.Millisecond)比写rate.Limit(10)更直观,后者容易误算成“每秒 10 次”而实际是“每 100ms 1 次” - 别用
Allow()判断后才处理请求——它不阻塞,可能在高并发下瞬间打穿阈值;改用Wait(ctx),配合超时控制更稳
熔断不能只靠 github.com/sony/gobreaker,得配失败判定逻辑
gobreaker 本身只是状态机框架,真正决定“什么时候该熔断”的是你的 cb.Execute 里怎么定义失败——HTTP 5xx?还是包含 429 和连接超时?甚至是否把慢请求(如 >2s)也计入失败?这些全由你传入的函数决定。
典型坑是把整个 HTTP 请求体直接塞进 cb.Execute,结果一次超时就触发熔断,但其实下游只是临时抖动。更合理的做法是:只对明确的业务错误码(如 status == 503 || err != nil)计数,且设置 MaxRequests: 3 防止冷启动雪崩。
立即学习“go语言免费学习笔记(深入)”;
- 熔断恢复期(
Timeout)别设太短,至少 30 秒起,否则反复开闭会加剧下游压力 - 开启半开状态后,第一次试探请求必须带
context.WithTimeout,避免试探本身拖垮恢复流程 - 不要在中间件里无差别 wrap 所有 handler——数据库查询、本地缓存这类低风险操作没必要熔断
IP 限流 + 熔断组合时,顺序错了会绕过防护
必须先限流、再熔断。如果反过来,熔断打开后所有请求走 fallback,限流器根本收不到流量,IP 统计就停摆,等熔断关闭瞬间大量请求涌进来直接击穿。
另一个常见错是把两者都放在 Gin 的 Use() 全局中间件里,但没做路径过滤——健康检查接口 /healthz 被限流或熔断,K8s 就会不断重启 Pod。
- 推荐结构:
限流中间件 → 路由匹配 → 熔断包装具体 handler - 限流 key 建议用
hash(ip + path)而非纯 IP,避免 /login 这种高频路径拖垮整个 IP 的其他请求 - 熔断器命名要带业务标识,比如
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{Name: "user-service-call"}),否则日志和 metrics 无法区分
生产环境必须暴露指标,否则等于没加
限流拒绝数、熔断开启次数、半开试探成功率——这些数据不打点,你就永远不知道策略是否生效,还是在误杀正常流量。
别用 fmt.Println 或 log.Printf 打印,它们没法聚合。直接集成 prometheus/client_golang,为每个限流器注册 prometheus.NewCounterVec,key 包含 IP 段前缀(如 192.168.1.)和路径模板(如 /api/v1/user/:id)。
- 限流器拒绝时调用
counter.WithLabelValues("blocked", ipPrefix, pathTpl).Inc() - 熔断器状态变更时,用
Gauge记录当前状态(0=close, 1=open, 2=half-open) - 注意
rate.Limiter本身不暴露内部计数,你需要包装一层,在Wait()失败时手动打点
IP 限流和熔断本身不难,难的是让它们在真实流量里可观察、可调试、不互相干扰。一旦漏掉指标采集或者执行顺序,上线后基本等于盲操。










