首页 / 帮助文档 / CC防护的Token桶算法与漏桶算法选择

CC防护的Token桶算法与漏桶算法选择

选Token桶还是漏桶,本质上是在选你的CC防护策略是“削峰填谷”还是“刚性限流”。很多人在配置WAF或网关时,面对这两个选项直接蒙圈,随便勾一个默认了事,结果要么误杀严重,要么被大流量直接打穿。要彻底搞明白怎么选,得先抛开那些晦涩的数学公式,从这两种算法在CC防护场景下的真实行为逻辑说起。

漏桶算法在CC防护中的强制平滑效应

漏桶算法的核心动作非常粗暴:强制整流。你可以把它想象成一个底部有个小洞的水桶,不管上游进水的水量多大,下游出水的速率是恒定不变的。在CC防护里,这就意味着它只认你设定的那个固定处理速率。

当一个IP或者一个会话的请求到达时,漏桶会把这些请求排队。如果桶满了,也就是请求堆积超过了预设的容量上限,新来的请求会被直接丢弃。这种机制带来的直接好处是,后端服务器的负载曲线会被压得非常平。哪怕攻击者瞬间爆发了10万QPS的请求,经过漏桶后,系统还是按照你设定的1000QPS慢悠悠地处理。

但问题也出在这里。漏桶算法无法处理突发流量。比如你的电商平台正在做秒杀,正常用户在一瞬间涌入大量点击,这种合法的“脉冲流量”在漏桶眼里和CC攻击长得一模一样。因为漏桶只看流速,不看历史。一旦那几秒钟的合法突发流量把桶灌满了,后续真实用户的请求就会被无差别丢弃,导致正常业务受损。所以,漏桶特别适合保护那种处理能力有严格上限、完全无法承受任何波动的核心接口,比如登录接口、短信验证码发送接口。这些地方宁可错杀,也不能让后端数据库连接池爆掉。

Token桶算法在CC防护中的弹性放行逻辑

Token桶算法比漏桶多了一个维度:它允许一定程度的突发。它的模型是一个以恒定速率往桶里放Token的桶,每个请求需要消耗一个Token才能被放行。如果桶里的Token满了,多出来的Token会被丢弃,不能无限积攒。

在CC防护场景下,Token桶的妙处在于它的“预支”能力。假设你配置的速率是1000QPS,桶的容量是3000个Token。如果系统在之前的一段时间内没有流量,桶里就会存满3000个Token。当秒杀开始的瞬间,这3000个请求可以在一秒内瞬间被放行,而不会触发限流。这完美契合了正常业务流量的特征:大部分时间低流量,偶尔出现高峰。

对于CC攻击的防御,Token桶的难点在于容量的设定。如果桶容量设得太大,攻击者完全可以利用这个漏洞,先静默一段时间积攒Token,然后瞬间爆发一波大流量把后端打死,这种攻击方式叫“低速率脉冲攻击”。但如果容量设得太小,又退化成漏桶,失去了应对正常业务突发的弹性。因此,Token桶实际上是在风险和体验之间做权衡,它默认信任了“过去一段时间表现良好的IP值得拥有一定的突发额度”。

CC攻击流量特征下的算法失效边界

要做出正确选择,必须看透CC攻击的流量模型。现在的CC攻击早就不是傻傻的高频请求了,而是高度模拟真实用户的慢速攻击或者分布式低频攻击。

漏桶在面对分布式低频CC时,几乎完全失效。比如攻击者控制了10万个肉鸡,每个肉鸡每秒只发1个请求。对于单个IP来看,这个速率非常低,漏桶根本不会触发限流。但10万个IP加起来,后端瞬间就挂了。漏桶只能做单IP或单会话的速率控制,无法感知全局的并发连接数压力。

Token桶在面对这种攻击时,同样会被绕过,但它多了一个风险点:如果攻击者摸清了你的Token生成速率和桶容量,他可以精心设计请求频率,刚好卡在触发限流的阈值之下,持续消耗你的Token,让桶始终处于空的状态。当真实用户到来时,因为没有Token可用,反而被拒绝服务。这就是所谓的“饿死正常用户”现象。

所以,单纯依靠这两种算法中的任何一种,都无法应对复杂的CC攻击。真正的生产环境,往往是组合拳。

基于业务场景的算法选择决策树

在实际配置WAF或Nginx限流模块时,可以按照下面的逻辑来决策:

第一,看业务接口的类型。如果是API接口,尤其是写操作接口,比如下单、支付、发帖,首选漏桶算法。这类接口对数据库的压力非常敏感,必须严格控制并发量,绝对不能允许突发。哪怕因此导致用户看到“系统繁忙”的提示,也比数据库死锁、数据不一致要好。对于这类场景,漏桶的恒定速率就是数据库的承压红线。

第二,看流量模型。如果是读操作接口,比如商品详情页、新闻列表,并且业务有明显的波峰波谷,Token桶是更好的选择。因为这类接口通常做了大量缓存,后端压力不大,允许瞬间的高并发读取。Token桶能保证用户在点击下一页时不会因为限流而卡顿,体验更好。

第三,看攻击面。如果这个接口经常遭受撞库或CC攻击,且攻击者IP变化频繁,那么漏桶的刚性拒绝能更快地消耗攻击者的资源。但如果攻击是低频慢速的,必须引入基于行为分析的算法,比如滑动窗口计数器,而不是单纯依赖Token或漏桶。滑动窗口可以精确计算“最近N秒内的请求数”,比Token桶的“平均速率”更敏锐。

进阶配置:如何避免误伤与穿透

很多人配置了限流算法,结果要么误杀手机App的弱网重试请求,要么被攻击者穿透。这里有几个硬核的配置细节。

关于突发容量的设置,Token桶的容量有一个经验公式:容量 = 限流速率 * 最大允许突发秒数。比如限流1000QPS,允许突发2秒,容量就是2000。千万不要设成速率的几十倍,那等于没限。对于漏桶,队列长度的设置要参考后端服务的超时时间。如果后端超时是1秒,队列里积压的请求处理时间总和不能超过1秒,否则排队的请求即使被处理了,客户端也早就超时断开,白白浪费服务器资源。

还有一个关键点是限流的键值选取。对于未登录用户,强烈建议使用IP+User-Agent+ASN的组合键,单纯用IP限流在NAT网关环境下会误杀一大片。对于登录用户,必须基于用户ID进行限流。这就引出了两种算法的混合使用:可以对全局流量做漏桶限制,保护整个集群;同时对单个用户ID做Token桶限制,允许VIP用户有更高的突发容量。

在代码实现层面,如果使用Redis做分布式限流,Token桶的实现要特别注意原子性。下面是一段基于Redis Lua脚本实现的Token桶核心逻辑,可以直接用在网关层:

-- KEYS[1]: 桶的Key
-- ARGV[1]: 容量
-- ARGV[2]: 生成速率(每秒)
-- ARGV[3]: 当前请求消耗Token数(通常为1)
-- ARGV[4]: 当前时间戳(秒)

local bucket_key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local requested = tonumber(ARGV[3])
local now = tonumber(ARGV[4])

-- 获取上次填充时间和当前Token数
local last_time = redis.call("HGET", bucket_key, "last_time")
local tokens = redis.call("HGET", bucket_key, "tokens")

if last_time == false then
    -- 首次请求,初始化桶
    tokens = capacity
    last_time = now
else
    tokens = tonumber(tokens)
    last_time = tonumber(last_time)
    -- 计算应该生成的Token数
    local delta = math.max(0, now - last_time)
    local new_tokens = math.floor(delta * rate)
    if new_tokens > 0 then
        tokens = math.min(capacity, tokens + new_tokens)
        last_time = now
    end
end

local allowed = false
if tokens >= requested then
    tokens = tokens - requested
    allowed = true
end

-- 更新桶状态
redis.call("HSET", bucket_key, "tokens", tokens)
redis.call("HSET", bucket_key, "last_time", last_time)
redis.call("EXPIRE", bucket_key, 10) -- 过期时间防止内存泄漏

-- 返回结果:1表示放行,0表示拒绝,同时返回剩余Token数
return { allowed and 1 or 0, tokens }

这段脚本保证了在高并发下Token的生成和消耗是原子操作,不会出现超发或多扣的问题。在实际部署时,如果Nginx前面挂了LVS或者F5这种四层负载,需要在Nginx层做限流,因为四层设备无法解析HTTP协议,只能做简单的连接数限制,精细化的七层CC防护必须靠应用层网关实现。

超越算法:CC防护的全链路视角

选完算法不等于万事大吉。Token桶和漏桶解决的只是“请求速率”问题,而CC攻击还会通过耗尽连接数、占用慢连接等方式打垮服务器。

比如Slowloris这种慢速攻击,它每个请求的速率极低,Token桶和漏桶都会放行,但它会长时间占用服务器的连接资源。这时候需要配合连接超时时间和最小传输速率的配置。Nginx的client_body_timeout和client_header_timeout就是干这个的。如果一个连接长时间不发数据,直接断开,不管它Token有多少。

再比如,攻击者可能通过请求大文件或者复杂查询来消耗服务器CPU和内存。这时候需要引入“请求成本”的概念。可以对不同的URI分配不同的权重。请求搜索接口消耗3个Token,请求静态页面只消耗1个Token。这种加权限流是标准Token桶的变种,能更精准地保护后端资源。

最终,算法选择只是CC防护策略里的一环。漏桶是保底手段,用来兜底后端服务的最坏情况;Token桶是体验优化手段,用来消化正常业务的起伏。如果非要给一个明确的建议:在核心交易链路,用漏桶,宁可误杀不可宕机;在内容浏览链路,用Token桶,兼顾体验和安全。而对于那些真正的大规模混合CC攻击,只有结合行为分析、验证码挑战和IP信誉库,才能构建完整的防线,单靠限流算法是顶不住的。