暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Apache APISIX 插件开发之精细化限速场景完善

APISIX 云原生微服务网关 2021-11-11
869


需求背景


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").new
          local core = require("apisix.core")
          local plugin_name = "limit-count-by-client"
          local limit_redis_cluster_new
          local limit_redis_new
          do
             local redis_src = "apisix.plugins.limit-count.limit-count-redis"
             limit_redis_new = require(redis_src).new

             local cluster_src = "apisix.plugins.limit-count.limit-count-redis-cluster"
             limit_redis_cluster_new = require(cluster_src).new
          end
          local 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 then
                 return false, err
             end

             return true
          end


          local 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 = 0
             local item_time_window = 0
             if conf.map[req_key] ~= nil then
                 item_count = conf.map[req_key].count
                 item_time_window = conf.map[req_key].time_window
             else
                 item_count = conf.default_count
                 item_time_window = conf.default_time_window

             end

             if not conf.policy or conf.policy == "local" then
                 return limit_local_new("plugin-" .. plugin_name, item_count,
                                        item_time_window)
             end

             if conf.policy == "redis" then
                 return limit_redis_new("plugin-" .. plugin_name,
                                        item_count, item_time_window, conf)
             end

             if conf.policy == "redis-cluster" then
                 return limit_redis_cluster_new("plugin-" .. plugin_name, item_count,
                                                item_time_window, conf)
             end

             return nil
          end


          function _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 then
                 core.log.error("failed to fetch limit.count object: ", err)
                 return 500
             end
             local req_key = ctx.var[conf.key]
             local limit_key = req_key .. conf.scope
             local key = (limit_key or "") .. ctx.conf_type .. ctx.conf_version
             core.log.info("limit key: ", key)

             local delay, remaining = lim:incoming(key, true)
             if not delay then
                 local err = remaining
                 if err == "rejected" then
                     return conf.rejected_code
                 end

                 core.log.error("failed to limit req: ", err)
                 return 500, {error_msg = "failed to limit count: " .. err}
             end
             local item_count = 0
             local item_time_window = 0
             if conf.map[req_key] ~= nil then
                 item_count = conf.map[req_key].count
             else
                 item_count = conf.default_count

             end
             core.response.set_header("X-RateLimit-Limit", item_count,
                                      "X-RateLimit-Remaining", remaining)
          end


          return _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


              文章转载自APISIX 云原生微服务网关,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

              评论