Rate-limiting是kong 生态的一款开源限流插件, 提供根据时间窗口内请求数量限流的一款插件,其限流策略支持Local、 Redis、cluster, 限流粒度支持: consumer, credential, ip, service, header, path几个级别的限流, 限流方式可以用于 Global, Router, Service 。
Docker-compose.yml文件如下:
version: "3.7"services: kong-database:image: postgres:9.6container_name: kong-databaseports:- "5432:5432"environment:- POSTGRES_USER=kong- POSTGRES_DB=kong- POSTGRES_PASSWORD=kongpassnetworks:- kong-netvolumes:- ./docker-data/postgresql/postgresql:/var/lib/postgresql/datakong-gateway:# 镜像版本,目前最新image: kong/kong-gateway:2.8.1.0-alpinecontainer_name: kong-gatewayenvironment:# 数据持久化方式,使用postgres数据库 - "KONG_DATABASE=postgres"# 数据库容器名称,Kong连接数据时使用些名称 - "KONG_PG_HOST=kong-database" - "KONG_PG_USER=kong" - "KONG_PG_PASSWORD=kongpass"# 数据库名称 - "KONG_CASSANDRA_CONTACT_POINTS=kong-database"# 日志记录目录 - "KONG_PROXY_ACCESS_LOG=/dev/stdout" - "KONG_ADMIN_ACCESS_LOG=/dev/stdout" - "KONG_PROXY_ERROR_LOG=/dev/stderr" - "KONG_ADMIN_ERROR_LOG=/dev/stderr"# 暴露的端口 - "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" - "KONG_ADMIN_GUI_URL=http://localhost:8002"ports: - 8000:8000 - 8443:8443 - 8001:8001 - 8444:8444 - 8002:8002 - 8445:8445 - 8003:8003 - 8004:8004# 使用docker网络networks: - kong-net# 依赖数据库服务depends_on: - kong-database# kong 管理界面konga:image: pantsel/kongacontainer_name: kongaenvironment: - "TOKEN_SECRET=51liveup.cn" # 初始化使用development, 去初始化数据表, 正式使用production - "NODE_ENV=development" - "DB_ADAPTER=postgres"- "DB_HOST=kong-database" - "DB_PORT=5432" - "DB_USER=kong" - "DB_PASSWORD=kongpass" - "DB_DATABASE=konga-db"ports: - 8080:1337networks: - kong-netdepends_on: - kong-databasenetworks:kong-net:external: true此文件执行启动时会包kong数据库初始化异常,需要执行初始化脚本,注意版本匹配
docker run --rm --network=kong-net-e "KONG_DATABASE=postgres"-e "KONG_PG_HOST=kong-database"-e "KONG_PG_PASSWORD=kongpass" kong/kong-gateway:2.8.1.0-alpine kong migrations bootstrap具体环境部署会在后期文章中详细讲解下面进入正题,
因本次再使用Rate-limiting 插件中, 需要根据具体某一path 实现限流,其path需要支持通配, 故使用了konga UI 界面配置了 根据path限流, 如下
在测试过程中发现并未达到想要的效果, 即 path匹配则限流, path 不匹配则不限流, 在多次测试中发现, 均使用了Ip 限流,分析起源码发现是插件本身逻辑的问题
插件源码路径: /usr/local/share/lua/5.1/kong/plugins/rate-limiting
local function get_identifier(conf)local identifierif conf.limit_by == "service" thenidentifier = (kong.router.get_service() orEMPTY).idelseif conf.limit_by == "consumer" thenidentifier = (kong.client.get_consumer() orkong.client.get_credential() orEMPTY).idelseif conf.limit_by == "credential" thenidentifier = (kong.client.get_credential() orEMPTY).idelseif conf.limit_by == "header" thenidentifier = kong.request.get_header(conf.header_name)elseif conf.limit_by == "path" thenlocal req_path = kong.request.get_path()if req_path == conf.path thenidentifier = req_pathendreturn identifier or kong.client.get_forwarded_ip()end逻辑解析: Rate limiting 只执行时需要根据 rout_id, service_id, indentifier, period_date, period生成一个唯一的local_key进行标识(如下图),而从上面的代码发现 indentifier是根你的限流粒度紧密相关,
从上面的代码可以看出在path匹配判断中, 如果path 匹配,将使用req_path作为identifier去后续生成local-key, 但是如果path不匹配,将使用ip 作为identifier, 故此时自动回退到使用ip 限流
local get_local_key = function(conf, identifier, period, period_date)local service_id, route_id = get_service_and_route_ids(conf)return fmt("ratelimit:%s:%s:%s:%s:%s", route_id, service_id, identifier, period_date, period)end2. 改造插件:由于我的部署方式采用的是docker 部署,故需要先将脚本文件挂载出来, 再修改,对docker-compose 配置文件 kong-gateway服务下添加挂载volumes,如下
volumes: - ./docker-data/kong/handler.lua:/usr/local/share/lua/5.1/kong/plugins/rate-limiting/handler.lua修改handler.lua脚本如下:
local function get_identifier(conf)local identifierif conf.limit_by == "service" thenidentifier = (kong.router.get_service() orEMPTY).idelseif conf.limit_by == "consumer" thenidentifier = (kong.client.get_consumer() orkong.client.get_credential() orEMPTY).idelseif conf.limit_by == "credential" thenidentifier = (kong.client.get_credential() orEMPTY).idelseif conf.limit_by == "header" thenidentifier = kong.request.get_header(conf.header_name)elseif conf.limit_by == "path" thenlocal req_path = kong.request.get_path()--此处可以添加通配处理逻辑,if req_path == conf.path thenidentifier = req_pathendendkong.client.get_forwarded_ip())--去掉默认ip 限流配置,但此时如果path 不匹配 identifier 将会是空(nil)--return identifier or kong.client.get_forwarded_ip()return identifierend修改此方法如下 local function get_usage(conf, identifier, current_timestamp, limits)
local function get_usage(conf, identifier, current_timestamp, limits)local usage = {}local stop--添加identifier 为空时不进行限流计算(源码没有这个为空校验),在后续处理中,stop为空,将不会进行限流if identifier thenfor period, limit in pairs(limits) dolocal current_usage, err = policies[conf.policy].usage(conf, identifier, period, current_timestamp)if err thenreturn nil, nil, errend-- What is the current usage for the configured limit name?local remaining = limit - current_usage-- Recording usageusage[period] = {limit = limit,remaining = remaining,}if remaining <= 0 thenstop = periodendendendreturn usage, stopend主方法处理逻辑如下:
function RateLimitingHandler:access(conf)local current_timestamp = time() * 1000-- Consumer is identified by ip address or authenticated_credential idlocal identifier = get_identifier(conf)local fault_tolerant = conf.fault_tolerant-- Load current metric for configured periodlocal limits = {second = conf.second,minute = conf.minute,hour = conf.hour,day = conf.day,month = conf.month,year = conf.year,}local usage, stop, err = get_usage(conf, identifier, current_timestamp, limits)if err thenif not fault_tolerant thenreturn error(err)endkong.log.err("failed to get usage: ", tostring(err))end --将限流信息放进header 中包含信息有: RATELIMIT_LIMIT, RATELIMIT_REMAINING,RATELIMIT_RESET等字段,可以在浏览器端查看if usage then-- Adding headerslocal resetlocal headersif not conf.hide_client_headers thenheaders = {}local timestampslocal limitlocal windowlocal remainingfor k, v in pairs(usage) dolocal current_limit = v.limitlocal current_window = EXPIRATION[k]local current_remaining = v.remainingif stop == nil or stop == k thencurrent_remaining = current_remaining - 1endcurrent_remaining = max(0, current_remaining)if not limit or (current_remaining < remaining) or (current_remaining == remaining and current_window > window)thenlimit = current_limitwindow = current_windowremaining = current_remainingif not timestamps thentimestamps = timestamp.get_timestamps(current_timestamp)endreset = max(1, window - floor((current_timestamp - timestamps[k]) / 1000))endheaders[X_RATELIMIT_LIMIT[k]] = current_limitheaders[X_RATELIMIT_REMAINING[k]] = current_remainingendheaders[RATELIMIT_LIMIT] = limitheaders[RATELIMIT_REMAINING] = remainingheaders[RATELIMIT_RESET] = resetend-- If limit is exceeded, terminate the request-- 如果此处stop 为空将不会进行限流,if stop thenheaders = headers or {}headers[RETRY_AFTER] = resetreturn kong.response.error(429, "API rate limit exceeded", headers)endif headers thenkong.response.set_headers(headers)endendlocal ok, err = timer_at(0, increment, conf, limits, identifier, current_timestamp, 1)if not ok thenkong.log.err("failed to create timer: ", err)endend原文:https://juejin.cn/post/7097143922895355912logo设计
创造品牌价值
¥500元起
APP开发
量身定制,源码交付
¥2000元起
商标注册
一个好品牌从商标开始
¥1480元起
公司注册
注册公司全程代办
¥0元起
查
看
更
多