自从上次博客被D后,我换到了更Hexo平台,原以为攻击就这么结束了,哪知攻击者转而瞄准我的评论系统继续发起攻击。
本来我打算使用Valine作为评论系统的,但因为各种原因,我想将所有数据放在自己手上,我按着Valine的样式重写了一个有后端的项目,后端是PHP写的,我可以很直接在Nginx上设置请求速率限制,以限制资源使用上限,不至于打到php挂掉。
开始
经过一番学习,我了解到Nginx有两个内置模块:
- limit_conn_module 限制连接模块
- 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地址(也可以是其它变量)
-
第一个参数
key
代表用哪个键进行判断,用的最多是客户端ip地址($binary_remote_addr
)这个键,如果你设置成了$server_name
表示会限制单一虚拟站点的总连接数,由于limit_conn_zone
可以多次使用声明多个不同的内存空间,由此可以灵活地限制ip连接数和虚拟站点站点连接数,这里我就不举例子了。 -
第二个参数
name
表示这个内存空间的名字,可以在后面的指令中引用这个内存空间 -
第三个参数
size
表示内存空间的大小,保存每个状态会消耗32个字节,如果是64位系统,会消耗64个字节,在64位系统上,1m空间大概能保存大约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
指令限制连接并发数
- 第一个参数
zone
需要填写上面的内存空间名字进行引用 - 第二个参数
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
的参数一样,但多了个rate
,rate
表示速率,通常以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];
用于限制请求处理速率,该限制基于令牌桶算法,注意不要和漏桶算法混淆。
漏桶算法,我的理解就是:你拿桶去水龙头接水,水满了会溢出来,溢出的水相对于是没有有效利用的资源,如果你非常需要水,水龙头也不会流的变快,还是那么慢。简单来说就是超时不候,多的没有。
令牌桶算法相对于漏桶算法的最大特点就是支持突发处理能力,这里不详细展开。
- 第一个参数
name
和limit_conn
指令中的意义一样 - 第二个参数
burst
表示突发请求的意思,如果请求速率超过了rate
设置的速率,多余的请求会被挂起,并丢到这个burst
队列延迟处理,而在突发队列也满了之后,后续的请求会直接返回503 - 第三个参数
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,像这样直到全部返回完。
同时加上burst
和nodelay
参数
还是同时发起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;
}