浅谈限流(上)

限流的必要性

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

限流的算法

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

令牌桶

令牌算法的过程以下算法

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

    漏桶

    一直以为应该叫漏斗啊。
  5. 一个固定容量的漏桶,按照固定速率流出漏桶
  6. 能够以任意速度流入水桶
  7. 若是流入的水超过桶的容量,则水就溢出,被丢弃
    在这里插入图片描述
    #### 令牌桶和漏桶的比较
  8. 令牌桶是按照固定速率往桶中添加令牌,请求是否处理主要看同种是否有令牌,流入不限制,能够一次拿多个令牌,只要桶中有令牌,则处理请求;若是没有令牌,则拒绝请求。
  9. 漏桶则是流入请求不限制,按照固定速率流出请求,若是流入的请求的速度小于等于流出的请求,桶为空桶,则处理请求;若是流入的请求的速度大于流出的请求,累积请求留在同种,可是桶未满,则处理请求;若是累积请求大于桶容量时,则拒绝请求。
  10. 两个算法实现同样,方向相反,令牌是匀速流入,流通是匀速流出。安全

#### 计数器
计数器比较简单,没有什么算法和描述。知足必定的条件的流量计数加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.能够动手试下。lua

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后端技术,关注公众号

相关文章
相关标签/搜索