限流器调研

单机限流算法


计数器(固定窗口)


简介

基于一个给定的时间窗口,维护一个计数器用于统计访问次数,然后实现以下规则:

  • 如果访问次数小于阈值,则代表允许访问,访问次数 +1。
  • 如果访问次数超出阈值,则限制访问,访问次数不增。
  • 如果超过了时间窗口,计数器清零,并重置清零后的首次成功访问时间为当前时间。这样就确保计数器统计的是最近一个窗口的访问量。


代码示例

// 毫秒为单位的时间窗口
private final long windowInMs;
// 时间窗口内最大允许的阈值
private final int threshold;
// 最后一次成功请求时间
private long lastReqTime = System.currentTimeMillis();
// 计数器
private long counter;

// 这里的key可以看成限流的资源(维度),比如按接口限流,接口A一个key,接口B一个key
public synchronized boolean tryAcquire(String key) {
    long now = System.currentTimeMillis();
    // 如果当前时间已经超过了上一次访问时间开始的时间窗口,重置计数器,以当前时间作为新窗口的起始值
    // 首先重置检测
    if (now - lastReqTime > windowInMs) {       #1
        counter = 0;
        lastReqTime = now;                  #2
    }
    // 然后再阈值判断
    if (counter < threshold) {                  #3
        counter++;                          #4
        return true;
    } else {
        return false;
    }
}


优点

算法简单。

缺点

临界突变
以 1 分钟允许 100 次访问为例,如果流量均匀保持 200 次/分钟的访问速率,系统的访问量曲线大概是这样的(按分钟清零):
理想型
但如果流量并不均匀,假设在时间窗口开始时刻 0:00 有几次零星的访问,一直到 0:50 时刻,开始以 10 次/秒的速度请求,就会出现这样的访问量图线:

图片
在临界的 20 秒内(0:50~1:10)系统承受的实际访问量是 200 次,换句话说,最坏的情况下,在窗口临界点附近系统会承受 2 倍的流量冲击,这就是简单窗口不能解决的临界突变问题。

平滑度(突刺)问题
突刺跟临界突变差不多,也是由短时间内流量激增引起的,但是突刺可能出现在时间窗口的任何位置,导致时间窗口内大部分时间服务都是处于空闲状态。


滑动窗口


简介

把整个大的时间窗口切分成更细粒度的子窗口,每个子窗口独立计数,但是限流是按照整个大的时间窗口的计数来。每经过一个子窗口大小的时间,就向右滑动一个子窗口,更新大窗口的计数,这样一来就能解决固定窗口临界突变的问题了。
在这里插入图片描述
如上图所示,将一分钟的时间窗口切分成 6 个子窗口,每个子窗口维护一个独立的计数器用于统计 10 秒内的访问量,每经过 10s,时间窗口向右滑动一格。
回到简单窗口出现临界突变的例子,结合上面的图再看滑动窗口如何消除临界突变。如果 0:50 到 1:00 时刻(对应灰色的格子)进来了 100 次请求,接下来 1:00~1:10 的 100 次请求会落到橙色的格子中,由于算法统计的是 6 个子窗口的访问量总和,这时候总和超过设定的阈值 100,就会拒绝后面的这 100 次请求。

优点

解决了临界突变问题。


缺点

精度问题
现在思考这么一个问题:滑动窗口算法能否精准地控制任意给定时间窗口 T 内的访问量不大于 N?

答案是否定的,还是将 1 分钟分成 6 个 10 秒大小的子窗口的例子,假设请求的速率现在是 20 次/秒,从 0:05 时刻开始进入,那么在 0:05~0:10 时间段内会放进 100 个请求,同时接下来的请求都会被限流,直到 1:00 时刻窗口滑动,将大窗口计数置为0,左边界时间置为0:10,放入请求,假设在 1:00~1:05 时刻放进 了100 个请求。如果把 0:05~1:05 看作是 1 分钟的时间窗口(滑动窗口只会保证0:10~1:10内的请求量不回超过100,但是我们按0:05~1:05来看确实超了),那么这个窗口内实际的请求量是 200,超出了给定的阈值 100。
虽然不能做到精准控制,但是可以通过提高窗口细分的粒度来提高精度,比如1分钟分成60个格子,每个格子1s。
平滑度(突刺)问题
使用滑动窗口算法限制流量时,我们经常会看到像下面一样的流量曲线。

图片
突发的大流量在窗口开始不久就直接把限流的阈值打满,导致剩余的窗口内所有请求都无法通过。在时间窗口的单位比较大时(例如以分为单位进行流控),这种问题的影响就比较大了。在实际应用中我们要的限流效果往往不是把流量一下子掐断,而是让流量平滑地进入系统当中。


漏桶算法


简介

滑动窗口无法很好解决流量平滑度问题,而漏桶算法则能保证请求处理的速率<=指定的值V,起到削峰填谷的作用。在漏桶算法中,要理解一下概念:

  • 最大允许请求数 N:桶的大小,由程序指定。

  • 时间窗口大小 T:一整桶水漏完的时间,由程序指定。

  • 最大访问速率 V:一整桶水漏完的速度,即 N/T。

  • 水:请求的流量。当请求数量小于N时,说明请求可以被处理,将水放入漏桶中。

  • 漏桶:缓冲队列。漏桶算法需要借助缓冲队列来保存外部流量请求。

  • 漏水:对请求进行处理。从缓冲队列中按固定速率V拿出请求并进行处理。

  • 溢出:请求被限流。桶注水的速度比漏水的速度快,最终导致指定时间内请求的数量超过指定值N,超过的这部分请求被丢弃(限流)。

代码实现

下面是溢出(限流 )判断代码,当返回true时,表示没有达到限流阈值,请求可以被处理,之后请求被放入缓冲队列中等待处理。

// 当前桶内剩余的水
private long left;
// 上次成功注水的时间戳
private long lastInjectTime = System.currentTimeMillis();
// 桶的容量
private long capacity;
// 一桶水漏完的时间
private long duration;
// 桶漏水的速度,即 capacity / duration
private double velocity;

public boolean tryAcquire(String key) {
    long now = System.currentTimeMillis();
    // 当前剩余的水 = 之前的剩余水量 - 过去这段时间内漏掉的水量
    // 过去这段时间内漏掉的水量 = (当前时间-上次注水时间) * 漏水速度
    // 如果当前时间相比上次注水时间相隔太久(一直没有注水),桶内的剩余水量就是0(漏完了)
    left = Math.max(0, left - (long)((now - lastInjectTime) * velocity));
    // 往当前水量基础上注一单位水,只要没有溢出就代表可以访问
    if (left + 1 <= capacity) {
        lastInjectTime = now;
        left++;
        return true;
    } else {
        return false;
    }
}

优点

平滑流量,提供一个稳定的流量输出速率。

缺点

在处理突发流量时缺乏效率,太保守,设定60秒内的流量阈值为60,那请求处理速率为1个/秒,假设在0~1秒时来了60个请求,本来系统可以很快一次性处理掉这60个请求的,偏偏就要等60秒才能处理完,缺乏效率。

令牌桶算法

简介

令牌桶算法就是以稳定的速率往桶中存放令牌,当桶满了,多余的令牌就会被丢弃。当请求到达时,会从同种拿一个令牌,如果能拿到,请求就会被放行处理,否则被丢弃(限流)。

令牌桶中没有使用缓存队列。

代码实现

下面是请求到来时,是否放行的判断。

// 当前桶内剩余的令牌
private long left;
// 上次成功添加令牌的时间戳
private long lastInjectTime = System.currentTimeMillis();
// 桶的容量
private long capacity;
// 空桶放满令牌所需的时间
private long duration;
// 桶中放令牌的速度,即 capacity / duration
private double velocity;

long now = System.currentTimeMillis();
left = Math.min(capacity, left + (long)((now - lastInjectTime) * velocity));
if (left - 1 > 0) {
    lastInjectTime = now;
    left--;
    return true;
} else {
    return false;
}

优点

既可以在大流量场景下限制处理请求的速率(通过限制放令牌到的速率),也可以在流量激增场景下处理瞬时流量(通过一次性拿到大量令牌)。

总结

图片

分布式限流

  • 背景:现在的服务都是分布式部署,流量都是网关通过负载均衡算法分配,同时经常发生扩缩容的情况,如果还用单机限流对应对分布式场景,会加剧误限、漏限,因此需要一种全局机制来保证全局配额。
  • 解决方案:
  1. 网关层限流:应用接入的网络架构中,在应用服务器之前往往有一层 LVS 或 Nginx 做统一入口,可以在这一层做入口的流控。本质上这就是单机流控的场景。
  2. 存储式限流:使用redis等缓存中间件来维护限流配置。redis官方给出的限流案例如下,下面代码还要加上分布式锁来保证线程安全。
FUNCTION LIMIT_API_CALL(ip):
current = GET(ip)
IF current != NULL AND current > 10 THEN
    ERROR "too many requests per second"
ELSE
    value = INCR(ip)
    IF value == 1 THEN
        EXPIRE(ip,1)
    END
    PERFORM_API_CALL()
END
  • 存在问题:限流器强依赖于Nginx和redis,当流量非常大时,Nginx或redis可能宕机。
  • 应对方案:
  1. 多层级限流:如网关层限流+存储式限流+单机限流同时生效,Nginx和redis同时维护各自的限流配额,只要有一方运行,限流就在进行;如果Nginx和redis都挂了,降级到单机限流进行兜底。
  2. 减少对redis的访问次数(降低精度):正常情况下,应用每接受一个请求,就需要请求一次redis,这样请求太频繁。现在预先给所有应用分配一定比例的配额(如50%),用完了再像redis申请,申请按照之前配额消耗的速率来,快的多,满的少。这样能大大减少对redis的请求次数,但是会发生漏限的情况,比如应用刚申请完配额,redis里面的配置就重置了,那应用申请的配额就用累加到这个时间窗口,导致实际liulai

 内容大部分来自:单机和分布式场景下,有哪些流控方案?

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值