OpenResty限频限流

OpenResty限频限流

之前对系统限流这个话题有过一次讨论,之后有在实际工作中遇到一次应用,写一篇文章记录本次实践。

背景

目前在做的系统有提供Open API接入,即客户通过API Key接入使用系统功能。所允许接入的API根据功能种类不同所需系统开销有所差异,从技术角度希望对系统增加一些保护措施,避免用户接入使用过程有意或无意高频调用一些API对系统构成压力,从而影响整个系统稳定性。

限制主要考虑两个维度:

  • 限频,限制单位时间内调用次数,关注调用速度
  • 限流,限制时间窗口内调用次数,关注调用总量

如上描述,限频、限流本质是同一件事:“限制一定时间内的调用次数”,但此处特意使用两个不同的名词用于体现差异性(后文会多次提及):

  • 单位时间,粒度小,常量,通常用秒描述
  • 时间窗口,粒度大,变量,如一分钟、五分钟、十五分钟或一小时等

在目标系统中针对特定API(POST /api/order),期望实现:“每秒调用不能超过20次,每分钟调用不能超过200次”,前者称之为限频,后者称为限流。

方案

限频限流可以选择在应用层实现,早期在一些项目中的确有借助Redis实践过,结论是只能满足前期需要,后期系统QPS过高时应用层与Redis的开销不容小觑。

当前项目的Open API网关是OpenResty,选择把限频限流做在网关这一层是个不错的选择,重点依赖OpenResty官方提供的几个Lua扩展:

借助上述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.jsonlimit.lua可完成

  • 达到限制时反馈Retry-After,建议多久后可重新请求

补充

针对resty.limit.req以及burst额外补充一些知识以便更深入理解限频与限流的区别。文章开始用“单位时间”描述限频,“时间窗口”描述限流,单位时间全文下来都假定为秒,这是为了更通用的表达和理解。但在Nginx内部实现的单位是毫秒,那么针对描述“每秒请求数不超过10”的理解应该是“每100毫秒的请求数不超过1”。

在一秒的开始时刻服务器接收到10个请求,按每100毫秒请求数不超过1原则,此时系统只会处理1个请求,剩余9个被拒绝。设置burst=10可解决该问题,让系统不拒绝剩余9个请求,从外部观察者角度即是1秒处理10个请求,没有触发限制。

结语

对于实现部分很多细节没有剖析和描述,主要由于此文在实践很久后才写,记忆力有些模糊。

最近两年因工作太忙,关注一时所得而忽视了长期成长,长久来看还是得不偿失,因此选择继续更新文章。

参考资料

博客原文

上一篇:redis笔记汇总


下一篇:使用Redis事务与乐观锁、Lua脚本解决秒杀系统问题