zoukankan      html  css  js  c++  java
  • 限流算法和nginx请求限流

    一、限流算法

    常见的限流算法有计数器(固定窗口)、滑动窗口、漏桶、令牌桶

    1、计数器(固定窗口)

    最简单的限流算法,计数器限制每一分钟或者每一秒钟内请求不能超过一定的次数,在下一秒钟计数器清零重新计算

    计数器限流存在一个缺陷,比如限制每分钟访问不能超过100次,客户端在第一分钟的59秒请求100次,在第二分钟的第1秒又请求了100次,那么在这2秒内后端会受到200次请求的压力,形成了流量突刺

    2、滑动窗口

    滑动窗口其实是细分后的计数器,它将每个时间窗口又细分成若干个时间片段,每过一个时间片段,整个时间窗口就会往右移动一格

    比如限制每分钟访问不能超过100次,如图每分钟被分成了4个时间片段,每个时间片段15秒,假设客户端在第一分钟的50秒请求了100次,时间到了第二分钟的10秒,时间窗口向右滑动一格,这时这个时间窗口其实已经打满了100次,客户端将被拒绝访问

    时间窗口划分的越细,滑动窗口的滚动就越平滑,限流的效果就会越精确

    3、漏桶

    漏桶算法类似一个限制出水速度的水桶,通过一个固定大小FIFO队列+定时取队列元素的方式实现,请求进入队列后会被匀速的取出处理(桶底部开口匀速出水),当队列被占满后后来的请求会直接拒绝(水倒的太快从桶中溢出来)

    漏桶桶的优点是可以削峰填谷,不论请求多大多快,都只会匀速发给后端,不会出现突刺现象,保证下游服务正常运行

    缺点就是在桶队列中的请求会排队,响应时间拉长

    4、令牌桶

    令牌桶算法是以一个恒定的速度往桶里放置令牌(如果桶里的令牌满了就废弃),每进来一个请求去桶里找令牌,有的话就拿走令牌继续处理,没有就拒绝请求

    令牌桶的优点是可以应对突发流量,当桶里有令牌时请求可以快速的响应,也不会产生漏桶队列中的等待时间

    缺点就是相对漏桶一定程度上减小了对下游服务的保护

    二、nginx请求限流(ngx_http_limit_req_module)

    对于nginx接入层限流可以使用nginx自带的两个模块:连接数限流模块ngx_http_limit_conn_module和漏桶算法实现的请求限流模块ngx_http_limit_req_module,还可以使用OpenResty提供的Lua限流模块lua-resty-limit-traffic进行更复杂的限流场景

    本文只介绍请求限流模块ngx_http_limit_req_module,主要的指令是limit_req_zone和limit_req

    1、指令介绍

    (1)limit_req_zone

    设置共享内存区域的大小和请求的速率

    语法:limit_req_zone key zone=name:size rate=rate;
    位置:http
    版本:1.7.6之前key只可包含一个变量
    示例:limit_req_zone $binary_remote_addr zone=test123:10m rate=10r/s;
    

    key

    定义要限流的对象,通常是nginx内置变量,多个key可以用逗号分隔,示例中$binary_remote_addr是限制每个ip的请求速率

    一般有$binary_remote_addr(客户的ip)、$server_name(服务器名称)、$uri(不带参数的请求地址)、$request_uri(带参数的请求地址),更多变量可以在nginx包的"srchttp gx_http_variables.c"文件中查看,或者查看本文的最后

    zone

    定义存放限流信息的共享内存区域,记录每类客户端的访问频率,在worker进程间共享,size表示区域大小

    比如使用$binary_remote_addr的情况,“binary_”表示内存占用量经过缩减,IPv4固定占用4字节、IPv6固定占用16字节,在32位系统,每一个IP在32位系统将占用64字节、在64位系统将占用128字节来保存状态,1m空间在32位系统能保存1w6多个IP的状态,在64位系统能保存8k多个IP的状态

    当内存空间耗尽时nginx使用lru算法淘汰最长时间未使用的key,如果释放的空间仍不足以容纳新记录,nginx将直接限制请求返回状态码,所以需要提前预估key的数量分配合理的内存空间,避免指定的内存空间被耗尽

    rate

    设置最大请求速率,在示例中速率不能超过每秒10个请求,nginx以毫秒粒度跟踪请求,因此实际上是限制每100ms1个请求

    如果希望限制每分钟可以指定“r/m”

    limit_req_zone指令只是定义了共享区域和速率的参数,实际并没有限制请求,需要在server或者location中设置limit_req来搭配使用

    (2)limit_req

    设置所属共享区域名称和请求最大突发大小,并在指令出现的上下文中启用速率限制

    语法:limit_req zone=name [burst=number] [nodelay | delay=number];
    位置:http, server, location
    版本:1.15.7后可以使用delay参数
    示例:limit_req zone=test123 burst=5;
    

    zone:和需要对应的limit_req_zone内存区域名称一致

    burst:可选参数,设置允许突发请求的数量

    nodelay:无延迟排队

    delay:分段限速

    burst、nodelay、delay参数不同的组合可以产生4种限流效果,在下一节限流效果演示中会逐一说明

    指令可以叠加使用,示例中配置了单个ip地址的处理速度,同时限制了整个服务的处理速度

    limit_req_zone $binary_remote_addr zone=perip:10m rate=1r/s;
    limit_req_zone $server_name zone=perserver:10m rate=10r/s;
    
    server {
        ...
        limit_req zone=perip burst=5 nodelay;
        limit_req zone=perserver burst=10;
    }
    

    将基本速率限制与其他nginx功能结合使用,可以实现更细微的流量限制,比如搭配geo和map指令可以实现对来自不在“白名单”上的任何人的请求施加速率限制:

    geo $limit {
        default 1;
        10.0.0.0/8 0;
        192.168.0.0/24 0;
    }
     
    map $limit $limit_key {
        0 "";
        1 $binary_remote_addr;
    }
     
    limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;
     
    server {
        location / {
            limit_req zone=req_zone burst=10 nodelay;
     
            # ...
        }
    }
    (3)limit_req_log_level

    设置速率超出而拒绝请求或延迟请求处理的日志记录级别

    语法:limit_req_log_level info | notice | warn | error;
    默认:error
    位置:http, server, location
    版本:该指令出现在版本0.8.18以后
    

    延迟请求比拒绝请求第一个等级,比如配置的是error,拒绝请求日志记录为error,延迟请求日志记录为warn

    (4)limit_req_status

    设置响应被拒绝请求的状态码

    语法:limit_req_status code;
    默认:503
    位置:http, server, location
    版本:该指令出现在1.3.15版以后
    
    (5)limit_req_dry_run

    启用空运行模式,开启后请求速率不受限制,但在共享内存区域中请求的数量将照常计算

    语法:limit_req_dry_run on | off;
    默认:off	
    位置:http, server, location
    版本:该指令出现在1.17.1版以后
    

    2、限流效果演示

    (1)无burst的情况

     没有配置burst桶容量,桶容量为0,按照固定速率处理请求,如果请求被限流,直接返回503

    limit_req_zone $server_name zone=test123:10m rate=50r/s;
    limit_req zone=test123;
    jmeter线程数1,次数20
    

    可以看到请求每隔20ms成功一次

    (2)burst的情况

    配置了burst桶容量,没有配置nodelay就是延迟模式,来不及处理的请求会进入桶中,桶内的请求会以固定速率被处理,如果桶满了,新进入的请求被限流

    limit_req_zone $server_name zone=test123:10m rate=2r/s;
    limit_req zone=test123 burst=3;
    jmeter线程数6,一起请求2次,2批间隔300ms
    

    速率为500ms成功一次,设置了burst桶容量为3,相当于一个长度3的缓冲队列

    我们的预期是当同时有6个请求到达时,nginx将第1个请求立即处理,并将其余3个请求放入桶队列,然后它每500毫秒处理一个排队的请求,在请求使排队请求的数量超过3时返回503到客户端

    第一次6个请求进入后,请求1-1第一个被执行,请求1-6、1-3、1-4幸运的进入桶队列中等待匀速执行,看到这4个请求间隔500ms,请求1-5、1-2因为来不及处理且桶满了被限流

    第二次6个请求在最后一个请求1-4执行完后间隔300ms进入,这时距离下一次还不到500ms,所以新的请求得先进入桶队列中等待,看到请求1-3、1-5、1-6幸运的进入桶队列中,请求1-4、1-2被限流

    如果2批请求间隔600ms呢?

    那第二批请求将会成功4个,和第一批的情况一样

    (3)burst+nodelay的情况

    配置了burst桶容量,同时配置了nodelay就是非延迟模式,桶队列是一个有状态的插槽队列,当请求“过早”到达时,只要桶队列中有可用的插槽,nginx就会立即处理请求,并将该插槽标记为“已占用”,当某一次限流间隔过后没有请求时,该插槽就会被标记为“可用”

    这种逻辑和令牌桶非常像,只要桶的插槽没有被占用完,突发的请求就能迅速被处理,不用像延迟模式一样需要进入队列排队等待,在流量洪峰过去后插槽可以慢慢被恢复,类似令牌慢慢被填充满桶

    limit_req_zone $server_name zone=test123:10m rate=2r/s;
    limit_req zone=test123 burst=3 nodelay;
    jmeter线程数6,一起请求2次,2批间隔600ms
    

    速率为500ms成功一次,设置了burst桶容量为3,相当于有3个插槽可用

    我们的预期是当同时有6个请求到达时,nginx将第1个请求立即处理,同时也立即处理之后3个请求,同时将桶中的3个插槽标记占用,将其他2个请求限流,在第二批请求间隔600ms到达后,有一个请求被处理

    第一次6个请求进入后,请求1-6第一个被执行,请求1-4、1-1、1-5幸运的使用了桶队列中的插槽被执行,看到这4个请求没有等待时间都是立即执行,请求1-3、1-2因为来不及处理且桶插槽用完被限流

    第二次6个请求在第一批请求执行完后间隔600ms进入,这时距离下一次间隔超过了500ms,一个插槽被重置,请求1-5进入后幸运的使用了这个插槽被执行,其他5个请求因为来不及处理且桶插槽用完被限流

    再看一下复杂一点的情况:

    limit_req_zone $server_name zone=test123:10m rate=2r/s;
    limit_req zone=test123 burst=3 nodelay;
    jmeter线程数6,一起请求7次,分别间隔1000ms、300ms、300ms、300ms、1500ms、2000ms
    

    一样的参数,线程请求7次,每批分别间隔1000ms、300ms、300ms、300ms、1500ms、2000ms

    第一批序号1-6同第一个例子

    第二批序号7-12,因为这次间隔是1000ms,所以有2个插槽被重置,成功了2个请求

    第三批序号12-18,因为间隔300ms太短,没有到500ms间隔,请求全部限流

    第四批序号19-24,序号1的第一个请求是51.815执行的,每隔500ms恢复一个插槽的话,第3次恢复是在53.315,刚好53.447序号19的请求1-1拿到了这个插槽,之后其他请求被限流

    第五批序号25-30,间隔300ms,因为上一批53.447刚用掉插槽,下一个插槽恢复是在53.815(51.815+4*500ms),这一批是53.754没有到时间,所以请求全部被限流

    第六批序号31-36,间隔了1500ms,到55.261已经恢复了3个插槽了(53.815,54.315,54.815),所以成功了3个请求

    第七批序号37-42,间隔了2000ms,到57.307恢复了4个插槽(55.315,55.815,56.315,56.815),所以成功了4个请求

    (4)burst+delay的情况

    配置了burst桶容量和delay参数后,就是部分延迟模式,比如burst=12,delay=8,则桶的前8位是插槽队列,后4位是缓冲队列

    假设有这样的配置:

    limit_req_zone $server_namezone=test123:10m rate=5r/s;
    limit_req zone=test123 burst=12 delay=8;
    

    该配置最多允许12个突发请求,其中前8个突发请求将被立即处理,后4个请求被强制以5 r / s的匀速执行,在缓冲队列空出之前多于12个的请求被限流

    使用此配置后,以8 r / s连续发出请求流的客户端将表现为图中的情况

    通过测试可以发现nginx会先等待缓冲队列清空后再恢复插槽队列

    limit_req_zone $server_name zone=test123:10m rate=2r/s;
    limit_req zone=test123 burst=6 delay=4;
    jmeter线程数10,一起请求2次,2批间隔300ms
    

    速率为500ms成功一次,设置了burst桶容量为6,delay为4,相当于有4个插槽可用,附带一个长度为2的缓冲队列

    我们的预期是第一批10个请求进来立即执行1+4个请求,有2个请求进入队列缓慢执行,执行完后间隔300ms,第二批10个请求进来,有2个请求进入空出的缓冲队列,其他8个请求限流(因为300ms,没有到500ms的间隔,插槽没有来得及恢复)

    第一批10个请求进入后,前5个请求立即被执行(4个使用了插槽),请求1-10、1-1进入缓冲队列匀速执行,可以看到请求间隔了500ms,其他3个请求既没有使用插槽,也没有进缓冲队列,被限流

    第二批10个请求间隔300ms进入,这时虽然缓冲队列是空的,但是插槽来不及恢复,所以只有2个缓冲队列的位置可用,所以看到请求1-6、1-8进入了队列匀速执行

    如果把两批请求的间隔延长一些呢?

    limit_req_zone $server_name zone=test123:10m rate=2r/s;
    limit_req zone=test123 burst=6 delay=4;
    jmeter线程数10,一起请求2次,2批间隔1600ms
    

    间隔改为1600ms,足够3个插槽恢复了

    可以看到表现和预期的一样,和间隔300ms的图唯一区别,序号11-13的3个请求拿到了恢复的3个插槽,立刻被执行了

    3、附nginx内置变量

    $args                      请求中的参数;
    $binary_remote_addr        远程地址的二进制表示
    $body_bytes_sent           已发送的消息体字节数
    $content_length            HTTP请求信息里的"Content-Length"
    $content_type              请求信息里的"Content-Type"
    $document_root             针对当前请求的根路径设置值
    $document_uri              与$uri相同
    $host                      请求信息中的"Host",如果请求中没有Host行,则等于设置的服务器名;    
    $http_cookie               cookie 信息 
    $http_referer              来源地址
    $http_user_agent           客户端代理信息
    $http_via                  最后一个访问服务器的Ip地址
    $http_x_forwarded_for      相当于网络访问路径。    
    $limit_rate                对连接速率的限制          
    $remote_addr               客户端地址
    $remote_port               客户端端口号
    $remote_user               客户端用户名,认证用
    $request                   用户请求信息
    $request_body              用户请求主体
    $request_body_file         发往后端的本地文件名称      
    $request_filename          当前请求的文件路径名
    $request_method            请求的方法,比如"GET"、"POST"等
    $request_uri               请求的URI,带参数   
    $server_addr               服务器地址,如果没有用listen指明服务器地址,使用这个变量将发起一次系统调用以取得地址(造成资源浪费)
    $server_name               请求到达的服务器名
    $server_port               请求到达的服务器端口号
    $server_protocol           请求的协议版本,"HTTP/1.0"或"HTTP/1.1"
    $uri                       请求的URI,可能和最初的值有不同,比如经过重定向之类的
  • 相关阅读:
    win8 需要管理员权限才能删除此应用程序
    windows 8 解决端口(COM和LPT)问题:Prolific USBtoSerial Comm Port(COM4)驱动异常的问题
    sqlite 获取数据库中的所有表
    ListView simpleAdapter的基本使用
    一根长度为133米的材料需要截成长度为19米和23米的短料,求两种短料各截多少根时剩余的材料最少?
    打印输出九九乘法表
    写一个函数,输入一个十六进制,输出相应的十进制数。
    求3100的素数!
    从键盘输入若干个同学的成绩,统计并输出最高成绩 最低成绩 平均分,当输入负数时结束输入
    编写两个函数,分别求由键盘输入两个整数的最大公约数和最小公倍数。用主函数调用这两个函数,并输出结果
  • 原文地址:https://www.cnblogs.com/ctxsdhy/p/12354320.html
Copyright © 2011-2022 走看看