需求背景
Apache APISIX 当前版本 (2.10.1) 请求频率限制相关插件有 limit-count、limit-req、limit-conn 三种,但目前这些插件都只支持给固定 Key 来设置一个统一的限速。
比如在 Service 或 Route 下添加 limit-count 插件,给 Consumer 进行每秒 10 次的限速阈值。
"limit-count": {"count": 10,"key": "consumer_name","policy": "local","rejected_code": 503,"time_window": 1}
再比如,在 Consumer 侧给指定的 Consumer 添加 limit-count 插件,让该 Consumer 访问任意 Service 都是一样的限速阈值。
"limit-count": {"count": 10,"key": "service_id","policy": "local","rejected_code": 503,"time_window": 1}
但在实际业务场景中,这个限速还太笼统,达不到精细化业务要求。比如,业务 A 要求给 ConsumerA 限制 1 分钟能访问 100 次,给 ConsumerB 1 分钟限制能访问 1000 次,这个限速可以在 Consumer 侧给 A、B 分别设置限速,但是当 ConsumerA 和 ConsumerB 还需要访问业务 B,且业务 B 又有不同限速需求的时候,以上插件就玩不转了,除非客户端针对每个业务都要生成不同的 Consumer,那这个就太复杂了。
为了满足这个需求,我们一开始也与 Apache APISIX 社区技术负责人做了探讨,不过社区目前可能正在进行其他功能的排期,所以暂时没进行相关操作,感兴趣的朋友可以先看下 issue [1]。
所以后续我们自己在 limit-count 插件基础上进行改造,重新设计了一个限速机制,能够针对不同的服务给不同的 Consumer 设置差异化的访问限速,满足生产环境更精细化的限速需求。
解决方案
方案的原理很简单,就是将 limit-count 配置中插入一个 table,在 table 里支持定义更复杂的 Key 和阈值,具体插件的配置 Schema 如下:
{"scope": "route_id", # 标明插件添加位置,支持 route_id 和 service_id"default_count": 1000, # 设置默认的限速阈值"default_time_window": 60, # 设置默认的时间窗口"key": "consumer_name", # 设置要限速的客户端对象,支持 ["remote_addr", "server_addr", "http_x_real_ip", "http_x_forwarded_for", "consumer_name"]"map": { # 给每个限速对象分别设置不同的限速阈值和时间窗口"ConsumerA": {"count": 300,"time_window": 60},"ConsumerB": {"count": 300,"time_window": 60},"ConsumerC": {"count": 300,"time_window": 60}},"policy": "local","rejected_code": 429 # 官方的限速插件超过限制访问返回的 503,并不友好,这里改成更加直白的 429 Too Many Requests}
更多细节上的插件代码可以参考下方:
---- Licensed to the Apache Software Foundation (ASF) under one or more-- contributor license agreements. See the NOTICE file distributed with-- this work for additional information regarding copyright ownership.-- The ASF licenses this file to You under the Apache License, Version 2.0-- (the "License"); you may not use this file except in compliance with-- the License. You may obtain a copy of the License at---- http://www.apache.org/licenses/LICENSE-2.0---- Unless required by applicable law or agreed to in writing, software-- distributed under the License is distributed on an "AS IS" BASIS,-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.-- See the License for the specific language governing permissions and-- limitations under the License.--local limit_local_new = require("resty.limit.count").newlocal core = require("apisix.core")local plugin_name = "limit-count-by-client"local limit_redis_cluster_newlocal limit_redis_newdolocal redis_src = "apisix.plugins.limit-count.limit-count-redis"limit_redis_new = require(redis_src).newlocal cluster_src = "apisix.plugins.limit-count.limit-count-redis-cluster"limit_redis_cluster_new = require(cluster_src).newendlocal lrucache = core.lrucache.new({type = 'plugin', serial_creating = true,})local schema = {type = "object",properties = {key = {type = "string",enum = {"remote_addr", "server_addr", "http_x_real_ip","http_x_forwarded_for", "consumer_name"},default = "remote_addr",},default_count = {type = "integer", exclusiveMinimum = 0},default_time_window = {type = "integer", exclusiveMinimum = 0},scope = {type = "string",enum = {"route_id", "service_id"},default = "route_id",},map = {type = "object",items = {type = "object",count = {type = "integer", exclusiveMinimum = 0},time_window = {type = "integer", exclusiveMinimum = 0},}},rejected_code = {type = "integer", minimum = 200, maximum = 599, default = 429},policy = {type = "string",enum = {"local", "redis", "redis-cluster"},default = "local",}},dependencies = {policy = {oneOf = {{properties = {policy = {enum = {"local"},},},},{properties = {policy = {enum = {"redis"},},redis_host = {type = "string", minLength = 2},redis_port = {type = "integer", minimum = 1, default = 6379,},redis_password = {type = "string", minLength = 0,},redis_database = {type = "integer", minimum = 0, default = 0,},redis_timeout = {type = "integer", minimum = 1, default = 1000,},},required = {"redis_host"},},{properties = {policy = {enum = {"redis-cluster"},},redis_cluster_nodes = {type = "array",minItems = 2,items = {type = "string", minLength = 2, maxLength = 100},},redis_password = {type = "string", minLength = 0,},redis_timeout = {type = "integer", minimum = 1, default = 1000,},redis_cluster_name = {type = "string",},},required = {"redis_cluster_nodes", "redis_cluster_name"},}}}}}local _M = {version = 0.4,priority = 1002,name = plugin_name,schema = schema,}function _M.check_schema(conf)local ok, err = core.schema.check(schema, conf)if not ok thenreturn false, errendreturn trueendlocal function create_limit_obj(conf, ctx)core.log.info("create new limit-count plugin instance")local req_key = ctx.var[conf.key]local item_count = 0local item_time_window = 0if conf.map[req_key] ~= nil thenitem_count = conf.map[req_key].countitem_time_window = conf.map[req_key].time_windowelseitem_count = conf.default_countitem_time_window = conf.default_time_windowendif not conf.policy or conf.policy == "local" thenreturn limit_local_new("plugin-" .. plugin_name, item_count,item_time_window)endif conf.policy == "redis" thenreturn limit_redis_new("plugin-" .. plugin_name,item_count, item_time_window, conf)endif conf.policy == "redis-cluster" thenreturn limit_redis_cluster_new("plugin-" .. plugin_name, item_count,item_time_window, conf)endreturn nilendfunction _M.access(conf, ctx)core.log.info("ver: ", ctx.conf_version)local lim, err = core.lrucache.plugin_ctx(lrucache, ctx, conf.policy, create_limit_obj, conf, ctx)if not lim thencore.log.error("failed to fetch limit.count object: ", err)return 500endlocal req_key = ctx.var[conf.key]local limit_key = req_key .. conf.scopelocal key = (limit_key or "") .. ctx.conf_type .. ctx.conf_versioncore.log.info("limit key: ", key)local delay, remaining = lim:incoming(key, true)if not delay thenlocal err = remainingif err == "rejected" thenreturn conf.rejected_codeendcore.log.error("failed to limit req: ", err)return 500, {error_msg = "failed to limit count: " .. err}endlocal item_count = 0local item_time_window = 0if conf.map[req_key] ~= nil thenitem_count = conf.map[req_key].countelseitem_count = conf.default_countendcore.response.set_header("X-RateLimit-Limit", item_count,"X-RateLimit-Remaining", remaining)endreturn _M
启用方法
需要注意的是,这个插件改造后只能加到 Service 或 Router 中,而不能加到 Consumer 位置,所以取名叫 limit-count-by-client。大家在使用时一定要注意应用位置。
按照上述操作补充并完善后,就可以将其应用到实际场景中进行操作了。在这里,我们需要将插件代码保存为 limit-count-by-client,然后拷贝到 apisix/plugins。之后在 config.yaml 插件位置启用,其中包括 2 个配置:
# 前面略...nginx_config:http:lua_shared_dicts:plugin-limit-count-by-client: 10m # 插件的 policy 使用 local 模式的时候,需要用到共享内存# 内容略...plugins:- # 内容略..- limit-count-by-client## 后面内容略...
具体配置,这里贴一个结合 HMAC 认证插件,实现对具体用户进行限频的路由配置,仅供参考:
{"uris": ["/hello"],"plugins": {"hmac-auth": {"disable": false},"limit-count-by-client": {"default_count": 1000,"default_time_window": 60,"key": "consumer_name","map": {"consumer_A": {"count": 1000,"time_window": 60},"consumer_B": {"count": 500,"time_window": 60}},"policy": "local","rejected_code": 429,"scope": "route_id"}},"service_id": "service_foo","status": 1}
如果上述操作需要在官方 Dashboard 启用,则需要更新一下 Dashboard 中的 schema.json 文件,这里就不细说了。
原文链接:https://zhang.ge/5158.html
[1] :
https://github.com/apache/apisix/issues/4760

关于 Apache APISIX
Apache APISIX 是一个动态、实时、高性能的开源 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。Apache APISIX 可以帮助企业快速、安全地处理 API 和微服务流量,包括网关、Kubernetes Ingress 和服务网格等。
Apache APISIX 落地用户(仅部分)

Apache APISIX GitHub:https://github.com/apache/apisix
Apache APISIX 官网:https://apisix.apache.org/
Apache APISIX 文档:https://apisix.apache.org/zh/docs/apisix/getting-started




