服务器与大带宽专家 · 持牌IDC/CDN/ISP服务商
简米科技官网JIANMI TECH
资讯 2026-08-19 简米科技 4,277 字 10 分钟阅读

用令牌桶给函数接口加一层简单限流保护

导读给函数接口加限流保护,最直接有效的方式就是用令牌桶:它允许突发流量、平滑限速,在性能和可靠性之间取得平衡,且实现成本低,令牌桶为什么适合函数接口函数接口和传统Web API不同,它往往承载着更细粒度的业务逻辑调用,你不可能让每个函数都裸奔,尤其是涉及数据库读写、第三方支付、短信发送这类场景,限流不是为了让服务变……

给函数接口加限流保护,最直接有效的方式就是用令牌桶:它允许突发流量、平滑限速,在性能和可靠性之间取得平衡,且实现成本低。

令牌桶为什么适合函数接口

函数接口和传统Web API不同,它往往承载着更细粒度的业务逻辑调用,你不可能让每个函数都裸奔,尤其是涉及数据库读写、第三方支付、短信发送这类场景。限流不是为了让服务变慢,而是为了避免服务被拖垮

接口限流的本质是挡掉超出承载力的请求

业内专家指出,绝大多数接口故障并非代码逻辑错误,而是流量洪峰超出了系统设计容量,函数接口被高频调用时,CPU、内存、数据库连接池都会被快速消耗,如果不做任何保护,一次突发的刷单请求就能把整个服务打挂。

令牌桶的工作机制其实很朴素:系统以一个固定速率往桶里放令牌,每个请求进来先取一枚令牌,取到了就放行,桶空了就拒绝或排队,这个机制天然适合函数接口,因为函数调用频率通常有波动,令牌桶允许短时间的积压和突发,不会像固定窗口那样一刀切。

函数接口限流的核心指标

在落地之前,先想清楚三个问题:

  • QPS上限:这个接口正常情况下能扛多少并发。
  • 峰值容忍度:允许瞬时的请求突发多大,持续多久。
  • 超限策略:是直接拒绝、排队等待,还是降级返回默认值。

这三个问题对应令牌桶的三个核心参数:速率(rate)、容量(burst)、超限处理逻辑,参数定完之后,代码实现就是一个数据结构的事。

令牌桶限流和漏桶限流怎么选

很多人在选型时纠结于令牌桶和漏桶的区别,实际落地时两者的差距并不像理论看起来那么大。

对比维度 令牌桶算法 漏桶算法
允许突发流量 支持,桶容量决定突发上限 不支持,流出速率恒定
实现复杂度 低,一个计数器加时间戳即可 低,但需要队列配合
适用场景 函数接口、API网关、微服务 平滑流出场景,如消息队列消费
对流量波动的响应 允许突刺,随之平滑 完全削峰填谷

行业共识认为,函数接口场景下令牌桶的收益更高,原因很简单:

用令牌桶给函数接口加一层简单限流保护

用户请求天然带有随机性,业务上允许某些时刻瞬间涌来一批请求,只要总体速率可控就行,漏桶会把所有请求强制排队,虽然系统最安全,但用户体验会明显变差你排队等待的结果是接口响应延迟飙升。

如果非要说选型的边界,记住这句话:业务需要瞬时处理一批请求,选令牌桶;业务要求绝对均匀的输出速率,选漏桶,大多数函数接口属于前者。

接口限流方案怎么落地

确定用令牌桶之后,接下来就是代码实现,这里不推荐从零手写算法,成熟的语言库已经帮你处理好了边界情况,以Go语言的 golang.org/x/time/rate 为例,它是官方维护的令牌桶实现,性能很好。

单机场景下最简配置

import "golang.org/x/time/rate"
var limiter = rate.NewLimiter(rate.Limit(100), 200)
func ProtectFunc(ctx context.Context, req Request) error {
    if !limiter.Allow() {
        return errors.New("请求过于频繁,请稍后再试")
    }
    // 业务逻辑...
    return process(ctx, req)
}

上面这段代码,每秒生成100个令牌,桶容量200,意味着接口正常情况下每秒最多处理100个请求,但允许瞬时200个请求同时进来,第一个参数是速率,第二个参数是桶的大小,这个写法对于内部工具函数、定时任务回调、单实例部署的服务完全够用。

分布式场景下的令牌桶怎么处理

单机版本的缺点是令牌桶在内存里,多实例部署时各自独立,无法形成全局限流,这时需要把令牌桶搬到 Redis 上,用 Lua 脚本 保证原子性,伪代码如下:

-- 令牌桶的 Redis Lua 实现核心逻辑
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local tokens = redis.call("GET", key .. ":tokens")
if not tokens then tokens = capacity end
local lastRefill = redis.call("GET", key .. ":ts")
if not lastRefill then lastRefill = now end
local delta = math.max(0, now - lastRefill)
tokens = math.min(capacity, tokens + delta  rate)
if tokens >= requested then
    redis.call("SET", key .. ":tokens", tokens - requested)
    redis.call("SET", key .. ":ts", now)
    return 1
else
    return 0
end

这段脚本的特点是:判断和扣减在 Redis 内部原子完成

用令牌桶给函数接口加一层简单限流保护

,不会有并发扣减超卖的问题,实现上需要注意,每次请求都会消耗一次 Redis 的读写,如果接口本身 QPS 极高,Redis 的往返延迟会成为瓶颈,这时候可以考虑给本地做一个缓存副本,每隔几秒同步一次令牌数,牺牲一点精确度换取性能。

路由网关和代码层限流区别

这里有个选择问题:限流逻辑放在网关层还是业务代码里?

  • 网关层限流(如 Nginx 的 limit_req、云厂商的 API 网关):统一管控,不侵入业务代码,但对单个业务函数的针对性控制较弱。
  • 代码层限流(在函数入口手动加逻辑):灵活精准,能针对不同函数配置不同的速率,但需要每个函数都加一遍。

实际项目里通常结合使用,网关层做粗粒度的全局保护,代码层做细粒度的业务限流,比如支付回调接口需要严格控制频次,就在函数入口加一个 100 QPS 的令牌桶;而普通的查询函数,可能只在网关层做全局 QPS 限制就够了。

配置限流参数的实践经验

参数设多少合适?这个没有标准答案,但有一套可以遵循的推演路径。

从接口的支撑能力倒推

先压测你的函数接口,拿到单实例的吞吐上限,假设压测结果是单实例 500 QPS,部署了 3 个副本,理论集群上限 1500 QPS。限流阈值不要设到 1500,建议预留 20% 的缓冲,也就是 1200 QPS 左右,为什么留余量?因为压测数据是理想状态,真实请求有参数大小差异、有外部依赖耗时波动,不一定跑得满。

桶容量的大小怎么定

桶容量决定瞬时突发能力,普通业务接口建议桶容量设定为速率的 1-2 倍,比如速率 100 QPS,桶容量 150-200,如果业务存在批量操作场景,比如导入文件时一次性调用大量接口,桶容量可以适当放大到速率的 3-5 倍。

这里给一组参考配置:

接口类型 速率(QPS) 桶容量 超限处理
普通查询接口 500 800 直接拒绝
下单接口 200 300 排队 200ms
支付回调 100 150 直接拒绝
批量导入 50 250 延迟执行

限流之后怎么让用户感知到

限流不只是代码层面的事,还需要让调用方明确知道触发了限制,常用的做法是返回标准 HTTP 状态码

用令牌桶给函数接口加一层简单限流保护

429 Too Many Requests,并在响应头里带上 Retry-After 字段,告诉调用方多久之后可以重试,很多开源客户端库看到 429 会自动退避重试,这样做能显著减少重复请求对系统的二次压力。

限流效果监控与常见问题解答

限流上线后不是一劳永逸,需要观察实际触发次数和决策是否合理,建议在限流器上加一个计数器,记录每分钟被拒绝的请求数,如果这个数字长期为 0,说明限流阈值设置过高;如果长期超过总请求的 20%,说明阈值太紧,需要评估扩容或者调整参数。

做限流保护时容易疏忽的地方

  • 没有处理 Allow() 返回 false 时的降级逻辑,直接抛异常导致调用方报错。
  • 限流器创建在函数内部,每次调用都新建一个实例,导致限流完全失效。
  • 忽略了限流器本身的异常处理,Redis 不可用时直接 panic 导致服务雪崩。

限流保护常见问题解答

问:令牌桶限流放本地内存好还是放 Redis 好?

单实例部署且进程不频繁重启,用本地内存性能最高,延迟在纳秒级别;多实例共享流量且需要统一限制,用 Redis 方案,虽然每次请求多了一次网络开销,但换来了全局一致性,折中方案是本地内存做初步拦截,Redis 做二次精确控制。

问:限流和熔断有什么区别?

限流针对的是请求速率,不管系统健康与否,超过阈值就拦截;熔断针对的是下游依赖的健康状态,当下游错误率达到阈值时直接短路,不再发起请求,两者可以配合使用:限流保护上游入口,熔断保护下游依赖。

问:调大桶容量会不会导致系统压力瞬间过大?

理论上会,桶容量决定了允许的突发上线,但实际压力取决于填满桶的速度,即使桶容量设得很大,只要令牌生成速率还是原来的值,被放行的总请求量不会超过生成速率加上存量令牌的总和,所以调大桶容量不会导致无限的突发流量,它只是把之前积累的令牌释放出来,如果当下游系统已经接近瓶颈,把桶容量调小是更稳妥的选择。

收束

函数接口的限流本质上是给系统装一个安全阀,令牌桶算法用最小的代价实现了这个能力,定好速率、容量,配上合理的超限策略,再做好监控和参数调优,就能在绝大多数场景下起到保护作用。

分享本文
本文为 简米科技官网 原创,已由运维技术专家审核。转载请注明来源:原文链接
售前咨询 服务热线 售后 邮箱