自从上次博客被D后,我换到了更Hexo平台,原以为攻击就这么结束了,哪知攻击者转而瞄准我的评论系统继续发起攻击。

本来我打算使用Valine作为评论系统的,但因为各种原因,我想将所有数据放在自己手上,我按着Valine的样式重写了一个有后端的项目,后端是PHP写的,我可以很直接在Nginx上设置请求速率限制,以限制资源使用上限,不至于打到php挂掉。

开始

经过一番学习,我了解到Nginx有两个内置模块:

  1. limit_conn_module 限制连接模块
  2. limit_req_module 限制请求模块

HTTP协议是基于TCP协议的,所以一个TCP连接至少可以产生一个请求(支持TCP复用的情况下可以有多个请求)

协议版本 连接关系
HTTP1.0 TCP不能复用
HTTP1.1 顺序性TCP复用
HTTP2.0 多路TCP复用

限速率原理

当Nginx收到请求时会进行判断,如果内存里找不到这个ip,会放行并记录下这个ip。如果找到且离上次请求时间非常短,会返回503,否则的话会放行并再次记录下这个ip

limit_conn_zone 指令

# 支持的上下文: http
# 语法:
limit_conn_zone key zone=name:size;

表示申请一块内存,用键值对的形式来记录一些连接状态,这些Keys通常是客户端ip地址(也可以是其它变量)

  1. 第一个参数key代表用哪个键进行判断,用的最多是客户端ip地址($binary_remote_addr)这个键,如果你设置成了$server_name表示会限制单一虚拟站点的总连接数,由于limit_conn_zone 可以多次使用声明多个不同的内存空间,由此可以灵活地限制ip连接数和虚拟站点站点连接数,这里我就不举例子了。

  2. 第二个参数name表示这个内存空间的名字,可以在后面的指令中引用这个内存空间

  3. 第三个参数size表示内存空间的大小,保存每个状态会消耗32个字节,如果是64位系统,会消耗64个字节,在64位系统上,1m空间大概能保存

    1024×1024÷64=16,3841024 \times 1024 \div 64 = 16,384

    大约1.6万个,当空间用尽后,服务器会对后续请求直接返回503

例子:

http {
    # 1m的内存空间,按客户端ip进行判断
    limit_conn_zone $binary_remote_addr zone=addr:1m;

limit_conn 指令

# 支持的上下文: http, server, location
# 语法:
limit_conn zone number;

配合上面的limit_conn_zone指令限制连接并发数

  1. 第一个参数zone需要填写上面的内存空间名字进行引用
  2. 第二个参数number就是并发数的限制,如果设为3number=3,表示同时只能有3个连接

例子:

http {
    # 1m的内存空间,按客户端ip进行判断
    limit_conn_zone $binary_remote_addr zone=addr:1m;

    server {
        location /download/ {
            # 同一个ip在同一时间只能有1个连接
            limit_conn addr 1;

limit_req_zone 指令

# 支持的上下文: http
# 语法:
limit_req_zone key zone=name:size rate=rate;

参数和 limit_conn_zone 的参数一样,但多了个raterate表示速率,通常以s为单位(rate=1r/s),也可以用分钟m(30r/m),每分钟30次。

例子:

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=30r/m;

limit_req 指令

# 支持的上下文: http, server, location
# 语法:
limit_req zone=name [burst=number] [nodelay];

用于限制请求处理速率,该限制基于令牌桶算法,注意不要和漏桶算法混淆。

漏桶算法,我的理解就是:你拿桶去水龙头接水,水满了会溢出来,溢出的水相对于是没有有效利用的资源,如果你非常需要水,水龙头也不会流的变快,还是那么慢。简单来说就是超时不候,多的没有。

令牌桶算法相对于漏桶算法的最大特点就是支持突发处理能力,这里不详细展开。

  1. 第一个参数namelimit_conn 指令中的意义一样
  2. 第二个参数burst表示突发请求的意思,如果请求速率超过了rate设置的速率,多余的请求会被挂起,并丢到这个burst队列延迟处理,而在突发队列也满了之后,后续的请求会直接返回503
  3. 第三个参数nodelay,加上nodelay参数后会使得limit_req 指令拥有一个突发处理能力,可以在短时间内突破rate设置的处理上限,峰值速度 = rate + burst,具体请看下面的解析

burst 和 nodelay 参数详解

几乎所有文章把这两个参数讲得很模糊,除了这一篇文章,真的是非常非常的详细,感兴趣的话一定要去原文看看哦。我这里就简单总结一下这篇文章。

下面的的说明均以这个配置作为参考

limit_req_zone $binary_remote_addr zone=req_zone:1m rate=1r/m;

server {
    location / {
        #limit_conn conn_zone 3;
        #limit_req zone=req_zone burst=3;
        #limit_req zone=req_zone burst=3 nodelay;

不加burst参数

请求处理速度会严格按照rate设置的进行,超过rate速率的请求会直接返回503。如果同时发起10个请求,只有一个请求会成功,返回200,其它9个请求会被立即返回503

只加burst参数

会把额外的请求暂时放到burst队列里挂起,然后按着rate的速率依次处理,超过burst容量的那部分请求会立即返回503。还是同时发起10个请求,1个请求会立即返回200,3个请求会挂起,不会立即返回,同时剩余的6个请求会立即返回503。等到一分钟过去后,第二个请求被处理,返回200,再一分钟后,第三个请求返回200,像这样直到全部返回完。

同时加上burstnodelay参数

还是同时发起10个请求时,但10个请求全部没有等待的过程,全都立即返回了。有4个请求会立即返回200,还有6个请求会立即返回503。

有意思的地方来了,明明设置了rate=1r/m啊,怎么会超过了rate的限制呢,其实是这样的:在加上nodelay参数后,burst已经不再是原来的等待队列了,更像是变成了一个计数器,当10个请求到达时,其中1个请求被正常处理,返回200,再有3个请求被视为是突发请求,同样会被立即返回,在返回这3个突发请求后,会消耗掉burst的三个突发处理次数,即burst从3变为0,因为被消耗掉了,最后的6个请求会被立即返回503,因为超过了突发处理峰值能力(这个能力的计算方法可以参考limit_req指令中的讲解)。

如果此时再立即发起10个请求,10个请求会全部返回503。

如果是一分钟后再发起10个请求,仅有1个请求会返回200,另外9个返回503。

如果是两分钟后再发起10个请求,有2个请求会返回200,另外8个返回503。

突发处理次数的那个计数器会随着rate的恢复而恢复,这里我设置的是每分钟+1,每一个请求到达时都会消耗掉1个,如果等于0了不够了,就会返回503。当突发处理次数慢慢恢复满了以后,多余的次数就会溢出被丢掉。

完整例子:

http {
    # 1m的内存空间,按客户端ip进行判断
    limit_conn_zone $binary_remote_addr zone=addr:1m;
    # 10m的内存空间,按客户端ip进行判断,每分钟最多处理30个请求
    limit_req_zone $binary_remote_addr zone=one:10m rate=30r/m;

    server {
        location /search/ {
            # 同一个ip在同一时间只能有1个连接
            limit_conn addr 1;
            
            #limit_conn conn_zone 3;
            #limit_req zone=req_zone burst=3;
            #limit_req zone=req_zone burst=3 nodelay;
        }

资料参考:

  1. Module ngx_http_limit_req_module NGINX官方文档
  2. Nginx下limit_req模块burst参数超详细解析 超详细的burst参数解析
  3. nginx请求限制 - 知乎
  4. nginx的请求限制(连接限制和请求限制)
  5. nginx 请求限制和访问控制 - Crazymagic - 博客园