首页 / 帮助文档 / 基于令牌桶算法的网关层CC攻击流量整形

基于令牌桶算法的网关层CC攻击流量整形

网关层面对CC攻击,最棘手的问题不是“打死”,而是“半死不活”。攻击流量恰好卡在服务器CPU 90%、数据库连接池满、正常用户请求间歇性超时的临界点。传统的IP黑名单、验证码跳转属于事后响应,在触发阈值前,那几秒的流量尖峰已经足够把后端打穿。真正有效的防线必须下沉到流量进门的瞬间,用算法在数据包级别就完成整形。令牌桶算法在这个场景下的价值,不是因为它复杂,恰恰是因为它足够简单、可预测,能在不维护海量状态的前提下,对突发流量进行精确的削峰填谷。

为什么是令牌桶而不是漏桶

很多人容易混淆令牌桶和漏桶。漏桶的核心是强制平滑输出,不管上游来多少流量,下游都以固定速率处理,多余的请求直接丢弃或排队。这种机制对保护后端确实有效,但在CC攻击场景下存在致命缺陷:攻击流量会消耗掉所有处理配额,正常用户的请求即使到达网关,也会因为漏桶已满而被无情丢弃。攻击者不需要打穿服务器,只需要占满漏桶容量,就能制造大面积拒绝服务。

令牌桶的思路完全相反。它以固定速率向桶中放入令牌,请求到达时必须从桶中取走令牌才能被转发。桶有容量上限,当令牌积攒到上限后,新生成的令牌会被丢弃。这意味着系统允许一定程度的突发流量:如果一段时间内没有请求,桶中会积攒令牌,后续突发请求可以一次性消耗这些令牌。关键在于,令牌桶允许你根据客户端行为做差异化控制,而不是对所有流量一视同仁地排队。

网关层的实现位置与数据平面选择

在网关层实现令牌桶,通常有两种落点:一是运行在用户态的API网关进程内,比如基于OpenResty的Lua脚本,或者Go语言写的网关插件;二是直接下沉到内核态,利用eBPF或XDP在网卡驱动层做限流。用户态实现灵活,方便与业务逻辑联动,但性能受限于系统调用和上下文切换。内核态实现吞吐量极高,可以处理数百万pps的流量,但开发复杂度大,调试困难。

对于绝大多数业务场景,用户态网关配合共享内存的令牌桶计数器已经足够。以OpenResty为例,每个Worker进程维护一个本地的令牌桶状态,通过共享字典在不同Worker之间同步消耗速率。更进一步的优化是将令牌桶状态放在Redis或本地内存中,但需要处理好原子性和时钟漂移问题。实际落地时,建议采用无锁的原子操作更新令牌计数,避免在高并发下因为锁竞争导致限流器本身成为瓶颈。

令牌桶的核心参数与动态调整策略

令牌桶只有两个参数:速率和桶容量。速率决定了长期平均允许通过的请求数,桶容量决定了允许的突发大小。这两个参数如果设置成静态值,在复杂的攻击模式下几乎一定会失效。攻击者可以通过缓慢增加请求速率的方式探测阈值,一旦找到边界,就可以将攻击流量精确控制在这个阈值以下,长期占用资源。

更有效的做法是引入动态调整机制。网关可以实时监控每个客户端的请求间隔分布、URL访问熵、请求成功率等指标,根据这些指标动态调整该客户端的令牌生成速率。例如,当检测到某个IP在短时间内访问了大量不同URL,且请求间隔极度均匀(正常用户的访问模式通常带有随机性和长尾分布),系统可以自动降低该IP对应的令牌桶速率,甚至将桶容量缩减到零,直接拒绝后续请求。这种基于行为分析的动态令牌桶,本质上是在流量整形的基础上叠加了一层轻量级的威胁检测。

多维度的令牌桶粒度设计

如果只对源IP做令牌桶限流,攻击者很容易通过IP池绕过。真正有效的网关层防御必须支持多粒度组合:全局维度、IP维度、会话维度、API路径维度。全局令牌桶保证整个网关的吞吐量不会超过后端承载上限,这是最后一道保命防线。IP维度的令牌桶限制单个IP的请求速率,能挡住最粗暴的单IP洪水攻击。会话维度的令牌桶针对已认证用户,防止单用户行为异常影响其他用户。API路径维度的令牌桶则用于保护某些高消耗接口,比如搜索、导出、报表生成等。

多维度令牌桶的挑战在于协调。一个请求到达网关时,可能同时命中多个维度的限流规则。此时需要定义清晰的优先级和短路逻辑。通常的做法是:先检查全局桶,再检查IP桶,然后是API路径桶,最后是会话桶。任何一个桶的令牌获取失败,请求立即被拒绝,不再继续后续检查。这种链式判断可以最大程度减少不必要的计算开销。

令牌桶的原子操作与并发安全

在高并发网关中,令牌桶的并发安全是核心难点。假设网关有32个Worker进程,每个进程都在处理请求,令牌桶的剩余令牌数是一个共享变量。如果不加锁,多个进程同时读取并修改这个变量,必然导致计数错乱,实际通过的请求数可能远超预期。如果加锁,限流器本身就会成为性能瓶颈。

实际工程中,通常采用Redis的Lua脚本或原子命令来实现分布式令牌桶。将整个“检查令牌、扣减令牌”的逻辑封装在一个Lua脚本中,利用Redis的单线程特性保证原子性。脚本逻辑大致如下:获取当前桶中的令牌数,计算自上次填充以来应该新生成多少令牌,将新令牌数加上去但不超过桶容量,然后判断是否足够扣减本次请求需要的令牌数。如果足够,扣减并返回通过;如果不够,返回拒绝。

-- Redis Lua脚本实现令牌桶
local key = KEYS[1]           -- 令牌桶的key
local rate = tonumber(ARGV[1]) -- 令牌生成速率(个/秒)
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3])  -- 当前时间戳(毫秒)
local requested = tonumber(ARGV[4]) -- 请求的令牌数

local bucket = redis.call('hmget', key, 'tokens', 'last_time')
local tokens = tonumber(bucket[1])
local last_time = tonumber(bucket[2])

if tokens == nil then
    -- 首次访问,初始化桶
    tokens = capacity
    last_time = now
end

-- 计算应该生成的令牌数
local delta = (now - last_time) * rate / 1000
tokens = math.min(capacity, tokens + delta)
last_time = now

if tokens >= requested then
    tokens = tokens - requested
    redis.call('hmset', key, 'tokens', tokens, 'last_time', last_time)
    redis.call('expire', key, 60) -- 设置过期时间,避免内存泄漏
    return 1 -- 通过
else
    redis.call('hmset', key, 'tokens', tokens, 'last_time', last_time)
    redis.call('expire', key, 60)
    return 0 -- 拒绝
end

这个脚本的核心在于将时间差换算成令牌增量,然后做一次原子判断和扣减。即使多个Worker同时执行,Redis也会串行处理,保证数据一致性。对于性能要求更高的场景,可以进一步优化:将令牌桶状态放在本地内存中,通过定时同步或广播的方式维持全局一致性,但这样会牺牲一定的精确度,换来更高的吞吐量。

CC攻击流量整形的实际效果与局限性

令牌桶在网关层做流量整形,最直接的效果是让后端服务器看到的流量曲线变得平滑可控。无论攻击者如何构造请求尖峰,网关都会按照预设的速率向后端转发,超出部分直接在网关层丢弃。这相当于在攻击流量和后端服务之间插入了一个大容量的缓冲器。攻击者想要打穿后端,必须先打穿网关的令牌桶,而网关本身是无状态的、可水平扩展的,增加网关节点就能线性提升抗攻击能力。

但令牌桶并非万能。对于慢速CC攻击,比如每个攻击IP的请求速率都控制在令牌桶阈值以下,但攻击IP数量巨大,总请求量仍然可以压垮后端。这种情况下,单纯靠令牌桶无法区分正常流量和攻击流量,因为每个IP的行为看起来都是合法的。此时必须结合行为分析、设备指纹、验证码挑战等手段,在令牌桶之上构建更复杂的检测逻辑。

生产环境中的监控与调优

令牌桶上线后,监控指标必须跟上。关键指标包括:每个维度令牌桶的令牌消耗速率、令牌桶被击穿的频率、被拒绝的请求数与比例、令牌桶操作的延迟等。这些指标可以暴露攻击模式的变化,也能帮助运维人员调整速率和容量参数。一个常见的错误是速率设置过高,导致令牌桶形同虚设;或者容量设置过大,允许了过度的突发流量,在突发瞬间仍然可能压垮后端。

调优时,建议从后端服务的真实承载能力反推。先通过压测确定后端单实例的最大QPS,然后乘以实例数得到总容量,再将总容量的70%到80%作为全局令牌桶的速率上限,留出缓冲空间应对流量波动。IP维度的速率则根据正常用户的访问行为设定,通常单个用户在非自动化场景下的请求速率不会超过每秒几次,可以将IP维度的速率设置为这个值的两到三倍,既能容忍正常突发,又能拦截明显的自动化攻击。

与其它限流算法的组合使用

在实际网关架构中,令牌桶很少单独使用。通常会与滑动窗口计数器、漏桶、甚至简单的固定窗口组合成多层限流体系。滑动窗口适合做精确的短时间窗口计数,比如“最近1秒内最多10个请求”,这种场景用令牌桶反而不直观。漏桶适合做流量整形,保证后端接收到的请求速率绝对均匀。令牌桶则处于两者之间,既能限制平均速率,又能处理突发流量,最适合作为网关的第一道防线。

一个典型的组合策略是:第一层用令牌桶做宽松的流量整形,放行大部分正常流量,拦截明显的流量尖峰;第二层用滑动窗口做精确的短时频控,针对敏感接口做更严格的限制;第三层用行为分析引擎做异步检测,识别出慢速但恶意的请求模式,动态下发黑名单或降低令牌桶速率。这种分层防御体系,比单一算法更能适应复杂的攻击手法。

网关层的CC攻击防御,本质上是在和攻击者比拼资源利用效率。令牌桶算法的价值在于,它用极小的计算和存储成本,实现了对海量流量的精细化控制。当攻击流量到达网关时,令牌桶在毫秒级时间内就能完成判断,不依赖外部服务,不产生大量日志,不消耗后端连接。这种轻量级特性,使得它可以在不显著增加延迟的前提下,为后端服务提供持续稳定的保护。对于任何面向公网的业务系统,在网关层部署基于令牌桶的流量整形,都应该成为基础的安全标配,而不是等到被攻击之后才匆忙上线。