Fiber 的全局中间件不适合做限流,因为它使所有路由共享同一令牌桶,无法按下游服务容量差异(如支付20 QPS、用户500 QPS)独立控制,易压垮弱服务或浪费强服务资源。

不能用 app.Use() 统一配限流中间件——它会让所有路由共享同一个令牌桶,而真实下游服务的容量差异极大,比如支付接口可能只能扛 20 QPS,用户查询却能处理 500 QPS。混用会压垮弱服务或浪费强服务资源。
为什么 Fiber 的全局中间件不适合做限流
Fiber 的 app.Use() 或 app.Get("/xxx", ...) 路由级中间件,本质是路径匹配后统一执行同一段逻辑。一旦你在里面调用 sentinel.Entry() 或 rate.Limit(),所有匹配请求都会塞进同一个限流器实例。这违背了「按下游服务隔离」的基本原则。
常见错误现象包括:
- 对
/api/v1/payment和/api/v1/users设置同一阈值,结果支付服务频繁 429,用户服务却空转 - 误用
c.Path()生成 resource name(如GET:/api/v1/users/123),导致每条 ID 请求都被视为新资源,限流规则完全失效 - 忘记
defer entry.Exit(),造成连接泄漏或指标不准
正确做法:在 service 层按下游服务实例化限流器
限流入口必须落在封装好的业务函数里,比如 paymentService.Charge() 开头就检查自己的 rate.Limiter,而不是在 HTTP handler 里统一拦。
每个第三方服务对应一个独立的 http.Client + 独立的 rate.Limiter,二者参数需数值对齐(如超时 3s、连接池 maxIdle=20、QPS=20)。
资源名必须用静态路径模板,推荐写法:
if route := c.Route(); route != nil {
resourceName := fmt.Sprintf("%s:%s", c.Method(), route.Path)
entry, err := sentinel.Entry(resourceName, sentinel.WithTrafficType(base.Inbound))
if err != nil {
return c.Status(429).SendString("rate limited")
}
defer entry.Exit()
}
注意:
- Fiber v2.48 及更早版本中
c.Route()可能返回nil,需 fallback 到预设常量(如"POST:/api/v1/orders") - 务必拼入 HTTP 方法,
GET:/api/v1/orders和POST:/api/v1/orders是两个独立 resource - 绝对不要带 query 参数(如
?trace_id=abc),否则 resource 名不可枚举
限流失败后必须记录日志并提供 fallback
仅判 err != nil 就直接返回 500 是典型错误。这会导致两件事无法追踪:一是没记录被限流日志,你根本不知道策略是否合理;二是没 fallback,用户看到的是服务器错误而非明确提示。
建议写法:
entry, err := sentinel.Entry(resourceName)
if err != nil {
log.Warn().Str("resource", resourceName).Msg("rate limit exceeded")
return c.Status(429).JSON(map[string]string{
"error": "too many requests",
"retry_after": "60",
})
}
defer entry.Exit()
关键点:
- 用
log.Warn()显式记录被限流事件,方便后续分析策略宽松度 - 返回标准
429+Retry-Afterheader(如果限流器支持),前端可据此控制重试节奏 - 避免 panic、忽略 error 或吞掉异常,这会让问题沉底,直到监控报警炸锅才暴露
真正难的不是写几行限流代码,而是把「下游服务粒度」这个抽象概念落到每个 service 初始化、每个 http.Client 配置、每个 resource name 构造里——漏掉任意一环,限流就形同虚设。

















