用令牌桶给函数接口加限流保护,核心思路就是:维护一个按固定速率补充令牌的桶,每次请求需要取走一个令牌,桶里没令牌就直接拒绝或排队,这样能把流量平滑地控制在设定速率内。
为什么函数接口需要限流:从一次雪崩说起
前几天一个朋友跟我吐槽,他写的一个爬虫服务,平时跑得好好的,结果某次目标网站搞活动,流量猛增十倍,他的接口瞬间被击穿,数据库连接池全部打满,整个服务卡死,最后不得不手动重启,这个场景你应该不陌生接口没有限流,就像没装保险丝的电表,电流一大必然烧毁。
限流保护在业内早已是标配,行业共识认为,凡是暴露给外部或内部高频调用的函数接口,都应该考虑限流措施,尤其是微服务架构下,一个接口被上游多个服务调用,任何一个调用方出问题都可能拖垮你,据不完全统计,很多中小团队在接口上线初期都不加限流,等到出事故才回头补,这是非常被动的。
令牌桶算法到底是怎么工作的:用发券思维一次性看懂
令牌桶特别适合理解:想象一个景区门口有个保安,手里拿着一个桶,桶里装满代币,每隔固定时间,保安往桶里扔一个代币,桶满了就扔不进去,游客进景区必须交一个代币,没有代币就得在外面等着或直接不让进。
对应到代码里:令牌就是许可,桶就是缓冲区,生成速率就是平均QPS上限,桶容量就是允许的瞬时突发流量,举个例子,你设定每秒生成10个令牌,桶容量为20,那么接口长期平均每秒最多处理10个请求,但短时间可以冲到20个,因为桶里攒了20个令牌。
这跟漏桶算法有本质区别,漏桶是固定速率流出,不管进来多少,出去都是匀速,突发流量直接被削平,令牌桶允许一定的突发流量,更适合大多数业务接口因为你的正常流量本来就有波峰波谷,完全削平反而影响体验。
用令牌桶给函数接口加一层简单限流保护:完整实现步骤
下面我用Go语言写一个最精简的版本,因为Go的golang.org/x/time/rate库内置了令牌桶实现,足够简单,如果你用Java,也有Guava的RateLimiter,思路完全一致。
第一步:引入限流器并初始化
import "golang.org/x/time/rate" var limiter = rate.NewLimiter(rate.Limit(10), 20) // 每秒10个令牌,桶容量20
这一行代码就完成了核心配置,第一个参数是生成速率,第二个参数是桶大小,这里有个实战经验:桶容量不要设成1,否则接口完全失去突发能力,稍微有点尖峰就会报错。
第二步:在接口函数入口处检查令牌
func MyAPI() (interface{}, error) {
if !limiter.Allow() {
return nil, fmt.Errorf("请求过多,请稍后重试")
}
// 正常业务逻辑
return doBusiness(), nil
}
Allow()方法会尝试从桶里取一个令牌,取到返回true,取不到返回false,这是最简单的做法,适合非关键路径,但要注意:Allow()取不到令牌直接拒绝,不会等待,如果你希望请求排队等待而不是立即失败,应该用Wait()或WaitN()。
// 等待直到获取令牌,或超时
ctx, cancel := context.WithTimeout(context.Background(), time.Second2)
defer cancel()
if err := limiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("服务繁忙,请稍后再试")
}
Wait()会阻塞直到有令牌,但你可以通过上下文设置超时时间,实际项目中我更推荐Wait(),因为它能平滑地把多余请求均匀分摊到后续时间,而不是一股脑全部拒绝,用户体验更好。
第三步:按用户或IP粒度隔离限流
如果你的接口被多个调用方共享,全局一个限流器不够公平,比如A服务每秒调用50次,B服务只调用5次,你们约定每个服务上限10QPS,那么A会占掉B的令牌。
这时候需要一个带key的限流器,简单做法是用sync.Map缓存多个限流器实例:
var limiters sync.Map
func getLimiter(key string) rate.Limiter {
if l, ok := limiters.Load(key); ok {
return l.(rate.Limiter)
}
l := rate.NewLimiter(rate.Limit(10), 20)
limiters.Store(key, l)
return l
}
调用时传入用户ID或调用方AppID,注意内存管理,如果key非常多,记得定期清理不活跃的限流器,否则有内存泄漏风险。
第四步:配合中间件或装饰器模式
如果你不想在每个函数里手动写检查逻辑,可以用装饰器统一处理,以Go语言为例,写一个包装函数:
func RateLimited(handler func(w http.ResponseWriter, r http.Request)) func(w http.ResponseWriter, r http.Request) {
return func(w http.ResponseWriter, r http.Request) {
if !limiter.Allow() {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
handler(w, r)
}
}
然后这样注册路由:
http.HandleFunc("/api/user", RateLimited(userHandler))
这样一来,限流逻辑和业务逻辑完全解耦,后续想换算法、改参数、加监控都很方便。
令牌桶和别的限流方案怎么选:对比后才知道最优解
很多新手会纠结:到底是令牌桶、漏桶还是滑动窗口?其实没有绝对最好,只有适不适合,业内专家指出,令牌桶是均衡性好、容错高的通用方案,因为它兼顾了平均速率限制和突发流量容忍,而固定窗口(时间窗口计数器)有个经典问题:在窗口边界处可能出现两倍流量尖峰,滑动窗口更精确但内存开销大,漏桶适合需要绝对平滑的场景,比如消息推送、日志写入。
我做了张表,方便你直观感受:
| 方案 | 允许突发流量 | 实现复杂度 | 适用场景 |
|---|---|---|---|
| 令牌桶 | 允许(桶容量可控) | 低 | 通用API接口,绝大多数业务 |
| 漏桶 | 不允许 | 低 | 消息队列消费、数据落盘 |
| 固定窗口 | 边界有漏洞 | 最低 | 简单限制,对精度不敏感 |
| 滑动窗口 | 基本不允许 | 中 | 对流量精度要求高的场景 |
对比下来你会发现,如果你的需求就是”给函数接口加一层简单限流保护”,令牌桶是性价比最高的选择,写代码不超过10行,效果却非常稳。
限流参数怎么定:从业务指标倒推
设置令牌桶参数不能拍脑袋,这里给一个可操作的思路:
- 先看你的接口正常峰值QPS,比如日志统计显示日常最高每秒50个请求。
- 再看依赖的下游能力,比如数据库连接池上限能支撑每秒100个查询,那你的接口上限就不该超过80QPS。
- 确定桶容量,一般是生成速率的2到5倍,比如速率10QPS,桶容量设20到50,这样既能吸收瞬时小高峰,又不会让大突发全部通过。
- 压测验证,用wrk或Jmeter压一下,观察限流生效后的响应时间和错误率,如果错误率过高,适当调大桶容量或降低速率。
一个常见误区是把速率设成和最大QPS完全相等,这会导致平时没流量时桶是满的,一旦突发流量来临,桶里积累的令牌瞬间全部放行,然后立刻进入限流状态,呈现一种”放行一波卡一批”的锯齿状流量,所以速率应该略低于下游能承受的峰值,桶容量用来缓冲短期内的小突发。
限流失败后的处理:不只是返回错误
拿到限流拒绝后,客户端该怎么做?很多人直接报错完事,其实有三种更好的策略:
- 等待重试:响应头加
Retry-After,告诉客户端多少秒后再试,HTTP 429状态码就是专门干这个的。 - 降级返回:如果接口有缓存或备选数据源,限流时直接走降级逻辑,保证主流程不中断。
- 异步处理:把请求写入队列稍后处理,适合不要求即时响应的场景,比如发送通知、生成报告。
这四种做法里,等待重试是最通用的,只要客户端支持标准HTTP语义,都能理解429和Retry-After。
在Kubernetes或云环境下的额外建议
如果你把函数接口部署在K8s里,建议配合HPA(水平自动扩展)一起用,限流是防御手段,扩容才是根治方法,当你发现限流频繁触发,说明当前副本数不够了,赶紧告警出来,而不是一味提高限流阈值。限流阈值拉太高,等于没限,风险全暴露给下游。
可以在网关层(如Nginx、API Gateway)做一层全局限流,然后在函数内部再做一层本地限流,两层配合的好处是:全局限流可以保护整个集群,本地限流能防止某个实例被热点打垮,两者参数不用完全一致,通常全局阈值略高于本地阈值总和,因为流量分布不均会打折扣。
Q&A:关于限流你还会问什么
令牌桶限流和熔断器是什么关系?
令牌桶限流是事前控制,限制单位时间内的请求量,熔断器是事后保护,当下游错误率达到阈值就快速失败,停止调用下游,二者互补:限流管”别来太多”,熔断管”来了也别打到坏的服务上”,实际项目中经常组合使用,先限流再接熔断。
给函数接口加限流会不会影响性能?
不会,令牌桶检查就是一次内存计数操作,耗时在纳秒级别,对接口整体性能的影响可以忽略不计,相比记录日志、序列化JSON这些常规操作,限流检查几乎不占资源,真正要注意的是不要用分布式限流(如Redis)去给每个函数调用做检查,那会引入网络开销。
令牌桶满了之后令牌还会生成吗?
会,令牌生成是独立的,和桶里有没有位置无关,桶满了之后新生成的令牌直接丢弃,但生成器一直运行,这样做的目的是,当突然来一波流量时,桶里始终有存储空间来积累令牌,而不是填满后停顿一段时间再重新开始。
限流这件事,看起来简单,但做对了能挽救服务于水火之中,用令牌桶给你的函数接口加一层保护,成本极低,收益却很实在至少下次流量突增时,你不需要再深夜爬起来重启服务器了。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/635841.html




