OpenResty限频限流
之前对系统限流这个话题有过一次讨论,之后有在实际工作中遇到一次应用,写一篇文章记录本次实践。
背景
目前在做的系统有提供Open API接入,即客户通过API Key接入使用系统功能。所允许接入的API根据功能种类不同所需系统开销有所差异,从技术角度希望对系统增加一些保护措施,避免用户接入使用过程有意或无意高频调用一些API对系统构成压力,从而影响整个系统稳定性。
限制主要考虑两个维度:
- 限频,限制单位时间内调用次数,关注调用速度
- 限流,限制时间窗口内调用次数,关注调用总量
如上描述,限频、限流本质是同一件事:“限制一定时间内的调用次数”,但此处特意使用两个不同的名词用于体现差异性(后文会多次提及):
- 单位时间,粒度小,常量,通常用秒描述
- 时间窗口,粒度大,变量,如一分钟、五分钟、十五分钟或一小时等
在目标系统中针对特定API(POST /api/order
),期望实现:“每秒调用不能超过20次,每分钟调用不能超过200次”,前者称之为限频,后者称为限流。
方案
限频限流可以选择在应用层实现,早期在一些项目中的确有借助Redis实践过,结论是只能满足前期需要,后期系统QPS过高时应用层与Redis的开销不容小觑。
当前项目的Open API网关是OpenResty,选择把限频限流做在网关这一层是个不错的选择,重点依赖OpenResty官方提供的几个Lua扩展:
-
resty.limit.req, 用于限制单位时间(秒)的请求数
-
resty.limit.conn, 用于限制并发连接数
-
resty.limit.count, 用于限制时间窗口内的请求数量限制,时间窗口可自定义
-
resty.limit.traffic, 用于对三者进行组合,以实现更丰富的限制策略
借助上述OpenResty Lua扩展,除实现限频限流功能外,还期望能对用户提供一些“反馈”信息:
- 当前API的限频限流的限制信息,如每秒允许请求数,限流的时间窗口大小以及请求数
- 触发限频时,请求被系统延迟了多久处理(当某请求触发限频时有两个选择:Pending到下一秒处理、拒绝)
- 针对限流,当前时间窗口剩余可请求次数
最初希望直接使用resty.limit.traffic
达到目的,但在阅读与熟悉官方文档后,发现上述反馈信息需求无法被满足,最终选择参考resty.limit.traffic
代码进行一些调整以达到目的。
实现
针对API调用,限频限流已实现特性列表:
- 限频,API粒度每秒调用次数限制
- 限流,API粒度N秒调用次数限制,并反馈状态信息
- 限并发,API粒度并发连接数限制
- 触发限制时返回原因信息
- IP白名单,调用不受限制
- API Key白名单,调用不受限制
代码不多也不少,请移步Gist查看:
- limit.lua,对
resty.limit.traffic
改造后的实现,以满足限流状态实时反馈 - limit.json,限流限频配置文件,{“method:path”: [每秒允许请求数, 限频burst, 时间窗口允许请求数, 时间窗口大小]}
- init_worker.lua,OpenResty初始化时执行
- access.lua,OpenResty请求处理时执行
- log.lua,OpenResty请求完成时执行
OpenResty相关配置:
http {
lua_package_path "/usr/local/openresty/lualib/?.lua;/usr/local/openresty/nginx/conf/lua/?.lua;;";
# for limit
lua_shared_dict limit 2m;
lua_shared_dict limit_req_store 16m;
lua_shared_dict limit_count_store 16m;
lua_shared_dict limit_conn_store 16m;
init_worker_by_lua_file init_worker.lua;
server {
# ...
access_by_lua_file access.lua;
log_by_lua_file log.lua;
}
}
待实现特性列表:
-
更灵活的限制粒度,如一组API共享限制,实现难度不高,扩展
limit.json
与limit.lua
可完成 -
达到限制时反馈Retry-After,建议多久后可重新请求
补充
针对resty.limit.req
以及burst
额外补充一些知识以便更深入理解限频与限流的区别。文章开始用“单位时间”描述限频,“时间窗口”描述限流,单位时间全文下来都假定为秒,这是为了更通用的表达和理解。但在Nginx内部实现的单位是毫秒,那么针对描述“每秒请求数不超过10”的理解应该是“每100毫秒的请求数不超过1”。
在一秒的开始时刻服务器接收到10个请求,按每100毫秒请求数不超过1原则,此时系统只会处理1个请求,剩余9个被拒绝。设置burst=10可解决该问题,让系统不拒绝剩余9个请求,从外部观察者角度即是1秒处理10个请求,没有触发限制。
结语
对于实现部分很多细节没有剖析和描述,主要由于此文在实践很久后才写,记忆力有些模糊。
最近两年因工作太忙,关注一时所得而忽视了长期成长,长久来看还是得不偿失,因此选择继续更新文章。