限速策略基础:限速的核心思想、令牌桶算法、漏桶算法、计数器算法
聊到限速,很多刚入行的朋友第一反应就是“限制流量嘛,简单”。
嗯,确实简单,但想限得稳、限得准、限得让攻击者难受,这里面的门道可不少。
我做了这么多年DDoS防护,见过太多因为限速策略没设计好,结果把正常用户也误杀了的案例。
说白了,限速不是一刀切,而是要在“让坏人进不来”和“让好人进得来”之间找到那个平衡点。
这一章,咱们就把限速的底裤扒干净。
从最核心的思想开始,再到三种经典算法,我会结合实战经验,把每个坑都给你指出来。
限速的核心思想:你凭什么限我?
限速的本质是什么?
我个人习惯把它理解成“资源分配”。
你想想看,服务器处理请求的能力是有限的。CPU、内存、带宽、数据库连接池,这些都是稀缺资源。
当请求量超过处理能力时,系统就会崩溃。限速就是要在系统崩溃之前,主动拒绝一部分请求。
但问题来了:拒绝谁?
这里有两个核心原则:
- 公平性:每个用户或每个IP应该获得相对公平的资源份额。不能因为一个用户疯狂刷请求,就把所有资源都占光了。
- 优先级:在资源紧张时,要优先保障高价值用户或核心业务的请求。比如支付接口的限速阈值,肯定要比日志查询接口高得多。
我踩过的坑:
有一次,我对所有API接口用了同一个限速阈值。结果双十一大促时,商品详情页的请求量暴增,直接把支付接口的配额也吃掉了。用户下单失败,投诉电话打爆了。
从那以后,我坚持每个核心接口独立配置限速策略,并且给支付、下单这类关键链路预留了“应急配额”。
所以,限速的核心思想可以总结为三句话:
- 识别:区分出谁是正常用户,谁是攻击者(或者异常流量)。
- 限制:对异常流量进行压制,对正常流量进行保护。
- 反馈:让被限制的请求得到明确的响应(比如返回429状态码),而不是让客户端一直傻等超时。
接下来,我们看看实现限速的三种经典算法。每种算法都有它的脾气,选错了,效果会大打折扣。
令牌桶算法:我最常用的“温柔一刀”
令牌桶算法,是我个人在实战中最喜欢用的算法。
为什么?因为它够“温柔”。
它的工作原理是这样的:
- 系统以固定的速率往一个“桶”里放令牌。比如每秒放10个。
- 每个请求来的时候,必须从桶里拿走一个令牌,才能被处理。
- 如果桶里的令牌用完了,新来的请求就会被拒绝或者排队等待。
- 桶有一个最大容量。如果令牌放满了,新生成的令牌就会丢弃。
你发现没有?
令牌桶允许“突发流量”。
比如桶的容量是100,速率是每秒10个。如果系统空闲了10秒,桶里就攒了100个令牌。这时候突然来了一波100个请求的突发流量,令牌桶可以一口气全部放行。这很符合很多互联网业务的真实场景——平时请求不多,但搞活动或上热搜时,流量会瞬间暴涨。
实战建议:
令牌桶算法适合大多数Web API和微服务场景。我一般把桶容量设置为速率的2-3倍,这样既能应对突发流量,又不会让系统被冲垮。
下面是一个简单的令牌桶实现示例(伪代码):
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate # 令牌生成速率(个/秒)
self.capacity = capacity # 桶的最大容量
self.tokens = capacity # 当前令牌数
self.last_refill_time = time.now()
def allow_request(self):
# 先补充令牌
now = time.now()
elapsed = now - self.last_refill_time
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.last_refill_time = now
# 判断是否允许请求
if self.tokens >= 1:
self.tokens -= 1
return True
else:
return False
代码很简单,但有几个细节要注意:
- 时间精度:如果时间精度太低,令牌补充会不准确。我建议用毫秒级的时间戳。
- 并发安全:在高并发下,令牌的扣减和补充操作必须是原子性的。可以用锁,也可以用Redis的Lua脚本。
漏桶算法:滴水不漏的“铁闸门”
漏桶算法和令牌桶正好相反。
它像一个底部有漏洞的水桶。水(请求)以任意速率倒进桶里,但水只能以固定的速率从漏洞流出去。如果桶满了,新倒进来的水就会溢出(被拒绝)。
漏桶算法的核心特点是:强制平滑流量。
不管你的请求来得有多猛,漏桶都会把它们排队,然后以恒定的速率放行。这非常适合那些对流量平稳性要求极高的场景,比如数据库写入、消息队列生产等。
注意:
漏桶算法不擅长处理突发流量。如果桶的容量设置得太小,正常的突发请求也会被大量拒绝。我曾经在一个视频上传服务上用过漏桶,结果用户上传大文件时频繁失败,因为上传请求的并发量超过了桶的容量。
漏桶算法的实现也很直观:
class LeakyBucket:
def __init__(self, rate, capacity):
self.rate = rate # 漏水速率(个/秒)
self.capacity = capacity # 桶的最大容量
self.water = 0 # 当前水量(请求数)
self.last_leak_time = time.now()
def allow_request(self):
# 先漏水
now = time.now()
elapsed = now - self.last_leak_time
self.water = max(0, self.water - elapsed * self.rate)
self.last_leak_time = now
# 判断是否允许请求
if self.water < self.capacity:
self.water += 1
return True
else:
return False
你看,代码结构和令牌桶很像,但逻辑完全相反。
令牌桶是先加令牌再扣减,漏桶是先漏水再加水。
计数器算法:简单粗暴的“土办法”
计数器算法,顾名思义,就是统计单位时间内的请求次数。
比如,限制1分钟内最多100个请求。超过100个,就拒绝。
它是最容易理解的算法,实现起来也最简单:
class CounterLimiter:
def __init__(self, limit, window_size):
self.limit = limit # 窗口内允许的最大请求数
self.window_size = window_size # 时间窗口大小(秒)
self.counter = 0
self.window_start = time.now()
def allow_request(self):
now = time.now()
# 如果当前时间已经超出了当前窗口,重置计数器
if now - self.window_start >= self.window_size:
self.counter = 0
self.window_start = now
if self.counter < self.limit:
self.counter += 1
return True
else:
return False
但是,计数器算法有一个著名的缺陷:临界突变。
举个例子:
我限制1分钟100个请求。如果用户在第一个59秒内发了100个请求,然后在第60秒到第61秒之间又发了100个请求。因为第60秒重置了计数器,所以这200个请求实际上是在2秒内发完的,但计数器算法却认为它们分别在两个不同的窗口内,全部放行了。
这就是所谓的“突刺”现象。
我曾经踩过的坑:
早期做CC攻击防护时,我用计数器算法限制每个IP每秒10个请求。结果攻击者卡着每秒的边界,在每秒钟的最后0.1秒和下一秒的最开始0.1秒集中发送请求,瞬间打出了20 QPS的流量,直接绕过了我的限速。后来我改用滑动窗口计数器,才解决了这个问题。
滑动窗口计数器是对固定窗口计数器的改进。它把时间窗口切分成更小的粒度(比如把1分钟切成6个10秒的小窗口),然后统计最近N个小窗口的总请求数。这样就能更平滑地统计流量,避免临界突变。
三种算法对比:选哪个?
我把这三种算法的核心差异整理成了一张表,方便你对比:
| 特性 | 令牌桶 | 漏桶 | 计数器(固定窗口) |
|---|---|---|---|
| 是否允许突发 | 允许(桶容量内) | 不允许 | 允许(窗口边界有风险) |
| 流量平滑度 | 中等 | 最高 | 最低 |
| 实现复杂度 | 中等 | 中等 | 简单 |
| 适用场景 | Web API、微服务、网关 | 数据库写入、消息队列、日志收集 | 简单限流、IP黑名单、非关键路径 |
| 主要缺点 | 需要处理并发安全 | 无法应对突发流量 | 临界突变问题 |
我个人习惯的选型原则是:
- 大部分对外API,用令牌桶。它最灵活,能兼顾突发和限速。
- 对后端存储或中间件的写入操作,用漏桶。保护下游系统不被冲垮是第一位的。
- 简单的IP级别限速或临时防护,用计数器。实现快,够用就行。
知识体系总览
为了让你更直观地理解这三种算法的关系,我画了一张图:
这张图把三种算法的定位和适用场景串在了一起。你可以把它当作一个速查表,在实际项目中快速决策。
我的一个小习惯:
在项目初期,我会先用计数器算法快速实现一个限速功能,保证系统能上线。等业务稳定了,再根据实际流量特征,逐步替换成令牌桶或漏桶。不要一开始就追求完美,先跑起来再说。
好了,限速的基础知识就聊到这里。
这三种算法是后续所有高级限速策略的基石。理解了它们,你就能看懂Nginx的limit_req模块、Guava的RateLimiter、以及各种网关的限速配置到底在干什么。