转载

聊聊高并发系统之限流特技(二)

上一篇《聊聊高并发系统限流特技-1》讲了限流算法、应用级限流、分布式限流;本篇将介绍接入层限流实现。

接入层限流

接入层通常指请求流量的入口,该层的主要目的有:负载均衡、非法请求过滤、请求聚合、缓存、降级、限流、 A/B 测试、服务质量监控等等,可以参考笔者写的《 使用 Nginx+Lua(OpenResty) 开发高性能 Web 应用 》。

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

limit_conn 用来对某个 KEY 对应的总的网络连接数进行限流,可以按照如 IP 、域名维度进行限流。 limit_req 用来对某个 KEY 对应的请求的平均速率进行限流,并有两种用法:平滑模式( delay )和允许突发模式 (nodelay)

ngx_http_limit_conn_module

limit_conn 是对某个 KEY 对应的总的网络连接数进行限流。可以按照 IP 来限制 IP 维度的总连接数,或者按照服务域名来限制某个域名的总连接数。但是记住不是每一个请求连接都会被计数器统计,只有那些被 Nginx 处理的且已经读取了整个请求头的请求连接才会被计数器统计。

配置示例:

================================

http {

limit_conn_zone$binary_remote_addr zone=addr:10m;
limit_conn_log_level error;
limit_conn_status 503;

...

server {

...

location /limit {

limit_conn addr 1;

}

================================

limit_conn :要配置存放 KEY 和计数器的共享内存区域和指定 KEY 的最大连接数;此处指定的最大连接数是 1 ,表示 Nginx 最多同时并发处理 1 个连接;

limit_conn_zone :用来配置限流 KEY 、及存放 KEY 对应信息的共享内存区域大小;此处的 KEY 是“ $binary_remote_addr ”其表示 IP 地址,也可以使用如 $server_name 作为 KEY 来限制域名级别的最大连接数;

limit_conn_status :配置被限流后返回的状态码,默认返回 503

limit_conn_log_level :配置记录被限流后的日志级别,默认 error 级别。

limit_conn 的主要执行过程如下所示:

1 、请求进入后首先判断当前 limit_conn_zone 中相应 KEY 的连接数是否超出了配置的最大连接数;

2.1 、如果超过了配置的最大大小,则被限流,返回 limit_conn_status 定义的错误状态码;

2.2 、否则相应 KEY 的连接数加 1 ,并注册请求处理完成的回调函数;

3 、进行请求处理;

4 、在结束请求阶段会调用注册的回调函数对相应 KEY 的连接数减 1

limt_conn 可以限流某个 KEY 的总并发 / 请求数, KEY 可以根据需要变化。

按照 IP 限制并发连接数配置示例:

首先定义 IP 维度的限流区域:

================================

limit_conn_zone $binary_remote_addrzone=perip:10m;

================================

接着在要限流的 location 中添加限流逻辑:

================================

location /limit {

limit_conn perip 2;

echo "123";

}

================================

即允许每个 IP 最大并发连接数为 2

使用 AB 测试工具进行测试,并发数为 5 个,总的请求数为 5 个:

================================

ab -n 5 -c 5 http://localhost/limit

================================

将得到如下 access.log 输出:

================================

[08/Jun/2016:20:10:51+0800] [1465373451.802] 200

[08/Jun/2016:20:10:51+0800] [1465373451.803] 200

[08/Jun/2016:20:10:51 +0800][1465373451.803] 503

[08/Jun/2016:20:10:51 +0800][1465373451.803] 503

[08/Jun/2016:20:10:51 +0800][1465373451.803] 503

================================

此处我们把 access log 格式设置为 log_format main  '[$time_local] [$msec] $status' ;分别是“日期 日期秒 / 毫秒值 响应状态码”。

如果被限流了,则在 error.log 中会看到类似如下的内容:

================================

2016/06/08 20:10:51 [error] 5662#0: *5limiting connections by zone "perip", client: 127.0.0.1, server: _,request: "GET /limit HTTP/1.0", host: "localhost"

================================

按照域名限制并发连接数配置示例:

首先定义域名维度的限流区域

================================

limit_conn_zone $ server_name zone=perserver:10m;

================================


接着在要限流的
location中添加限流逻辑:
================================

location /limit {

limit_conn perserver 2;

echo "123";

}

================================

即允许每个域名最大并发请求连接数为 2 ;这样配置可以实现服务器最大连接数限制。

ngx_http_limit_req_module

limit_req 是令牌桶算法实现,用于对指定 KEY 对应的请求进行限流,比如按照 IP 维度限制请求速率。

配置示例:

================================

http {

limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

limit_conn_log_level error;

limit_conn_status 503;

...

server {

...

location /limit {

limit_req zone=one burst=5 nodelay;

}

================================

limit_req :配置限流区域、桶容量(突发容量,默认 0 )、是否延迟模式(默认延迟);

limit_req_zone :配置限流 KEY 、及存放 KEY 对应信息的共享内存区域大小、固定请求速率;此处指定的 KEY 是“ $binary_remote_addr ”表示 IP 地址;固定请求速率使用 rate 参数配置,支持 10r/s 60r/m ,即每秒 10 个请求和每分钟 60 个请求,不过最终都会转换为每秒的固定请求速率( 10r/s 为每 100 毫秒处理一个请求; 60r/m ,即每 1000 毫秒处理一个请求)。

limit_conn_status :配置被限流后返回的状态码,默认返回 503

limit_conn_log_level :配置记录被限流后的日志级别,默认 error 级别。

limit_req 的主要执行过程如下所示:

1 、请求进入后首先判断最后一次请求时间相对于当前时间(第一次是 0 )是否需要限流,如果需要限流则执行步骤 2 ,否则执行步骤 3

2.1 、如果没有配置桶容量( burst ),则桶容量为 0 ;按照固定速率处理请求;如果请求被限流,则直接返回相应的错误码(默认 503 );

2.2 、如果配置了桶容量( burst>0 )且延迟模式 ( 没有配置 nodelay) ;如果桶满了,则新进入的请求被限流;如果没有满则请求会以固定平均速率被处理(按照固定速率并根据需要延迟处理请求,延迟使用休眠实现);

2.3 、如果配置了桶容量( burst>0 )且非延迟模式(配置了 nodelay );不会按照固定速率处理请求,而是允许突发处理请求;如果桶满了,则请求被限流,直接返回相应的错误码;

3 、如果没有被限流,则正常处理请求;

4 Nginx 会在相应时机进行选择一些( 3 个节点)限流 KEY 进行过期处理,进行内存回收。

场景 2.1 测试

首先定义 IP 维度的限流区域:

================================

limit_req_zone $binary_remote_addrzone=test:10m rate=500r/s;

================================

限制为每秒 500 个请求,固定平均速率为 2 毫秒一个请求。

接着在要限流的 location 中添加限流逻辑:

================================

location /limit {

limit_req zone=test;

echo "123";

}

================================

即桶容量为 0 burst 默认为 0 ),且延迟模式。

使用 AB 测试工具进行测试,并发数为 2 个,总的请求数为 10 个:

================================

ab -n 10 -c 2 http://localhost/limit

================================

将得到如下 access.log 输出:

================================

[08/Jun/2016:20:25:56+0800] [1465381556.410] 200

[08/Jun/2016:20:25:56 +0800][1465381556.410] 503

[08/Jun/2016:20:25:56 +0800][1465381556.411] 503

[08/Jun/2016:20:25:56+0800] [1465381556.411] 200

[08/Jun/2016:20:25:56 +0800][1465381556.412] 503

[08/Jun/2016:20:25:56 +0800][1465381556.412] 503

================================

虽然每秒允许 500 个请求,但是因为桶容量为 0 ,所以流入的请求要么被处理要么被限流,无法延迟处理;另外平均速率在 2 毫秒左右,比如 1465381556.410 1465381556.411 被处理了;有朋友会说这固定平均速率不是 1 毫秒嘛,其实这是因为实现算法没那么精准造成的。

如果被限流在 error.log 中会看到如下内容:

================================

2016/06/08 20:25:56 [error] 6130#0: *1962limiting requests, excess: 1.000 by zone "test", client: 127.0.0.1,server: _, request: "GET /limit HTTP/1.0", host:"localhost"

================================

如果被延迟了在 error.log (日志级别要 INFO 级别)中会看到如下内容:

================================

2016/06/10 09:05:23 [warn] 9766#0: *97021delaying request, excess: 0.368, by zone "test", client: 127.0.0.1,server: _, request: "GET /limit HTTP/1.0", host:"localhost"

================================

场景 2.2 测试

首先定义 IP 维度的限流区域:

================================

limit_req_zone $binary_remote_addr    zone=test:10m rate=2r/s;

================================

为了方便测试设置速率为每秒 2 个请求,即固定平均速率是 500 毫秒一个请求。

接着在要限流的 location 中添加限流逻辑:

================================

location /limit {

limit_req zone=test burst=3;

echo "123";

}

================================

固定平均速率为 500 毫秒一个请求,通容量为 3 ,如果桶满了新的请求被限流,否则可以进入桶中排队并等待(实现延迟模式)。

为了看出限流效果我们写了一个 req.sh 脚本:

================================

ab -c 6 -n 6 http://localhost/limit

sleep 0.3

ab -c 6 -n 6 http://localhost/limit

================================

首先进行 6 个并发请求 6 URL ,然后休眠 300 毫秒,然后再进行 6 个并发请求 6 URL ;中间休眠目的是为了能跨越 2 秒看到效果,如果看不到如下的效果可以调节休眠时间。

将得到如下 access.log 输出:

================================

[09/Jun/2016:08:46:43+0800] [1465433203.959] 200

[09/Jun/2016:08:46:43 +0800][1465433203.959] 503

[09/Jun/2016:08:46:43 +0800][1465433203.960] 503

[09/Jun/2016:08:46:44+0800] [1465433204.450] 200

[09/Jun/2016:08:46:44+0800] [1465433204.950] 200

[09/Jun/2016:08:46:45 +0800][1465433205.453] 200

[09/Jun/2016:08:46:45 +0800][1465433205.766] 503

[09/Jun/2016:08:46:45 +0800][1465433205.766] 503

[09/Jun/2016:08:46:45 +0800][1465433205.767] 503

[09/Jun/2016:08:46:45+0800] [1465433205.950] 200

[09/Jun/2016:08:46:46+0800] [1465433206.451] 200

[09/Jun/2016:08:46:46+0800] [1465433206.952] 200

================================

聊聊高并发系统之限流特技(二)

桶容量为 3 ,即桶中在时间窗口内最多流入 3 个请求,且按照 2r/s 的固定速率处理请求(即每隔 500 毫秒处理一个请求);桶计算时间窗口( 1.5 秒) = 速率( 2r/s / 桶容量 (3) ,也就是说在这个时间窗口内桶最多暂存 3 个请求。因此我们要以当前时间往前推 1.5 秒和 1 秒来计算时间窗口内的总请求数;另外因为默认是延迟模式,所以时间窗内的请求要被暂存到桶中,并以固定平均速率处理请求:

第一轮:有 4 个请求处理成功了,按照漏桶桶容量应该最多 3 个才对;这是因为计算算法的问题,第一次计算因没有参考值,所以第一次计算后,后续的计算才能有参考值,因此第一次成功可以忽略;这个问题影响很小可以忽略;而且按照固定 500 毫秒的速率处理请求。

第二轮:因为第一轮请求是突发来的,差不多都在 1465433203.959 时间点,只是因为漏桶将速率进行了平滑变成了固定平均速率(每 500 毫秒一个请求);而第二轮计算时间应基于 1465433203.959 ;而第二轮突发请求差不多都在 1465433205.766 时间点,因此计算桶容量的时间窗口应基于 1465433203.959 1465433205.766 来计算,计算结果为 1465433205.766 这个时间点漏桶为空了,可以流入桶中 3 个请求,其他请求被拒绝;又因为第一轮最后一次处理时间是 1465433205.453 ,所以第二轮第一个请求被延迟到了 1465433205.950 。这里也要注意固定平均速率只是在配置的速率左右,存在计算精度问题,会有一些偏差。

如果桶容量改为 1 burst=1 ),执行 req.sh 脚本可以看到如下输出:

================================

[09/Jun/2016:09:04:30+0800] [1465434270.362] 200

[09/Jun/2016:09:04:30 +0800][1465434270.371] 503

[09/Jun/2016:09:04:30 +0800] [1465434270.372]503

[09/Jun/2016:09:04:30 +0800][1465434270.372] 503

[09/Jun/2016:09:04:30 +0800][1465434270.372] 503

[09/Jun/2016:09:04:30+0800] [1465434270.864] 200

[09/Jun/2016:09:04:31 +0800][1465434271.178] 503

[09/Jun/2016:09:04:31 +0800][1465434271.178] 503

[09/Jun/2016:09:04:31 +0800][1465434271.178] 503

[09/Jun/2016:09:04:31 +0800][1465434271.178] 503

[09/Jun/2016:09:04:31 +0800][1465434271.179] 503

[09/Jun/2016:09:04:31+0800] [1465434271.366] 200

================================

桶容量为 1 ,按照每 1000 毫秒一个请求的固定平均速率处理请求。

场景 2.3 测试

首先定义 IP 维度的限流区域:

================================

limit_req_zone $binary_remote_addrzone=test:10m rate=2r/s;

================================

为了方便测试配置为每秒 2 个请求,固定平均速率是 500 毫秒一个请求。

接着在要限流的 location 中添加限流逻辑:

================================

location /limit {

limit_req zone=test burst=3 nodelay;

echo "123";

}

================================

桶容量为 3 ,如果桶满了直接拒绝新请求,且每秒 2 最多两个请求,桶按照固定 500 毫秒的速率以 nodelay 模式处理请求。

为了看到限流效果我们写了一个 req.sh 脚本:

================================

ab -c 6 -n 6 http://localhost/limit

sleep 1

ab -c 6 -n 6 http://localhost/limit

sleep 0.3

ab -c 6 -n 6 http://localhost/limit

sleep 0.3

ab -c 6 -n 6 http://localhost/limit

sleep 0.3

ab -c 6 -n 6 http://localhost/limit

sleep 2

ab -c 6 -n 6 http://localhost/limit

================================

将得到类似如下 access.log 输出:

================================

[09/Jun/2016:14:30:11+0800] [1465453811.754] 200

[09/Jun/2016:14:30:11+0800] [1465453811.755] 200

[09/Jun/2016:14:30:11+0800] [1465453811.755] 200

[09/Jun/2016:14:30:11+0800] [1465453811.759] 200

[09/Jun/2016:14:30:11 +0800][1465453811.759] 503

[09/Jun/2016:14:30:11 +0800][1465453811.759] 503

[09/Jun/2016:14:30:12+0800] [1465453812.776] 200

[09/Jun/2016:14:30:12+0800] [1465453812.776] 200

[09/Jun/2016:14:30:12 +0800][1465453812.776] 503

[09/Jun/2016:14:30:12 +0800][1465453812.777] 503

[09/Jun/2016:14:30:12 +0800][1465453812.777] 503

[09/Jun/2016:14:30:12 +0800][1465453812.777] 503

[09/Jun/2016:14:30:13 +0800] [1465453813.095]503

[09/Jun/2016:14:30:13 +0800][1465453813.097] 503

[09/Jun/2016:14:30:13 +0800][1465453813.097] 503

[09/Jun/2016:14:30:13 +0800][1465453813.097] 503

[09/Jun/2016:14:30:13 +0800][1465453813.097] 503

[09/Jun/2016:14:30:13 +0800][1465453813.098] 503

[09/Jun/2016:14:30:13+0800] [1465453813.425] 200

[09/Jun/2016:14:30:13 +0800][1465453813.425] 503

[09/Jun/2016:14:30:13 +0800][1465453813.425] 503

[09/Jun/2016:14:30:13 +0800][1465453813.426] 503

[09/Jun/2016:14:30:13 +0800][1465453813.426] 503

[09/Jun/2016:14:30:13 +0800][1465453813.426] 503

[09/Jun/2016:14:30:13+0800] [1465453813.754] 200

[09/Jun/2016:14:30:13 +0800][1465453813.755] 503

[09/Jun/2016:14:30:13 +0800][1465453813.755] 503

[09/Jun/2016:14:30:13 +0800][1465453813.756] 503

[09/Jun/2016:14:30:13 +0800][1465453813.756] 503

[09/Jun/2016:14:30:13 +0800][1465453813.756] 503

[09/Jun/2016:14:30:15+0800] [1465453815.278] 200

[09/Jun/2016:14:30:15+0800] [1465453815.278] 200

[09/Jun/2016:14:30:15+0800] [1465453815.278] 200

[09/Jun/2016:14:30:15 +0800][1465453815.278] 503

[09/Jun/2016:14:30:15 +0800][1465453815.279] 503

[09/Jun/2016:14:30:15 +0800][1465453815.279] 503

[09/Jun/2016:14:30:17+0800] [1465453817.300] 200

[09/Jun/2016:14:30:17+0800] [1465453817.300] 200

[09/Jun/2016:14:30:17+0800] [1465453817.300] 200

[09/Jun/2016:14:30:17+0800] [1465453817.301] 200

[09/Jun/2016:14:30:17 +0800][1465453817.301] 503

[09/Jun/2016:14:30:17 +0800][1465453817.301] 503

================================

聊聊高并发系统之限流特技(二)

桶容量为 3 (,即桶中在时间窗口内最多流入 3 个请求,且按照 2r/s 的固定速率处理请求(即每隔 500 毫秒处理一个请求);桶计算时间窗口( 1.5 秒) = 速率( 2r/s / 桶容量 (3) ,也就是说在这个时间窗口内桶最多暂存 3 个请求。因此我们要以当前时间往前推 1.5 秒和 1 秒来计算时间窗口内的总请求数;另外因为配置了 nodelay ,是非延迟模式,所以允许时间窗内突发请求的;另外从本示例会看出两个问题:

第一轮和第七轮:有 4 个请求处理成功了;这是因为计算算法的问题,本示例是如果 2 秒内没有请求,然后接着突然来了很多请求,第一次计算的结果将是不正确的;这个问题影响很小可以忽略;

第五轮: 1.0 秒计算出来是 3 个请求;此处也是因计算精度的问题,也就是说 limit_req 实现的算法不是非常精准的,假设此处看成相对于 2.75 的话, 1.0 秒内只有 1 次请求,所以还是允许 1 次请求的。

如果限流出错了,可以配置错误页面:

================================

proxy_intercept_errors

on;

recursive_error_pages

on;

error_page 503 //www.jd.com/error.aspx;

================================

limit_conn_zone/limit_req_zone 定义的内存不足,则后续的请求将一直被限流,所以需要根据需求设置好相应的内存大小。

此处的限流都是单 Nginx 的,假设我们接入层有多个 nginx ,此处就存在和应用级限流相同的问题;那如何处理呢?一种解决办法:建立一个负载均衡层将按照限流 KEY 进行一致性哈希算法将请求哈希到接入层 Nginx 上,从而相同 KEY 的将打到同一台接入层 Nginx 上;另一种解决方案就是使用 Nginx+Lua OpenResty )调用分布式限流逻辑实现。

lua-resty-limit-traffic

之前介绍的两个模块使用上比较简单,指定 KEY 、指定限流速率等就可以了,如果我们想根据实际情况变化 KEY 、变化速率、变化桶大小等这种动态特性,使用标准模块就很难去实现了,因此我们需要一种可编程来解决我们问题;而 OpenResty 提供了 lua 限流模块 lua-resty-limit-traffic ,通过它可以按照更复杂的业务逻辑进行动态限流处理了。其提供了 limit.conn limit.req 实现,算法与 nginx limit_conn limit_req 是一样的。

此处我们来实现 ngx_http_limit_req_module 中的【场景 2.2 测试】,不要忘记下载 lua-resty-limit-traffic 模块并添加到 OpenResty lualib 中。

配置用来存放限流用的共享字典:

================================

lua_shared_dict limit_req_store 100m;

================================

以下是实现【场景 2.2 测试】的限流代码 limit_req.lua

================================

local limit_req = require "resty.limit.req"
local rate = 2 --固定平均速率 2r/s
local burst = 3  --桶容量
local error_status = 503
local nodelay = false --是否需要不延迟处理
local lim, err = limit_req.new("limit_req_store", rate, burst)
if not lim then --没定义共享字典
ngx.exit(error_status)
end
local key = ngx.var.binary_remote_addr --IP维度的限流
--流入请求,如果请求需要被延迟则delay > 0
local delay, err = lim:incoming(key, true)
if not delay and err == "rejected" then --超出桶大小了
ngx.exit(error_status)
end
if delay > 0 then  --根据需要决定是延迟或者不延迟处理
if nodelay then
--直接突发处理了
else
ngx.sleep(delay) --延迟处理
end
end

================================

即限流逻辑再 nginx access 阶段被访问,如果不被限流继续后续流程;如果需要被限流要么 sleep 一段时间继续后续流程,要么返回相应的状态码拒绝请求。

在分布式限流中我们使用了简单的 Nginx+Lua 进行分布式限流,有了这个模块也可以使用这个模块来实现分布式限流。

另外在使用 Nginx+Lua 时也可以获取 ngx.var.connections_active 进行过载保护,即如果当前活跃连接数超过阈值进行限流保护。

================================

if tonumber(ngx.var.connections_active) >= tonumber(limit) then

//限流

end

================================

nginx也提供了limit_rate用来对流量限速,如limit_rate 50k,表示限制下载速度为50k。

到此笔者在工作中涉及的限流用法就介绍完,这些算法中有些允许突发,有些会整形为平滑,有些计算算法简单粗暴;其中令牌桶算法和漏桶算法实现上是类似的,只是表述的方向不太一样,对于业务来说不必刻意去区分它们;因此需要根据实际场景来决定如何限流,最好的算法不一定是最适用的。

参考资料

https://en.wikipedia.org/wiki/Token_bucket

https://en.wikipedia.org/wiki/Leaky_bucket

http://redis.io/commands/incr

http://nginx.org/en/docs/http/ngx_http_limit_req_module.html

http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html

https://github.com/openresty/lua-resty-limit-traffic

http://nginx.org/en/docs/http/ngx_http_core_module.html#limit_rate

原文  http://mp.weixin.qq.com/s?__biz=MzIwODA4NjMwNA==&mid=2652897782&idx=1&sn=cb46b23b2778f14ea3bcc419eb0392ce&scene=1&srcid=0614xnlcciitJsZxjl1ToKVp
正文到此结束
Loading...