一、限流算法

1、固定窗口

固定窗口就是定义一个固定的统计周期,比如 1 分钟或者 30 秒、10 秒这样,然后在每个周期统计当前周期中接收到的请求数量,经过计数器累加后如果达到设定的阈值就触发流量干预。直到进入下一个周期后,计数器清零,流量接收恢复正常状态。

nginx 限流 延迟处理 nginx限流策略_nginx

2、滑动窗口

滑动窗口其实就是对固定窗口做了进一步的细分,将原先的粒度切得更细,比如 1 分钟的固定窗口切分为 60 个 1 秒的滑动窗口。然后统计的时间范围随着时间的推移同步后移。

nginx 限流 延迟处理 nginx限流策略_共享内存_02

3、令牌桶

大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。

nginx 限流 延迟处理 nginx限流策略_限流策略_03

4、漏桶

漏桶模式的核心是固定“出口”的速率,不管进来多少量,出去的速率一直是这么多。如果涌入的量多到桶都装不下了,那么就进行流量干预。

nginx 限流 延迟处理 nginx限流策略_nginx 限流 延迟处理_04

四种算法的比较

算法

特点

固定窗口

流量进入速度波动,会出现:计数器提前计满,导致这个周期内剩下时间段的请求被限制;计数器计不满(限流阈值设定过大),导致资源无法充分利用。

滑动窗口

适用于对异常结果高容忍的场景(相比“两桶”少了缓冲区)

固定窗口

流量进入速度波动,会出现:计数器提前计满,导致这个周期内剩下时间段的请求被限制;计数器计不满(限流阈值设定过大),导致资源无法充分利用。

令牌桶

允许一定程度的突发流量,流量进入波动不是很大时(不至于一瞬间取完令牌,压垮后端系统),可以使用这个策略。

漏桶

宽进严出的思路在保护系统的同时还留有一些余地,适用场景更广。

二、限流方案的选择

1、接入层 nginx + lua

优点:准确,分布式限流
缺点:限流算法自己实现,耗费时间长

2、接入层 nginx的限制模块

优点:代码无侵入,实现简便,分布式限流
缺点:不能自定义算法

3、应用层

优点:可以自定义限流算法,也可以直接使用框架自带限流类
缺点:代码侵入,单机限流(如果使用redis+lua则可实现分布式限流)

三、nginx限流模块介绍

1、ngx_http_limit_req_module

用于限制每个定义键的请求处理速度。这种限制是使用“漏桶”方法实现的。

# 示例
http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

    ...

    server {

        ...

        location /search/ {
            limit_req zone=one burst=5;
        }

Syntax: limit_req zone=name [burst=number] [nodelay | delay=number];
Default: —
Context: http, server, location

设置共享内存区域(zone=name)和请求的最大突发大小(burst=5)。如果请求速率超过为区域配置的速率(rate=1r/s),则延迟处理请求,以定义的速率处理请求。过多的请求会被延迟,直到它们的数量超过最大突发大小,在这种情况下,请求会因错误而终止。默认情况下,最大突发大小等于零。
如果不希望在请求受到限制时延迟过多的请求,则应使用参数nodelay,delay参数指定了过度请求延迟的限制。默认值为零,即所有过量的请求都被延迟。
nodelay 针对的是 burst 参数,burst=5 nodelay 表示这5个请求立马处理,不能延迟,相当于特事特办。不过,即使这5个突发请求立马处理结束,后续来了请求也不会立马处理。burst=5 相当于缓存队列中占了5个位置,即使请求被处理了,这5个位置这只能按 1秒一个来释放。

Syntax: limit_req_log_level info | notice | warn | error;
Default: limit_req_log_level error;
Context: http, server, location

设置所需的日志记录级别,用于服务器因速率超过或延迟请求处理而拒绝处理请求的情况。延迟日志记录级别比拒绝日志记录级别低1点;例如,如果指定了“limit_req_log_level notice”,则使用info级别记录延迟。

Syntax: limit_req_status code;
Default: limit_req_status 503;
Context: http, server, location

设置状态代码以响应被拒绝的请求。

Syntax: limit_req_zone key zone=name:size rate=rate [sync];
Default: —
Context: http

设置共享内存区域的参数,该区域将保存各种键的状态。特别是,状态存储过多请求的当前数量(size)。键可以包含文本、变量及其组合。键值为空的请求不被计算。
如果区域存储耗尽,则删除最近最少使用的状态。即使在此之后无法创建新状态,请求也会因错误而终止。
速率以每秒请求数(r/s)指定。如果需要每秒少于一个请求的速率,则在每分钟请求(r/m)中指定。例如,每两秒一个请求是30r/m。
sync参数支持共享内存区域的同步。(启用集群节点之间共享内存区域的同步)
1M区域可以保存大约16000个64字节的状态,或者大约8000个128字节的状态。

2、ngx_http_limit_conn_module

用于限制每个定义键的连接数。
并不是所有的连接都被计算在内。只有当服务器正在处理一个请求并且整个请求头已经被读取时,才会计算连接。

# 示例
http {
    limit_conn_zone $binary_remote_addr zone=addr:10m;

    ...

    server {

        ...

        location /download/ {
            limit_conn addr 1;
        }

Syntax: limit_conn zone number;
Default: —
Context: http, server, location

设置共享内存区域和给定键值的最大允许连接数。当超过此限制时,服务器将返回错误以响应请求。
在HTTP/2和SPDY中,每个并发请求都被认为是一个单独的连接。
可以有几个limit_conn指令。

Syntax: limit_conn_log_level info | notice | warn | error;
Default: limit_conn_log_level error;
Context: http, server, location

当限制连接数量时设置所需的日志记录级别。

Syntax: limit_conn_status code;
Default: limit_conn_status 503;
Context: http, server, location

设置状态代码以响应被拒绝的请求。

Syntax: limit_conn_zone key zone=name:size;
Default: —
Context: http

设置共享内存区域的参数,该区域将保存各种键的状态。特别是,状态包含当前连接数。键可以包含文本、变量及其组合。键值为空的请求不被计算。
1M区域可以保存大约3.2万个32字节的状态,或者大约1.6万个64字节的状态。如果区域存储耗尽,服务器将把错误返回给所有后续请求。

四、限流key

可以使用nginx的内置变量,也可以自定义:

$binary_remote_addr:二进制的IP地址(官方推荐使用这种方式表示的IP)
$http_header:某个请求头(http_表示http请求,后面跟转化为小写形式、用下划线连接的请求头字段)
$uri:请求中的当前URI(不带请求参数,参数位于$args)
...

例如:

  • 根据uri,可以限制某接口的总请求速率
  • 根据ip,限制每个ip的请求速率

五、限流指标

需要参考:

  • 性能压测数据
  • 业务预期流量
  • 线上监控数据