在使用了负载均衡设施的网站中,对 HTTP 请求做访问控制(频率控制)是个经常遇到的需求。 频率控制的主要目的,传统应用场景中要 保护后端系统 。 在多租户的云服务上,通过频率控制 对不同租户的资源使用量进行有效分配 ,从而避免相互影响访问质量。 Nginx 的频率控制Nginx 是现在最常见的负载均衡软件,比较早的版本即有频率控制功能。 Nginx 中通常采用模块的形式提供各种功能,频率控制方面常用的有两个模块:
针对指定的 key,限制 请求处理频率 。
针对指定的 key,限制 同时连接数 。 这两个模块是按秒或分的时间精度来限制,使用 10r/s 或 1r/m 这种方式能够比较容易配置。 Tengine 改进了频率控制相关功能,但是仍然简单粗暴,支持的策略比较少,难以满足个性化的时间窗口需要。。 另外,以上2个模块针对进程级别做控制,多个 Nginx 部署之间不共享数据。 实际部署中, Nginx 作为负载均衡设施,一般会部署多台组成集群。 这时的目的是对这个集群进行保护,不是对单个server的访问限制,所以基于单机的限制有了明显的局限性。 频率控制系统的设计目标这样一个频率控制系统,我们有以下设计目标:
根据实际业务需求协商确定出公认的目标,是实现的关键步骤。 架构设计在设计目标指引下,选择使用稳定高效的开源软件来实现。 最终形成了 Nginx + Lua + Redis 的方案。 基于负载均衡层的流控系统,可以串联部署在负载均衡后端,作为应用层的代理,也可以并联部署在负载均衡后端旁路。 我们采用了旁路的方式来部署。 架构图:
组件失败保护:
关键配置1. 规范化频率控制的键针对每个请求URL,使用 nginx 的 map 功能或 location regex 进行提取,并规范化为限制规则使用的键名。 location ~* ^/(?P<org_name>[0-9a-zA-Z-_]+)/(?P<app_name>[0-9a-zA-Z-]+)/users$ { set $ratelimit_metric "$org_name#$app_name#users" proxy_pass http://backend_rest_servers; } 我们将 URL 中提取的信息使用 set 语法拼接,将它保存在 $ratelimit_metric 变量作为频率控制的键, 2. 加载 lualua 基础功能需要在 nginx 编译阶段指定选项。如果当前版本不支持 lua 功能,需要重新编译,并在编译时至指定 --with-lua 选项。 ./configure --with-lua 实现逻辑时需要访问 redis ,因此还需要加载 lua 的 redis 库: lua-resty-redis 。 在配置文件的 http 上下文部分加一行配置: lua_package_path "lua/lua-resty-redis/lib/?.lua;;"; 然后加载我们实现逻辑的 lua 脚本,之后的所有逻辑操作都在这个文件中完成。 access_by_lua_file 'ratelimit-with-redis.lua'; 3. 初始化 redis 连接首先加载 lua 的 redis 库,设置合理的超时时间。 当连接失败时,则直接从限流逻辑中跳出。 local cjson = require "cjson" local redis = require "resty.redis" local red = redis:new() red:set_timeout(1000) local ok, err = red:connect("127.0.0.1", 6379) if not ok then -- ngx.say("failed to connect: ", err) return end 4. counter incr 操作对于每一次客户请求,都需要去更新指定的 key。 local counter_key = ngx.var.ratelimit_metric -- ngx.say("counter key: ", counter_key) count, err = red:incr(counter_key) if not count then -- ngx.say("failed to incr: ", err) return end 5. 根据键值控制访问假定默认限制超过 100 次后,对后续访问进行限制,返回状态码 429。 if count > 100 then ngx.status = 429 --ngx.say("fooc: ", ok) ngx.say("rate limit: ", count, " > ", 100) ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE) end 6. 与 Redis 保持长连接local ok, err = red:set_keepalive(100000, 20) if not ok then -- ngx.say("failed to set keepalive: ", err) return end 7. 时间维度限制前面示例所做的限制没有考虑时间维度,即超出限制被禁后,不会随时间清零。 现在我们改造一下 key 的格式,使得 counter 只在一个时间周期内有效。 修改 counter_key 的定义: local counter_key = time()/60 .. ":" .. ngx.var.ratelimit_metric 8. 读取 limit 上限设置limit, err = red:get("limit:" .. ngx.var.ratelimit_metric) if not limit then -- ngx.say("use default limit: ", 100) limit = 100 end 限制默认为 100 9. 利用 Redis Pipelining同一次请求会产生多个 redis 操作,没有前后依赖关系,使用 redis 的 batch 方式减少交互 red:init_pipeline local results, err = red:commit_pipeline() if not results then ngx.say(“failed to commit the pipelined requests: “, err) return end print(cjson.encode(results)) for i, res in ipairs(results) do if type(res) == “table” then if not res[1] then ngx.say(“failed to run command “, i, “: “, res[2]) else – process the table value end else – process the scalar value end end 效果、功能与性能评估关键点
可以使用 ab (apache benchmarking) 进行测试。 思路总结最终方案中,各部分功能都采用了成熟的开源方案,有一些综合的优势:
但是在使用场景方面有局限性,针对 HTTP 数据都分析则不适合。 如果遇到与业务深度分析的场景时,Nginx 端实现则会有比较大的代价,既包括人工付出,也可能有一些性能方面的退化。 比如针对 API 请求中的 JSON 数据做有效性校验,使用 lua 去处理业务逻辑,以及容错方面的代码量将显著增加。 从开发成本来看这不是一个好的选择,对 Nginx 维护人员的能力也是挑战。 展望可以进一步优化为使用 redis scripting(也是 lua 脚本),将所有逻辑放到 redis 中实现。 既能够达到现有方案使用 [Redis Pipelining] 的降低延时效果,还能进一步减少 nginx <-> redis 之间的读写操作次数,从而进一步降低延时。 (责任编辑:IT) |