浏览 3.1k
NGINX速率限制是一个很重要的流量管理模块,用来限制单位时间的请求数。通过正确有效地配置,特定客户端对某一个URI的访问频率频率可以得到有效地限制, 从而可以有效地减缓暴力密码破解攻击,也可以有效减缓DDOS攻击的破坏性,还可以防止上游服务器被大量并发的请求耗尽资源。
本篇文章我们就速度限制功能的原理和源代码进行解析,从而可以更好地理解和使用速度限制功能。
漏桶(Leaky Bucket)算法和令牌桶(Token Bucket)算法被广泛使用于通信领域进行流量整形和速率控制。NGINX采用的是漏桶(Leaky Bucket)算法来实现速率控制。
漏桶(Leaky Bucket)算法思路很简单。我们可以把用户请求比做水先进入到漏桶里,漏桶以一定的速度出水(处理请求),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求。可以看出漏桶算法能强行限制数据的传输速率。
使用一幅经典的图片来解释漏桶算法的原理:
使用伪代码来表示漏桶算法就是:
int speed; //处理请求的速率,比如2r/s表示每秒处理2个请求
int requests; //当前系统的请求个数
int bucket; //系统的漏桶大小
time_t last_process_time; //上一次处理请求时间
int processed = 0;
/*当一个请求到达系统以后,计算从上一次处理请求到现在为止这段时间一共可以处理多少个请求。然后再计算系统当前未处理请求个数为多少。如果未处理的请求个数小于bucket大小,说明可以继续处理当前请求。反之,要把当前请求丢弃掉。*/
processed = (get_now_time() - last_porcess_time) * speed;
remain_requests = max(0, requests - processed);
last_process_time = get_now_time();
If (remain_requests < bucket ) {
requests = remain_requests ++;
return “可以处理请求”;
} else {
return “丢弃请求,不处理”;
}
NGINX通过limit_req_zone和limit_req两条指令来实现速率限制。指令limit_req_zone定义了限速的参数,指令limit_req在所在的location使能定义的速率。
配置语法:
Syntax:limit_req_zone key zone=name:size rate=rate;
Default:—
Context:http
参数key定义了基于什么样的参数进行速率控制。比如下面的例子中的$binary_remote_addr就是表示使用客户端的ip地址(remote_addr)进行速率控制。而且为了节省存储空间采用了二进制的表示方式。
参数zone定义一块共享内存区域。这片共享内存用来存储每一个ip地址的状态以及每个ip访问特定URL的频率。因为采用了共享内存的方式(关于NGINX共享内存可以参考这篇文章),所以,这些信息可以被所有的worker进程共用。通过此参数可以定义共享内存的名字和大小。指令limit_req通过这个共享内存的名字来对某一个URI进行速率控制。对于zone的大小,一般是每1M大小的空间可以存储16K不同客户的状态信息。当内存空间耗尽,再来新的连接时,会把最老的状态信息释放用来存储新的连接信息。而且,为了尽量防止zone空间被消耗空,NGINX每次创建新的状态信息时,会尝试删除几个前60秒内没有被使用的空间。
参数rate定义了最终要控制的速率。可以按秒为单位比如10r/s也可以按分钟为单位比如600r/m进行配置。最终NGINX会把所有的参数统一为按秒为单位。所以10r/s和600r/m效果是一样的。在最终的代码实现中,NGINX会以每一个请求间隔多少ms为单位进行控制。比如10r/s表示一秒只允许10个请求,也就是每100 ms允许一个请求。在这种情况下,如果处理完一个请求后,在100ms内到达的请求都会被丢掉。当然如果配置了burst参数效果会有不同。关于burst参数我们在指令limit_req中介绍。
配置语法如下:
Syntax:limit_req zone=name [burst=number] [nodelay | delay=number];
Default:—
Context:http, server, location
指令limit_req_zone定义了速率限制的参数。通过制定limit_req可以把定义的参数应用到所述的http, server或者location上下文中。
参数zone指明了要使用的哪一个limit_req_zone定义的参数。
参数burst用来处理突发请求。在上述limit_req_zone的例子中,任何在100ms之内到达的请求都对被丢弃掉。通过burst参数,可以定义个特定大小的queue,比如10。当一个请求早于100ms到达的请求可以先存放到queue中。如果queue满了,后续的请求都会被丢掉。然后对queue中的请求再按照100ms的频率去处理。
参数nodelay表示对于queue中的请求要立刻发送出去。默认不添加nodelay参数情况下queue中的请求会按照规定的速率比如100ms一个发送出。虽然queue中的请求被立刻发出去,但是queue中每一个请求对应的槽位只有在特定时候以后,我们的例子中是100ms,才能再次被使用。按照我们的例子,burst是10, 假如此时系统中10个burst槽位都是可以使用的,这是同时有20个请求到达,NGINX此时会立刻转发前11个请求(1+10 burst),同时会标注这10个burst定义的槽位不可以使用。同时对后面9个请求全部返回503状态。然后NGINX每隔100ms会释放一个burst槽位给后续请求使用。假如发送完前11个请求以后,再过了101ms,此时又连续达到20个请求,因为只有一个槽位可用,所以,NGINX会只转发1个请求,把剩余的19个请求全部返回503。假如转发完这一个请求后再过了501ms后又连续到达20个请求,因为501ms可以释放5个burst定义的槽位,所以此时NGINX会连续转发5个请求,然后对后面的15个请求返回状态503。这样运行整体的效果也会符合limit_req_zone定义的100ms处理一个请求的限制。
参数delay用来对流量进行两个阶段控制。配置了delay参数以后,比如配置limit_req zone=name burst=10 delay=3控制的效果是,对于burst中的10个请求,前面的10-3=7个请求按照nodelay的方式立刻发送出去。对于后面3个请求,按照指令定义的速率进行发送。
除了limit_req_zone和limit_req这两个功能指令以外,还有下列一些辅助指令。
Syntax:limit_req_dry_run on | off;
Default:
limit_req_dry_run off;
Context:http, server, location
使能演习(dry run)模式。在这种模式下,只会把请求的状态信息保存在共享内存中,限速功能不会生效。
Syntax:limit_req_log_level info | notice | warn | error;
Default:
limit_req_log_level error;
Context:http, server, location
设置与限速功能相关的log级别。比如设置为limit_req_log_level notice 则限速相关的log会按照notice的级别进行记录。
Syntax:limit_req_status code;
Default:
limit_req_status 503;
Context:http, server, location
设置NGINX拒绝连接请求时发送给客户端的HTTP代码,默认是503。
一个location块中可以配置多条limit_req指令。当符合请求的所有限制都被应用时,将采用最严格的那个限制。例如,多条匹配指令中有一条要拒绝这个请求,那么这个请求就被拒绝。如果匹配的指令都制定了延迟,将采用最长的那个延迟。
比如:
http {
limit_req_zone $uri zone=req_zone:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=req_zone_wl:10m rate=15r/s;
server {
location / {
limit_req zone=req_zone burst=10 nodelay;
limit_req zone=req_zone_wl burst=20 nodelay;
}
}
}
如果请求只会匹配到其中某一条limit_req,则按照匹配到的限速参数进行限速。如果两个限制能匹配到,则应用限制更强的每秒5个请求那个。
另外限速模块还可以和别的模块比如geo和map一起实现很多高级功能,比如只针对特定客户进行限速等等。
现在我们通过实验来验证NGINX在不同的配置情况下的速率限制行为。
客户端通过脚本发送HTTP请求到jikui.test.com服务器的path1路径上,然后我们通过NGINX服务器的access.log来观察NGINX在不同的配置策略下的处理行为,从而可以更深入地了解各个参数的含义。
用来发送HTTP请求的脚本存放在https://github.com/pei-jikui/limit_req中。
指令limit_req_zone定义了一个大小是10m的名称为one的区域用来根据客户端地址存储状态信息,然后速率的控制是每秒5个请求。
指令limit_req通过limit_req zone=one; 在服务器jikui.test.com的path1 URI中使能了上述速率限制定义。
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
server {
server_name jikui.test.com;
location path1 {
limit_req zone=one;
}
}
}
预期的效果是,NGINX每200ms处理一个HTTP请求。对于200ms之间的请求都会返回503。
观察log文件我们可以看到在17:05:11秒这一秒内按照每200ms一个的速率处理了5个请求,然后对于所有的200ms之间的请求都返回了503。
指令limit_req添加burst参数通过limit_req zone=one burst=10; 在服务器jikui.test.com的path1 URI中使能了上述速率限制定义。
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
server {
server_name jikui.test.com;
location path1 {
limit_req zone=one burst=10;
}
}
}
指令limit_req通过limit_req zone=one burst=10; 在服务器jikui.test.com的path1 URI中使能了上述速率限制定义而且定义了突发处理的容量是10。
预期的效果是,对于前11个请求后按照每200ms处理一个的速率进行处理。然后从第12个请求开始到下一个200ms期间(等待burst queue里面的槽位可以使用才可以处理新的请求)的所有请求都返回503。
通过把request的顺序作为脚本中curl命令的agent_name参数进行发送,我们可以从log中清楚地看到NGINX处理请求的行为。
我们可以看到,NGINX先处理request1,然后把request2-request11这10个request先queue起来。然后按照200ms的速率进行处理。对于request12到request20直接返回503。
指令limit_req添加burst和nodelay参数,通过limit_req zone=one burst=10 nodelay; 在服务器jikui.test.com的path1 URI中使能了上述速率限制定义。
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
server {
server_name jikui.test.com;
location path1 {
limit_req zone=one burst=10 nodelay;
}
}
}
指令limit_req通过limit_req zone=one burst=10 nodelay; 在服务器jikui.test.com的path1 URI中使能了上述速率限制定义而且定义了突发处理的容量是10。而且对于这些突发的请求,不再按照200ms的节奏去处理,而且立刻(nodelay)发送出去。
预期的效果是,对于前11个请求后会立刻进行处理。然后从第12个请求开始到下一个200ms期间(等待burst queue里面的槽位可以使用才可以处理新的请求)的所有请求都返回503。等发送第一个burst中的请求后200ms,NGINX又可以处理新的请求。
从log中我们可以看到,对于前11个请求,NGINX立刻进行了处理。然后在从处理第一个request之后的200ms之间(等待queue的第一个槽位可用),所有的请求都返回了503,然后200ms以后,queue的槽位有效以后立刻就可以再处理新的请求了。
指令limit_req添加burst和delay参数,通过limit_req zone=one burst=10 delay=3; 在服务器jikui.test.com的path1 URI中使能了上述速率限制定义。
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
server {
server_name jikui.test.com;
location path1 {
limit_req zone=one burst=10 delay=3;
}
}
}
指令limit_req通过limit_req zone=one burst=10 nodelay; 在服务器jikui.test.com的path1 URI中使能了上述速率限制定义而且定义了突发处理的容量是10。而且对于这些突发的10个请求,分两个阶段进行处理。第一个阶段前3个(delay参数定义的数量)立刻发送出,然后后面10-3=7个请求按照定义的速率每200ms处理一个。
预期的效果是,对于前11个请求中1+3=4个后会立刻进行处理。然后从第5-11个请求会按照200ms的速率进行处理。期间(等待burst queue里面的槽位可以使用才可以处理新的请求)的所有请求都返回503。等发送第11个请求处理完毕200ms后,NGINX又可以处理新的请求。
为了减小图片的大小,只列出了被接收处理的请求。从request4和request5开始每两个请求之间是200ms,这每个200ms之间的请求都会返回503。
从log可以看到,前面4个请求立刻得到了处理。然后后面的5-11个请求按照200ms的间隔进行处理。第47个请求是在第11个请求完成200ms后被处理的。
对于一个location添加多条limit_req不同速率控制的指令。为了演示效果,我们采用相同的key值。
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=two:10m rate=1r/s;
server {
server_name jikui.test.com;
location path1 {
limit_req zone=one;
limit_req zone=two;
}
}
}
对于每一个请求都会匹配到两条速率控制指令,控制更严格的每秒1个请求的指令将会被使用。
从log中我们可以看到,NGINX每秒放行一个请求。当然为了减小log的数量我们没有显示在每一秒之间所有被拒绝的请求。
本节,我们从源代码角度去分析速率控制功能是如何实现的。
速率控制主要是通过limit_req_zone和limit_req两条指令完成的。在NGINX的启动阶段对这两条指令进行解析。
指令limit_req_zone解析流程:
指令limit_req解析流程:
指令解析完毕以后相关数据结构的关系如下图所示:
NGINX速率限制对应的模块是ngx_http_limit_req_module,它处于HTTP的11个处理阶段的NGX_HTTP_PREACCESS_PHASE阶段,处理函数是ngx_http_limit_req_handler。
我们忽略NGINX是如何处理HTTP请求的,直接从函数ngx_http_limit_req_handler的处理逻辑开始分析。
函数ngx_http_limit_req_lookup的逻辑是:
函数ngx_http_limit_req_account的逻辑是:
函数ngx_http_limit_req_delay的逻辑是:
速率控制是NGINX流量管理功能中一个很重要的模块,通过它可以有效地保护上游服务器的安全和减缓上游服务器的负载。只有深入理解配置的几个参数比如burst, nodelay, delay的意义后才能正确的使用它。希望这篇文章能对您有所帮助。下篇我们将分析并发控制模块的使用和原理。
按点赞数排序
按时间排序
您好,我的nginx的限流配置为limit_req_zone $binary_remote_addr zone=reqconsole1:100m rate=1000r/s;我修改limit_req_zone的key从binary_remote_addr为client_real_ip,在reload nginx进程时error日志报错limit_req "reqconsole" uses the "$client_real_ip" key while previously it used the "$binary_remote_addr" key。并且nginx reload并不生效
请问这个和nginx的共享内存在reload时不释放引起的吗?
0
发表于2022-09-12 10:59