限制对nginx服务资源的使用
2019-04-20 10:02:44 阿炯

Nginx中用来限制用户在给定时间内HTTP请求的数量,可以是一个简单网站首页的GET请求,也可以是登录表单的POST请求。流量限制可以用作安全目的,比如可以减慢暴力密码破解的速率。通过将传入请求的速率限制为真实用户的典型值,并标识目标URL地址(通过日志),可以用来抵御DDOS或CC攻击。


nginx中使用ngx_http_limit_conn_module与ngx_http_limit_req_module模块的配合使用来实现此目的,前者针对的是连接(connection),而后者针对的是请求(request)。request和connection区别如下:

在nginx里面,limit_req和limit_conn都是用来限流的但是两者不在一个层次上,在开始之前,需要先清楚request和connect的区别:request是请求,是http层面的。connection是链接,指的是tcp层面。所以从含义上面可以知道两者不在一个层次。

我们在打开一个网页的时候,请求过程一般就是先经过tcp三次握手,然后在进行http请求。也就是一个connection建立之后,可以有很多个的request;一个connection建立,只要服务器处理能力够强,可以处理任意多的request都是没有问题的。

* ngx_http_limit_req_module:限制单个IP单位时间内的请求数,即速率限制,该模块默认已经安装

* ngx_http_limit_conn_module:限制单个IP同一时间连接数,即并发连接数限制


Nginx自身有的请求限制模块(ngx_http_limit_req_module)、流量限制模块(ngx_stream_limit_conn_module)基于令牌(漏)桶算法,可以方便的控制令牌速率,自定义调节限流,实现基本的限流控制。对于提供下载的网站,肯定是要进行流量控制的,例如软件下载站、视频服务等。它也可以减少一些爬虫程序或者DDOS的攻击。近年来Ng公司的官方文档《Limiting Access to Proxied HTTP Resources》有了很大的完善补充,比之于之前的手册参考好了很多。


Nginx的速率限制使用漏桶算法(leaky bucket algorithm),该算法在通讯和分组交换计算机网络中广泛使用,用以处理带宽有限时的突发情况。就好比向桶口在倒水,桶底在漏水的水桶;如果桶口倒水的速率大于桶底的漏水速率,桶里面的水将会溢出。同样,在请求处理方面,水代表来自客户端的请求,水桶代表根据先进先出调度算法(FIFO)等待被处理的请求队列,桶底漏出的水代表离开缓冲区被服务器处理的请求,桶口溢出的水代表被丢弃和不被处理的请求。


如对该算法感兴趣,可移步维基百科先行阅读,不过不了解此算法不影响阅读下文

1、空桶

我们从最简单的限流配置开始:
limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=10r/s;

server {
    location /login/ {
        limit_req zone=ip_limit;
        proxy_pass http://login_upstream;
    }
}

$binary_remote_addr 针对客户端ip限流
zone=ip_limit:10m 限流规则名称为ip_limit,允许使用10MB的内存空间来记录ip对应的限流状态
rate=10r/s 限流速度为每秒10次请求
location /login/ 对登录模块进行限流

限流速度为每秒10次请求,如果有10次请求同时到达一个空闲的nginx,它们都能得到执行吗?


漏桶漏出请求是匀速的。10r/s是怎样匀速的呢?每100ms漏出一个请求。在这样的配置下,桶是空的,所有不能实时漏出的请求,都会被拒绝掉。

所以如果10次请求同时到达,那么只有一个请求能够得到执行,其它的都会被拒绝。这不太友好,大部分业务场景下我们希望这10个请求都能得到执行。

2、Burst

我们把配置改一下,解决上一节的问题:
limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=10r/s;

server {
    location /login/ {
        limit_req zone=ip_limit burst=12;
        proxy_pass http://login_upstream;
    }
}

burst=12 漏桶的大小设置为12


逻辑上叫漏桶,实现起来是FIFO队列,把得不到执行的请求暂时缓存起来。

这样漏出的速度仍然是100ms一个请求,但并发而来,暂时得不到执行的请求,可以先缓存起来。只有当队列满了的时候,才会拒绝接受新请求。这样漏桶在限流的同时,也起到了削峰填谷的作用。在这样的配置下,如果有10次请求同时到达,它们会依次执行,每100ms执行1个。虽然得到执行了,但因为排队执行,延迟大大增加,在很多场景下仍然是不能接受的。

3、NoDelay


继续修改配置,解决Delay太久导致延迟增加的问题
limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=10r/s;

server {
    location /login/ {
        limit_req zone=ip_limit burst=12 nodelay;
        proxy_pass http://login_upstream;
    }
}

nodelay 把开始执行请求的时间提前,以前是delay到从桶里漏出来才执行,现在不delay了,只要入桶就开始执行



要么立刻执行,要么被拒绝,请求不会因为限流而增加延迟了。

因为请求从桶里漏出来还是匀速的,桶的空间又是固定的,最终平均下来,还是每秒执行了5次请求,限流的目的还是达到了。但这样也有缺点,限流是限了,但是限得不那么匀速。以上面的配置举例,如果有12个请求同时到达,那么这12个请求都能够立刻执行,然后后面的请求只能匀速进桶,100ms执行1个。如果有一段时间没有请求,桶空了,那么又可能出现并发的12个请求一起执行。

大部分情况下,这种限流不匀速,不算是大问题。不过nginx也提供了一个参数才控制并发执行也就是nodelay的请求的数量。


limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=10r/s;

server {
    location /login/ {
        limit_req zone=ip_limit burst=12 delay=4;
        proxy_pass http://login_upstream;
    }
}

delay=4 从桶内第5个请求开始delay



这样通过控制delay参数的值,可以调整允许并发执行的请求的数量,使得请求变的均匀起来,在有些耗资源的服务上控制这个数量,还是有必要的。



ngx_http_limit_conn_module模块指令

用ngx_http_limit_conn_module模块来限制连接数,需要通过配合ngx_http_limit_req_module模块来实现,该模块可以通过定义的键值来限制请求处理的频率。特别的,可以限制来自单个IP地址的请求处理频率。限制的方法如同漏斗,每秒固定处理请求数,推迟过多请求。

ngx_http_limit_conn_module对同一个ip的连接数,并发数进行限制。该模块可以根据定义的键来限制每个键值的连接数,如同一个IP来源的连接数。并不是所有的连接都会被该模块计数,只有那些正在被处理的请求(这些请求的头信息已被完全读入)所在的连接才会被计数。

ngx_http_limit_conn_module:限制IP连接数

ngx_http_limit_conn_module的模块也有2部分:
* http字段中定义触发条件,可以有多个条件;
* server、location段中定义达到触发条件时nginx所要执行的动作。


http段:
语法:limit_conn_zone key zone=name:memory_max_size;
实例:limit_conn_zone $binary_remote_addr zone=one:10m;
说明:limit_conn_zone模块只能配置在http字段中进行定义,该指令描述会话状态存储区域。主要用来定义变量、zone名称、共享内存大小,键的状态中保存了当前连接数,键的值可以是特定变量的任何非空值(空值不会被考虑)。size 定义各个键共享内存空间大小,如果共享内存空间被耗尽,服务器将会对后续所有的请求返回 503 (Service Temporarily Unavailable) 错误。从Nginx 1.1.8版本后limit_conn升级为limit_conn_zone。连接数限制不是所有的连接都计算在内;只有那些已请求该服务器并当前正在处理的请求(请求头已充分阅读的)。

server, location段:
1、limit_conn one
语法:limit_conn one conn_max_num;
实例:limit_conn one 20;
说明:该指令指定每个给定键值的最大同时连接数,当超过这个数字时返回503(Service )错误。如(同一IP同一时间只允许有20个连接)。

http {
limit_conn_zone  $binary_remote_addr  zone=one:10m;
server {
 listen 80;
 limit_conn one 20;
 }
}   

还可以叠加使用,同时限制访问的虚拟主机:

http {
limit_conn_zone  $binary_remote_addr  zone=perip:10m;
limit_conn_zone  $server_name  zone=perserver:10m;

server {
 listen 80;
 limit_conn perip 20; #同一个IP限制链接20个
 limit_conn perserver 100; #同一主机名链接限制在100个
 }
}   

2、limit_conn_log_level
语法: limit_conn_log_level info | notice | warn | error;
默认值: limit_conn_log_level error;
使用环境: http, server, location
设置触发最大限制后记录日志的级别,默认为error级别,该指令在 0.8.18版后新增

3、limit_conn_status
语法: limit_conn_status code;
默认值: limit_conn_status 503;
使用环境: http, server, location
当超出最大同时连接数的时候,对于新增连接返回的错误代码,默认503. 该指令在 1.3.15版本后新增

4、limit_rate
语法:limit_rate rate
默认值:0
实例:limit_rate 200k;
配置段:http, server, location, if in location
对每个连接的速率限制。参数rate的单位是字节/秒,设置为0将关闭限速。按连接限速而不是按IP限制,因此如果某个客户端同时开启了两个连接,那么客户端的整体速率是这条指令设置值的2倍。

上述指令如果没有看明白,那么再分别为各个指令讲解一次(示例可能有重复)。


limit_conn_zone
语法: limit_conn_zone $variable zone=name:size;
默认值: none
配置段: http

该指令描述会话状态存储区域。键的状态中保存了当前连接数,键的值可以是特定变量的任何非空值(空值将不会被考虑)。$variable定义键,zone=name定义区域名称,后面的limit_conn指令会用到的。size定义各个键共享内存空间大小,如:
limit_conn_zone $binary_remote_addr zone=addr:10m;

注释:客户端的IP地址作为键。注意,这里使用的是$binary_remote_addr变量,而不是$remote_addr变量。
$remote_addr变量的长度为7字节到15字节,而存储状态在32位平台中占用32字节或64字节,在64位平台中占用64字节。
$binary_remote_addr变量的长度是固定的4字节,存储状态在32位平台中占用32字节或64字节,在64位平台中占用64字节。
1M共享空间可以保存3.2万个32位的状态,1.6万个64位的状态。
如果共享内存空间被耗尽,服务器将会对后续所有的请求返回 503 (Service Temporarily Unavailable) 错误。
limit_zone 指令和limit_conn_zone指令同等意思,前者已经被弃用,就不再做说明了。


limit_conn_log_level
语法:limit_conn_log_level info | notice | warn | error
默认值:error
配置段:http, server, location

当达到最大限制连接数后,记录日志的等级。

limit_conn
语法:limit_conn zone_name number
默认值:none
配置段:http, server, location
指定每个给定键值的最大同时连接数,当超过这个数字时被返回503 (Service Temporarily Unavailable)错误。如:
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
    location /www.freeoa.net/ {
        limit_conn addr 1;
    }
}

limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
    location /www.freeoa.net/ {
        limit_conn addr 1;
    }
}

同一IP同一时间只允许有一个连接。当多个 limit_conn 指令被配置时,所有的连接数限制都会生效。比如,下面配置不仅会限制单一IP来源的连接数,同时也会限制单一虚拟服务器的总连接数:
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;

server {
    limit_conn perip 10;
    limit_conn perserver 100;
}

limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
server {
    limit_conn perip 10;
    limit_conn perserver 100;
}

limit_conn指令可以从上级继承下来。

limit_conn_status
语法: limit_conn_status code;
默认值: limit_conn_status 503;
配置段: http, server, location

该指令在1.3.15版本引入的。指定当超过限制时,返回的状态码,默认是503。

limit_rate
语法:limit_rate rate
默认值:0
配置段:http, server, location, if in location
对每个连接的速率限制。参数rate的单位是字节/秒,设置为0将关闭限速。按连接限速而不是按IP限制,因此如果某个客户端同时开启了两个连接,那么客户端的整体速率是这条指令设置值的2倍。

完整实例配置

http {
    limit_conn_zone $binary_remote_addr zone=limit:10m;
    limit_conn_log_level info;

    server {
        location  ^~ /download/ {
            limit_conn limit 4;
            limit_rate 200k;
            alias /data/www.freeoa.net/download/;
        }
    }
}


ngx_http_limit_req_module模块指令

ngx_http_limit_req_module:限制每秒请求数

ngx_http_limit_req_module模块通过漏桶原理来限制单位时间内的请求数,一旦单位时间内请求数超过限制,就会返回503错误。该模块有2部分:
* http字段中定义触发条件,可以有多个条件;
* server、location段中定义达到触发条件时nginx所要执行的动作。


http段:
语法:limit_req_zone $variable zone=name:size rate=rate;
实例:limit_req_zone $binary_remote_addr zone=two:100m rate=50r/s;
说明:
1)、$binary_remote_addr:是$remote_addr(客户端IP)的二进制格式,固定占用4个字节,而$remote_addr按照字符串存储,占用7-15个字节,用$binary_remote_addr可以节省空间。这样1M的内存可以保存大约1万6千个64字节的记录。
2)、zone=:区域名称,可自定义,这里写的是two,分配内存空间大小为10m,用来存储会话(二进制远程地址),如果限制域的存储空间耗尽了,对于后续所有请求,服务器都会返回503(Service Temporarily Unavailable)错误。
3)、rate=:平均处理的请求数,这里定义的是每秒不能超过每秒50次。也可以设置为每分钟处理请求数(r/m),其值必须是整数。

limit_req_zone指令一般定义在http块内部,使得该指令可以在多个环境中使用,该指令有下面三个参数(接上详述):

Key:在限流应用之前定义了请求的特征。在上面例子中,它是$binary_remote_addr(Nginx变量),该变量代表了某个客户端IP地址的二进制形式。这意味着我们可以将每个特定的IP地址的请求速率限制为第三个参数所定义的值(使用这个变量的原因是因为它比用string代表客户端IP地址的$remote_addr变量消耗更少的空间。)

Zone:定义了存储每个IP地址状态和它访问受限请求URL的频率的共享内存区域。将这些信息保存在共享内存中,意味着这些信息能够在Nginx工作进程之间共享。定义有两个部分:由zone=关键字标识的区域名称,以及冒号后面的区域大小。约16000个IP地址的状态信息消耗1M内存大小,因此我们的区域(zone)大概可以存储约160000个地址。

当Nginx需要添加新的记录时,如果此时存储耗尽了,最老的记录会被移除。如果释放的存储空间还是无法容纳新的记录,Nginx返回503 (Service Temporarily Unavailable)状态码。为了防止内存被耗尽,每次Nginx创建一个新的记录的同时移除多达两条前60秒内没有被使用的记录。

Rate:设置最大的请求速率。在上面的例子中,速率不能超过50个请求每秒。Nginx事实上可以在毫秒级别追踪请求,因此这个限制对应了1个请求每100毫秒。因为我们不允许突发(bursts,短时间内的突发流量,下有详述),这意味着如果某个请求到达的时间离前一个被允许的请求小于100毫秒,它会被拒绝。

limit_req_zone指令设置限流和共享内存区域的参数,但是该指令实际上并不限制请求速率。为了限制起作用,需要将该限制应用到某个特定的location或server块(block),通过包含一个limit_req指令的方式。


server, location段:
1、limit_req zone
语法: limit_req zone=name [burst=number] [nodelay];
实例:limit_req zone=two burst=20 nodelay;
说明:
1)、zone=:和http字段中limit_req_zone的zone定义必须一致
2)、burst=:可以理解为缓冲队列
3)、nodelay:对用户发起的请求不做延迟处理,而是立即处理。比如上面定义了rate=20r/s,即每秒钟只处理20个请求。如果同一时刻有22个请求过,若设置了nodelay,则会立刻处理这22个请求。若没设置nodelay,则会严格执行rate=20r/s的配置,即只处理20个请求,然后下一秒钟再处理另外2个请求。直观的感受就是页面数据卡了,过了一秒后才加载出来。

http {
......
limit_req_zone $binary_remote_addr zone=two:100m rate=50r/s;
server {
listen 80;
limit_req zone=two burst=20 nodelay;
}
}

对限流起作用的配置就是rate=50r/s和burst=20这两个配置,假如同一秒有55个请求到达nginx,其中50个请求被处理,另外的5个请求被放到burst缓冲队列里,由于配置了nodelay,第51-55个请求依然会被处理,但是会占用burst缓冲队列的5个长度,如果下一秒没有新的请求过来,这5个长度的空间会被释放,否则会继续占用burst缓冲队列5个长度,如果后面每秒请求都超过50个,那么多余的请求虽然会被处理但是会占用burst缓冲队列长度,直到达到设定的burst=20,后面的每秒第51个请求开始会被nginx拒绝,并返回503错误,被拒绝的请求在nginx的日志中可以看到被那个zone拒绝的。

2、limit_req_log_level
语法: limit_req_log_level info | notice | warn | error;
默认值: limit_req_log_level error;
配置段: http, server, location
设置日志级别,当服务器因为频率过高拒绝或者延迟处理请求时可以记下相应级别的日志。延迟记录的日志级别比拒绝的低一个级别,如果设置“limit_req_log_level notice”, 延迟的日志就是info级别。

3、limit_req_status
语法: limit_req_status code;
默认值: limit_req_status 503;
配置段: http, server, location
该指令在1.3.15版本引入。设置拒绝请求的响应状态码。

上述指令没有看明白,那么再分开各个指令讲一次。


limit_req_zone

语法: limit_req_zone $variable zone=name:size rate=rate;
默认值: none
配置段: http
设置一块共享内存限制域用来保存键值的状态参数,特别是保存了当前超出请求的数量。键的值就是指定的变量(空值不会被计算),如:
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

说明:区域名称为one,大小为10m,平均处理的请求频率不能超过每秒一次。
键值是客户端IP。使用$binary_remote_addr变量, 可以将每条状态记录的大小减少到64个字节,这样1M的内存可以保存大约1万6千个64字节的记录。如果限制域的存储空间耗尽了,对于后续所有请求,服务器都会返回 503 (Service Temporarily Unavailable)错误。速度可以设置为每秒处理请求数和每分钟处理请求数,其值必须是整数,所以如果你需要指定每秒处理少于1个的请求,2秒处理一个请求,可以使用 "30r/m"。

limit_req_log_level

语法: limit_req_log_level info | notice | warn | error;
默认值: limit_req_log_level error;
配置段: http, server, location
设置你所希望的日志级别,当服务器因为频率过高拒绝或者延迟处理请求时可以记下相应级别的日志。延迟记录的日志级别比拒绝的低一个级别;比如设置"limit_req_log_level notice",延迟的日志就是notice级别。

limit_req_status

语法: limit_req_status code;
默认值: limit_req_status 503;
配置段: http, server, location

该指令在1.3.15版本引入,设置拒绝请求的响应状态码。

limit_req

语法: limit_req zone=name [burst=number] [nodelay];
默认值: -
配置段: http, server, location
设置对应的共享内存限制域和允许被处理的最大请求数阈值。如果请求的频率超过了限制域配置的值,请求处理会被延迟,所以所有的请求都是以定义的频率被处理的。超过频率限制的请求会被延迟,直到被延迟的请求数超过了定义的阈值,这时该请求会被终止,并返回503 (Service Temporarily Unavailable) 错误。这个阈值的默认值为0,如:

limit_req_zone $binary_remote_addr zone=freeoa_net:10m rate=1r/s;
server {
location /app/ {
    limit_req zone=freeoa_net burst=5;
}
}

限制平均每秒不超过一个请求,同时允许超过频率限制的请求数不多于5个。如果不希望超过的请求被延迟,可以用nodelay参数,如:
limit_req zone=freeoa_net burst=5 nodelay;

limit_req_zone $binary_remote_addr zone=freeoa:10m rate=5r/s;
location {
  limit_req zone=freeoa burst=10 nodelay;
}

Ng将突发请求添加到延迟100毫秒一次的“队列”中,这会使站点看起来有些慢,因此nodelay参数会有条件的删除此队列延迟(直接丢弃)。对于当前配置,如果一次发出10个请求,nodelay参数将允许所有10个请求,然后将后面请求的速率限制为每秒5个请求。如果再提出6个请求,它将允许5个,并拒绝第6个,因为超出了限制。

limit_req zone=ip burst=10 delay=5;

在这里,前5个请求将立即通过。然后为每请求增加100ms被动延迟,允许客户机再发出5个请求,直到突发填满(10个)为止,之后它们将受到速率变量的限制。


limit_conn和limit_req变量可以在http, server, location使用,如果是限制nginx上的所有服务,所以添加到http里面(如果需要限制部分服务,可在nginx/conf/domains里面选择相应的server或者location添加)。

带宽速率限制
直接使用,无需定义容器区域(zone)
limit_rate 100k
limit_rate_after 1m

即下载完成1m后将速率限制在100 Kbps,这只是每一连接的限制,如果下载者开启多个连接进行下载还需要另外的连接并发限制:
limit_conn_zone $binary_remote_address zone=freeoa:10m

server {
  limit_conn freeoa 5;
  location /static/ {
    limit_conn freeoa 1;
    limit_rate 100k;
    limit_rate_after 1m;
  }
}


其它参数说明

处理流量突发(Bursts)
如果我们在1000毫秒内接收到2个请求该怎么办?对于第二个请求,Nginx将给客户端返回错误。这可能并不是我们想要的结果,因为应用本质上趋向于突发性。相反,我们希望缓冲任何超额的请求,然后及时地处理它们。在limit_req中使用burst参数:
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
upstream myweb {
    server 192.168.62.17:80 weight=1 max_fails=1 fail_timeout=1;
}

server {
listen 80;
server_name localhost;
location /login {
    limit_req zone=mylimit burst=20;
    proxy_pass http://myweb;
    proxy_set_header Host $host:$server_port;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

burst参数定义了超出zone指定速率的情况下(示例中的mylimit区域,速率限制在每秒10个请求,或每100毫秒一个请求),客户端还能发起多少请求。上一个请求100毫秒内到达的请求将会被放入队列,我们将队列大小设置为20。这意味着,如果从一个给定IP地址发送21个请求,Nginx会立即将第一个请求发送到上游服务器群,然后将余下20个请求放在队列中。然后每100毫秒转发一个排队的请求,只有当传入请求使队列中排队的请求数超过20时,Nginx才会向客户端返回错误。

无延迟排队
带有burst的配置产生平滑的网络流量,但是不实用,因为该配置会使得你的网站表现的很慢。在上面的例子中,队列中第20个数据包等待2秒才能被转发,这时该数据包的响应可能对于客户端已经没有了意义。为了处理这种情况,除了burst参数外,添加nodelay参数。
location /login/ {
limit_req zone=mylimit burst=20 nodelay;
proxy_pass http://my_upstream
}

带有nodelay参数,Nginx仍然会按照burst参数在队列中分配插槽(slot)以及利用已配置的限流,但是不是通过间隔地转发队列中的请求。相反,当某个请求来的太快,只要队列中有可用的空间(slot),Nginx会立即转发它。该插槽(slot)被标记为“已使用”,并且不会被释放给另一个请求,一直到经过适当的时间(在上面的例子中,是100毫秒)。

像之前一样假设有20个插槽的队列是空的,并且来自于给定的IP地址的21个请求同时地到达。Nginx立即转发这21个请求以及将队列中的20个插槽标记为“已使用”,然后每隔100毫秒释放一个插槽。(相反,如果有25个请求,Nginx会立即转发25个中的21个请求,标记20个插槽为“已使用”,并且用503状态拒绝4个请求。)

现在假设在转发第一个请求集合之后的101毫秒,有另外的20个请求同时地到达。队列中只有1个插槽被释放,因此Nginx转发1个请求,并且用503状态拒绝其它的19个请求。相反,如果在这20个新请求到达之前过去了501毫秒,则有5个插槽被释放,因此Nginx立即转发5个请求,并且拒绝其它15个请求。

效果等同于10个请求每秒的限流。如果你想利用请求之间的无限制性间隔的限流,nodelay选项则是非常有用的。

注意:对于大多数的部署,推荐在limit_req指令中包含burst和nodelay参数。

日志(Logging)
默认,NGNIX记录由于限流导致的延迟或丢弃的请求的日志,如下面的例子:
2018/05/13 04:20:00 [error] 120315#0: *32086 limiting requests, excess: 1.000 by zone "mylimit", client: 192.168.9.2, server: freeoa.net, request: "GET / HTTP/1.1", host: "freeoa.net"

该日志记录包含的字段:
limiting requests -- 日志条目记录了某个限流的标志

excess -- 超过这个请求代表的配置的速率的每毫秒请求数目

zone -- 定义了启用了限流的区域

client -- 产生请求的客户端IP地址

server -- 服务器的IP地址或主机名

request -- 客户端产生的实际的HTTP请求

host -- HTTP头部主机名的值

Nginx默认日志在error级别拒绝请求,如上面例子中的[error]所示(它在低一个级别上记录延迟的请求,因此默认是info),用limit_req_log_level指令来改变日志级别。下面我们设置在warn级别上记录被拒绝的请求的日志:
location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    limit_req_log_level warn;
    proxy_pass http://my_upstream;
}

定制发送给客户端的错误码
默认当某个客户端超过它的限流,Nginx用503(Service Temporarily Unavailable)状态码来响应。使用limit_req_status指令设置一个不同的状态码(在下面的例子是444):
location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    limit_req_log_level warn;
    limit_req_status 444;
}


完整实例配置

http {
limit_req_zone $binary_remote_addr zone=freeoa_net:10m rate=1r/s;

server {
location  ^~ /download/ {  
limit_req zone=freeoa_net burst=5;
alias /data/www.freeoa.com/download/;
}}
}

limit_rate limit_conn限制文件下载速度

使用Nginx的limit_rate和limit_rate_after这两个指令解决此问题,在一个location块使用这两个指令:
location ^~ /videos/ {
...
limit_rate_after 1m;
limit_rate 150k;
...
}

第一个指令limit_rate_after,从下载到你指定的文件大小之后开始限速,然后第二个指令limit_rate用于设置最高下载速度。

要注意的是上面的设置是限制的是每一个连接的下载速度,所以如果一个用户打开了多个连接下载,那么它的下载速度就能达到单个连接的限速乘以连接数。不过我们可以使用limit_zone和limit_conn这两个指令限制其连接数,例如:

在server块配置中:
limit_rate 128K;
limit_zone one $binary_remote_addr 10m;

在location配置块中:
limit_conn one 10;

上面的配置表示每个连接允许的速率为1Mbit,最大连接数10个。


关于limit_req和limit_conn的区别

limit_conn

http {
    limit_conn_zone $binary_remote_addr zone=one:20m;
    limit_conn one 1; #最多可以进行1个connection
    client_body_buffer_size 128M;
}

上面的配置文件的含义很明白,nginx只接受最多一个connect,我们使用ab命令查看下

$ ab -n10  http://192.168.8.1/ #默认通过一个connect送10个request

看一下nginx里面的日志
192.168.8.90 - - [20/Apr/2019:08:45:11 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:45:11 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:45:11 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:45:11 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:45:11 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:45:11 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:45:11 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:45:11 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:45:11 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:45:11 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"

在一个并发下,处理10个request下完全没有问题。接下来我们使用2个并发,2个请求试下,也就是两个用户,每个用户发送一个request。

$ ab -n2 -c2 http://192.168.8.1/

192.168.8.90 - - [20/Apr/2019:08:50:54 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:50:54 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"

由于nginx设置了至多限制一个并发,所以导致两个用户的请求只能有一个能被处理掉,另外一个返回了503。

limit_req


下面更改下nginx.conf配置文件

http {
    limit_conn_zone $binary_remote_addr zone=one:20m;
    limit_req_zone $binary_remote_addr zone=req_one:20m rate=1r/s;
    limit_conn one 20;
    limit_req zone=req_one burst=5;
}

上面的配置的含义是请求速率限制为1r/s,然后再缓存5个request,然后再依次处理请求(令牌桶算法)

$ ab -n10 -c10 http://192.168.8.1/

nginx日志如下:
192.168.8.90 - - [20/Apr/2019:08:58:02 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:58:02 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:58:02 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:58:02 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:58:02 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:58:03 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:58:04 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:58:05 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:58:06 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:08:58:07 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"

使用压测命令,10个并发,10个请求,根据配置的文件,当有请求过来,nginx先处理一个请求,然后将5个请求缓存下来(burst=5),根据设置的速率1r/s进行处理,也就是一共能够处理6个请求,其余的请求则被丢掉。

再继续更改下nginx配置文件(加入了nodelay参数)

http {
    limit_conn_zone $binary_remote_addr zone=one:20m;
    limit_req_zone $binary_remote_addr zone=req_one:20m rate=1r/s;
    limit_conn one 20;
    limit_req zone=req_one burst=5 nodelay;
}

增加了nodelay好像和不加区别较大。这是因为请求过来的时候不会被延迟,处理的总的请求数是burst+1,nodelay的其余请求马上被丢弃。

192.168.8.90 - - [20/Apr/2019:09:21:21 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:09:21:21 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:09:21:21 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:09:21:21 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:09:21:21 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:09:21:21 +0800] "GET / HTTP/1.0" 200 1826 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:09:21:21 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:09:21:21 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:09:21:21 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
192.168.8.90 - - [20/Apr/2019:09:21:21 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"

同样有4个连接被丢弃,但处理的速度却是非常快的。未安装前压测的话,因为有大量请求,所以access.log会有大量日志,而error.log日志文件基本上没有变化。有条件的建议用ab或者siege自测一下,下面为webbench的测试结果。

# webbench  -c 30 -t 30 http://xxx.com  
Benchmarking: GET http://xxx.com  
30 clients, running 30 sec.
Speed=193468 pages/min, 1254317 bytes/sec.
Requests: 96000 susceed, 0 failed.

适用上限制上后会发现很多超出的请求会返回503,access.log日志变化不快而error.log中有大量记录,提示limit_reque缓住了多少请求。

# webbench  -c 30 -t 30 http://xxxx.com
Benchmarking: GET http://xxx.com  
30 clients, running 30 sec.
Speed=120 pages/min, 778 bytes/sec.
Requests: 60 susceed, 0 failed.


总结

request和connect是完全不同层面的含义,一个属于http,一个属于tcp。

limit_req zone=req_zone burst=5 依照在limti_req_zone中配置的rate来处理请求,同时设置了一个大小为5的缓冲队列,在缓冲队列中的请求会等待慢慢处理,超过了burst缓冲队列长度和rate处理能力的请求被直接丢弃,表现为对收到的请求有延时。

limit_req zone=req_zone burst=5 nodelay 依照在limti_req_zone中配置的rate来处理请求,同时设置了一个大小为5的缓冲队列,当请求到来时不会被延迟,对于峰值处理数量之外的请求,直接丢弃。


如果觉得上述的设置方法过于复杂的话,可以考虑使用iptables。

iptables设置

单个IP在60秒内只允许新建20个连接到达Web的80端口
iptables -I INPUT -i eth1 -p tcp -m tcp --dport 80 -m state --state NEW -m recent --update --seconds 60 --hitcount 20 --name DEFAULT --rsource -j DROP

iptables -I INPUT -i eth1 -p tcp -m tcp --dport 80 -m state --state NEW -m recent --set --name DEFAULT --rsource

单个IP的最大并发连接数为20
iptables -I INPUT -p tcp --dport 80 -m connlimit --connlimit-above 20 -j REJECT

每个IP最多20个初始连接
iptables -I INPUT -p tcp --syn -m connlimit --connlimit-above 20 -j DROP

这样的好处在网络层就处理了,减轻了nginx主机的一些负担,代价是规则有些过于刚性,对人多的公共出口办公室上网不友好。