浅谈限流(上)

限流的必要性

随着应用的访问量越来越高,瞬时流量不可预估,为了保证服务对外的稳定性,限流成为每个应用必备的一道安全防火墙,即使普通的用户也会经常遇到,如微博的限流,抖音的限流,小米抢购的限流......如果没有这道安全防火墙,请求的流量超过服务的负载能力,很容易造成整个服务的瘫痪。
限流需要提前评估好,如果用的不当,可能会导致有些该限制的流量没有被限流,服务被这些过载流量打垮。有些不该限制流量的被限制,被用户抱怨。例如,整体服务的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后端技术,关注公众号

posted @ 2019-04-05 09:58  飞翔码农  阅读(2632)  评论(0编辑  收藏  举报