zoukankan      html  css  js  c++  java
  • 浅谈限流(上)

    限流的必要性

    随着应用的访问量越来越高,瞬时流量不可预估,为了保证服务对外的稳定性,限流成为每个应用必备的一道安全防火墙,即使普通的用户也会经常遇到,如微博的限流,抖音的限流,小米抢购的限流......如果没有这道安全防火墙,请求的流量超过服务的负载能力,很容易造成整个服务的瘫痪。
    限流需要提前评估好,如果用的不当,可能会导致有些该限制的流量没有被限流,服务被这些过载流量打垮。有些不该限制流量的被限制,被用户抱怨。例如,整体服务的QPS是400/s,如果限流阀值是300,就会导致每秒有100个请求本该接受服务,却被限制访问,如果阀值是500,就会导致每秒有100个请求负载,时间越长累积越多,这些过载的流量就有可能导致整个服务的瘫痪。

    限流的算法

    常见的限流算法有令牌同、漏桶,还有一种计数器。

    令牌桶

    令牌算法的过程如下

    1. 假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中
    2. 假设桶最多可以存发b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃;
    3. 当一个n个字节的数据包到达时,就从令牌桶中删除n个令牌,并且数据包被发送到网络;
    4. 如果令牌桶中少于n个令牌,那么不会删除令牌,并且认为这个数据包在流量限制之外,要不丢弃要不缓冲区等待
      在这里插入图片描述

    漏桶

    一直觉得应该叫漏斗啊。
    5. 一个固定容量的漏桶,按照固定速率流出漏桶
    6. 可以以任意速度流入水桶
    7. 如果流入的水超过桶的容量,则水就溢出,被丢弃
    在这里插入图片描述

    令牌桶和漏桶的比较

    1. 令牌桶是按照固定速率往桶中添加令牌,请求是否处理主要看同种是否有令牌,流入不限制,可以一次拿多个令牌,只要桶中有令牌,则处理请求;如果没有令牌,则拒绝请求。
    2. 漏桶则是流入请求不限制,按照固定速率流出请求,如果流入的请求的速度小于等于流出的请求,桶为空桶,则处理请求;如果流入的请求的速度大于流出的请求,累积请求留在同种,但是桶未满,则处理请求;如果累积请求大于桶容量时,则拒绝请求。
    3. 两个算法实现一样,方向相反,令牌是匀速流入,流通是匀速流出。

    计数器

    计数器比较简单,没有什么算法和描述。满足一定的条件的流量计数加1,达到阀值了限制,顾名思义叫计数限流。

    限流使用

    使用最常见的就是Nginx自带两个限流模块:连接数限流模块ngx_http_limit_conn_module 和请求数限流模块ngx_http_limit_req_module;还有openresty的限流模块lua-resty-limit-traffic;还可能需要应对复杂的业务需求而自研的计数限流。我们一一介绍下这些限流方法的使用

    ngx_http_limit_conn_module

    从名字就可以看出是Nginx的连接数限流。大多都是按照IP来源进行连接数限流,也可以按照域名对总的连接数进行限流。
    我们看下连接数限流的配置

    http {
        limit_conn_zone $binary_remote_addr zone=addr:10m;
        limit_conn_log_level error;
        limit_conn_status 503;
        ...
        server {
            ...
            location /download/ {
                limit_conn addr 1;
            }
    

    limit_conn_zone: 配置限流的key以及存储这些key共用的共享内存的大小;
    样例中的key 是$binary_remote_addr,表示IP地址,如果如果需要对总域名进行限流,key就应该使用 $server_name $host等等,能唯一表示该域名的Nginx变量;
    zone=addr:10m中,addr表示连接数限流的区域名称,10m表示可以分配的共享空间的大小。
    binary_remote_addr变量在64位平台中占用64字节。1M共享空间可以保存1.6万个64位的,10m就可以保存16万个。如果超过16万个,共享空间被用完,服务器将会对后续所有的请求返回 503。
    limit_conn:配置指定key的最大连接数。样例中指定的最大连接数是1,表示Nginx最多同时允许1个连接进行location /limit 的行为操作。
    limit_conn_status:配置被限流后返回的状态码,样例中设置的是503.
    limit_conn_log_level:配置被限流后的日志级别,设置的是error级别
    看下测试代码

    limit_conn_zone $server_name zone=addr:10m;
    limit_conn_log_level error;
    limit_conn_status 503;
    server{
        listen      80;
        server_name test.test.com;
        access_log /var/log/openresty/web_test.test.com_access.log test;
        error_log /var/log/openresty/web_test.test.com_error.log;
        location /test/ {
            limit_conn addr 2;
            content_by_lua '
                    ngx.sleep(1)
                    ngx.say("helloworld")
            ';
        }
    }
    ab 命令
    ab -n10 -c3 http://test.test.com/test/
    access_log
    127.0.0.1|1553438999.158|200
    127.0.0.1|1553438999.160|503
    127.0.0.1|1553438999.160|503
    127.0.0.1|1553438999.161|503
    127.0.0.1|1553438999.162|503
    127.0.0.1|1553438999.163|503
    127.0.0.1|1553438999.163|503
    127.0.0.1|1553438999.164|503
    127.0.0.1|1553439000.160|200
    127.0.0.1|1553439000.160|200
    error_log
    2019/03/24 22:49:59 [error] 700#0: *63 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/24 22:49:59 [error] 700#0: *64 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/24 22:49:59 [error] 700#0: *65 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/24 22:49:59 [error] 700#0: *66 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/24 22:49:59 [error] 700#0: *67 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/24 22:49:59 [error] 700#0: *68 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/24 22:49:59 [error] 700#0: *69 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    

    可以看到是符合我们配置语气的。如果我们将
    limit_conn_log_level info;
    limit_conn_status 500;
    可以看到,error_log里面记录的日志就是info的,当然error_log的级别要调到info级别。返回的HTTP状态码也会变为500.可以动手试下。

    limit_conn 的执行过程
    请求进入首先判断定义的key的连接数是否超过limit_conn配置的阀值,如果超过直接返回limit_conn_status定义的错误码;如果没有超过连接数+1
    请求处理
    请求处理完成之后连接数-1

    这就是为什么要做下sleep操作,否则在测试环境下没有任何压力,两个连接数完全可以在一秒之内处理完10个请求。为了测试出效果,就需要在一秒之内让Nginx无法完成10个请求。

    ngx_http_limit_req_module

    Nginx的请求数限流,请求数限流是漏桶算法实现的。通过定义的key来限制请求处理的频率,可以限制来自单个IP地址的请求处理频率。

    http {
        limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
        limit_req_log_level error;
        limit_req_status 503;
        ...
        server {
            ...
            location /search/ {
                limit_req zone=one burst=5;
            }
    

    limit_req_zone:配置限流的key,存放key对应的共享区域空间大小,固定的请求速率。样例中的key binary_remote_addr 表示IP地址。one 表示共享区域空间的名称,10m表示共享区域空间的大小,跟limit_conn的定义一致,10m就可以保存16万个IP地址。rate=1r/s 固定请求速率设置,每秒1个请求。
    limit_req:配置限流区域,桶容量,是否延迟模式。样例中桶容量是5,延迟模式默认是延迟。
    limit_req_status:配置被限流后返回的状态。样例中是503
    limit_req_log_level:配置被限流后的日志级别,样例中是error
    测试下上面的代码

    看下测试代码

     limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
     limit_req_log_level error;
     limit_req_status 503;
    server{
        listen      80;
        server_name test.test.com;
        access_log /var/log/openresty/web_test.test.com_access.log test;
        error_log /var/log/openresty/web_test.test.com_error.log info;
        location /test/ {
            limit_req zone=one burst=5;
            content_by_lua '
                    ngx.say("helloworld")
            ';
        }
    }
    
    ab 命令
    ab -n10 -c10 http://test.test.com/test/
    
    access_log
    127.0.0.1|1553525058.469|200
    127.0.0.1|1553525058.470|503
    127.0.0.1|1553525058.470|503
    127.0.0.1|1553525058.470|503
    127.0.0.1|1553525058.470|503
    127.0.0.1|1553525059.471|200
    127.0.0.1|1553525060.470|200
    127.0.0.1|1553525061.470|200
    127.0.0.1|1553525062.471|200
    127.0.0.1|1553525063.471|200
    
    error_log
    2019/03/25 22:44:18 [warn] 833#0: *144 delaying request, excess: 0.999, by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/25 22:44:18 [warn] 833#0: *145 delaying request, excess: 1.999, by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/25 22:44:18 [warn] 833#0: *146 delaying request, excess: 2.999, by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/25 22:44:18 [warn] 833#0: *147 delaying request, excess: 3.999, by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/25 22:44:18 [warn] 833#0: *148 delaying request, excess: 4.999, by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/25 22:44:18 [error] 833#0: *149 limiting requests, excess: 5.999 by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/25 22:44:18 [error] 833#0: *150 limiting requests, excess: 5.999 by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/25 22:44:18 [error] 833#0: *151 limiting requests, excess: 5.999 by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    2019/03/25 22:44:18 [error] 833#0: *152 limiting requests, excess: 5.999 by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
    

    测试代码中桶容量是5,按照1r/s的速度处理。可以看到在,由于默认是延迟模式,所以1553525059.471到1553525063.471这个时间段最多存储5个请求,然后按照1r/s的速度处理,由于延迟模式,error_log可以看到这五条记录都是延迟执行的(delaying request)。大于五条的记录都限流503 了。
    那为什么第一条记录执行成功了?这应该是计算算法的问题,第一条记录没有参考值,所以第一秒没有计算在内,这之后的都是按照第一条记录参考的时间,所以后面的基本上都是精确的。

    我们将延迟模式改为不延迟模式看下。

    location /test/ {
            limit_req zone=one burst=5 nodelay;
    	content_by_lua '
    		ngx.say("helloworld")
    	';
        }
    ab 测试
    ab -n7 -c7 http://test.test.com/test/
    ab -n7 -c7 http://test.test.com/test/
    ab -n7 -c7 http://test.test.com/test/
    
    access_log
    
    127.0.0.1|1554385661.861|200
    127.0.0.1|1554385661.862|200
    127.0.0.1|1554385661.862|200
    127.0.0.1|1554385661.862|200
    127.0.0.1|1554385661.862|200
    127.0.0.1|1554385661.862|200
    127.0.0.1|1554385661.862|503
    
    127.0.0.1|1554385665.513|200
    127.0.0.1|1554385665.514|200
    127.0.0.1|1554385665.514|200
    127.0.0.1|1554385665.514|503
    127.0.0.1|1554385665.514|503
    127.0.0.1|1554385665.514|503
    127.0.0.1|1554385665.514|503
    
    127.0.0.1|1554385667.361|200
    127.0.0.1|1554385667.361|200
    127.0.0.1|1554385667.362|503
    127.0.0.1|1554385667.362|503
    127.0.0.1|1554385667.362|503
    127.0.0.1|1554385667.362|503
    127.0.0.1|1554385667.362|503
    

    我们为了跨时间窗口测试,我们测试三组。先看下第一组,7个请求6个成功,一个503,其实理论上桶容量是5,至多只可能成功5个,有个503才对。我们说了第一组计算算法问题基本上忽略的。
    我们看下第二组,跟第一组相差4秒,处理速度是1r/s.4秒之后按按理应该桶里有4个位置,应该成功处理4个,3个503,怎么现在是4个503,成功处理三个,此处还是要强调下limit_req的实现算法不是特别精确
    我们看下第三组,比第二组晚了2秒,所以桶里会有2个位置,应该有2个请求成功,5个请求503.这个跟预想的基本吻合。
    所以整体上和理解是一致的。就是算法上不是特别的精确。我们生产上限流也是至少几千几万的限流,算法上的精确差异其实是可以忽略不计的。
    这一部分主要是聊了下限流的原理和常见的Nginx的两个限流模块。下一部分我们聊下生产中比较常见的lua限流。

    ------------------------------------end
    一起关注高性能WEB后端技术,关注公众号

  • 相关阅读:
    反汇编角度->C++ const
    反汇编->C++虚函数深度分析
    反汇编->C++内联
    反汇编->C++引用与指针
    数据库初步认识
    数据库系统的结构抽象与演变
    Android · PendingIntent学习
    Android · ContentProvider学习
    notepad++
    MapReduce使用JobControl管理实例
  • 原文地址:https://www.cnblogs.com/feixiangmanon/p/10658012.html
Copyright © 2011-2022 走看看