自从上次博客被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;
}