什么是限流?
限流是一种控制单位时间内系统处理请求数量的技术手段,用于保护系统免受过载请求的影响。当请求频率超过设定阈值时,系统会拒绝或延迟处理超额请求。
限流的场景
- 避免突发流量压垮服务:电商平台进秒杀活动。在商品开售瞬间,可能有数十万用户同时点击“立即购买”。如果没有限流,所有请求瞬间涌向商品库存和订单服务,极可能导致服务进程崩溃、数据库连接耗尽,整个购买流程瘫痪。通过限流(如每秒只处理1000个下单请求),系统可以按照自身处理能力平稳运行,虽然大部分用户会看到“请稍后重试”的提示,但核心交易系统得以保全,避免完全崩溃。
- 确保核心功能正常可用:一个在线视频会议软件。其核心功能是音视频流的稳定传输。当服务器压力过大时,非核心的辅助功能(如背景虚化、高清美颜、文件传输)会消耗大量算力和带宽。通过限流,系统可以优先保障音视频数据包的传输,对非核心功能的请求进行排队或降级(如降低美颜效果、延迟文件传输),从而确保所有用户的通话基本流畅,核心体验不受损。
- 防止恶意攻击:用户登录接口遭遇“撞库”攻击(暴力破解)。攻击者使用自动化脚本,从一个IP地址高速、连续地尝试用不同的用户名密码组合进行登录。通过限流(如限制同一IP每分钟只能尝试登录5次),可以极大增加攻击者的时间成本。超过阈值的异常请求会被直接拒绝或要求进行验证码校验,从而有效保护用户账号安全,防止攻击者耗尽认证服务资源。
- 流量整形:一个消息推送服务。下游的数据处理服务每秒最多能稳定处理500条消息。上游的消息队列可能因为某个事件(如系统通知、热点新闻)瞬间涌入上万条待推送消息。如果直接放行,下游服务会被瞬间击垮。通过限流器(如令牌桶算法),可以控制每秒最多只向下游放行500个请求,将突发的“毛刺”流量整形为一个平稳的“涓流”,让下游服务能够持续、稳定地消化任务,避免因处理不过来而堆积或崩溃。
限流算法
计数器算法(固定窗口)
计数器算法是限流算法中最基础、最直观的一种。它的核心思想是:在一个固定的时间窗口内,对请求进行计数,当请求数超过设定的阈值时,就触发限流。
下面给出一个简单的示例:
public class CounterLimiter {
private long windowSize; // 窗口大小(毫秒)
private long limit; // 限制数量
private long count; // 当前计数
private long windowStart; // 窗口开始时间
public boolean tryAcquire() {
long now = System.currentTimeMillis();
if (now - windowStart > windowSize) {
// 新窗口
count = 0;
windowStart = now;
}
if (count < limit) {
count++;
return true;
}
return false;
}
}
我们确定了一个时间窗口和最大请求数阈值,当一个时间窗口开始时,将计数器清零。
- 每当有一个新请求到达,计数器就加1。
- 在计数器加1前,先判断当前计数值是否已超过阈值。
- 如果未超过,则允许请求通过。
- 如果已达到或超过阈值,则拒绝该请求(即触发限流)。
计数器算法的实现非常简单,但是有一个很明显的缺陷:时间窗口的临界问题。
假设限流规则仍是每秒5次请求。
- 如果在
00:01:00.500到00:01:01.000这0.5秒内,涌入了5个请求。 - 随后在
00:01:01.000到00:01:01.500这接下来的0.5秒内,又涌入了5个请求。
虽然这两个相邻的时间窗口各自都没有超过阈值(5次/秒),但在 00:01:00.500到 00:01:01.500这实际的1秒内,系统却处理了10个请求,是阈值的两倍。这在流量突增时可能对系统造成压力。
滑动窗口算法
滑动窗口算法,它是固定窗口计数器算法的改进版,旨在解决其致命的“临界突变”问题。
滑动窗口算法不再使用固定的、不连续的时间窗口,而是将一个大的时间窗口(如1分钟)划分为多个更小的时间片(如6个10秒的片)。窗口的边界是动态滑动的,统计的是当前时间点往前回溯一整个窗口时长内的总请求数。
设定参数:
- 窗口时间长度
T(例如 60秒)。 - 窗口内允许的最大请求数
N(例如 100次)。 - 将窗口
T均匀划分为n个小时间片(例如 6个片,每个片10秒)。
维护一个时间片队列或环形数组,每个时间片记录该时间段内的请求计数。
当请求到达的时候:
-
检查并淘汰过期时间片:获取当前时间,删除所有时间戳早于
当前时间 - T的旧时间片。这就是“滑动”的过程,窗口随着时间向前移动。 -
统计当前窗口内总请求数:将剩余所有未过期时间片内的计数累加,得到过去
T时间内的总请求数。 -
判断是否限流:
- 如果
总请求数 < N,则允许当前请求通过,并将当前请求所在的时间片计数加1(如果该时间片不存在则创建)。 - 如果
总请求数 >= N,则拒绝当前请求(触发限流)。
- 如果
下面给出一个例子: 设定规则:每分钟(T=60s)最多100个请求,划分6个时间片(每个片10s)。
假设当前时间是 01:30:50:
- 系统会统计从
01:29:50到01:30:50这60秒滑动窗口内的总请求数。 - 请求落在
01:30:40至01:30:50这个时间片上。 - 如果过去60秒内总请求数已达100,则当前请求被限流;如果只有95个,则允许通过,并将对应时间片计数+1。
可以说,滑动窗口算法和计数器算法最大的不同就是废弃统计的请求数的粒度不同:
- 计数器算法是直接废弃一整个窗口的请求数
- 滑动窗口算法则是划分了更小的子窗口,逐个小窗口废弃。
漏桶算法
漏桶算法是一种经典的流量整形或速率限制算法,用于控制数据在网络中或系统间的传输速率,使其保持恒定、平滑的输出,避免突发流量对下游系统造成冲击。
它的核心思想非常形象:
- 一个漏桶:想象一个底部有固定大小漏孔的桶。
- 水流进入:无论上方的水流(数据包/请求)以多快或多不规律的速度注入桶中,只要桶没满,水都可以先存起来。
- 恒定速率流出:水从桶底的漏孔以恒定速率流出。这个速率是预设的,与进水速度无关。
工作原理详解
在技术实现上,漏桶包含以下几个关键部分:
- 请求到达:网络数据包或API请求等到达系统。
- 队列(桶) :如果桶(缓冲区)未满,请求会被放入队列等待处理;如果桶已满,新到达的请求则会被丢弃或拒绝(即“溢出”)。
- 恒定速率处理(漏孔) :一个独立的处理器以恒定的、预先配置好的速率从队列头部取出请求进行处理。例如,每秒处理10个请求。
主要特点
- 平滑输出:无论输入流量多么突发或不规则,输出流量始终是恒定、平滑的。这是它最核心的优势。
- 应对突发:对于短时间的突发流量,桶的缓冲区可以暂时容纳,但输出速率不变。如果突发流量持续不断导致桶满,则后续流量会被丢弃。
- 无法加速:即使桶是空的,输出速率也不会超过预设的恒定值,无法利用空闲带宽。
令牌桶算法
令牌桶算法是一种广泛应用于网络流量整形和速率限制的高效算法。它的核心思想是模拟一个以固定速率向桶中添加“令牌”的容器,每个请求需要获取并消耗一个令牌才能被处理;若桶中令牌不足,则请求会被延迟或拒绝。
工作原理:
-
令牌生成:系统以一个恒定的速率(如每秒r个)向一个容量固定的“桶”中添加令牌。
-
令牌消耗:当有请求到达时,它会尝试从桶中取出一个令牌。
- 如果桶中有令牌:请求成功取出令牌并被立即处理,令牌数减一。
- 如果桶中无令牌:请求无法被立即处理。根据具体策略,它可能被放入队列等待(直到有新的令牌生成),也可能被直接拒绝或丢弃。
-
桶容量限制:桶有一个最大容量(设为b)。当令牌数量达到上限时,新生成的令牌会被丢弃,这保证了流量突发上限的可控性。
关键参数
- 令牌添加速率 (r) :决定了系统长期允许的平均请求处理速率。
- 桶容量 (b) :决定了系统在短时间内能应对的突发流量上限。它允许在流量空闲期积累令牌,以应对后续的突发请求。