Kong的限流从技术上来讲支持三种,分别是本地限流(local)、数据库限流(cluster)和Redis限流,这三种限流方式采用的限流算法都是计数器法。支持按照秒/分/小时/日/月/年等不同时间单位限流,并且可以组合,例如可以设置每秒最多100次并且每分钟最多1000次。
首先介绍一下本地限流(local),本地限流计数器采用的是nginx的缓存,缓存的Key是:
--[[
second = math.floor(second:timestamp() * 1000),
minute = minute:timestamp() * 1000,
hour = hour:timestamp() * 1000,
day = day:timestamp() * 1000,
month = month:timestamp() * 1000,
year = year:timestamp() * 1000
--]]
-- name:上面数据结构的Key
-- period_date:上面数据结构的Value
local get_local_key = function(api_id, identifier, period_date, name)
return fmt("ratelimit:%s:%s:%s:%s", api_id, identifier, period_date, name)
end
如果用户配置了每秒最多100次并且每分钟最多1000次,则Kong会相应地创建2个缓存分别对时间单位秒和分进行计数,单位秒的缓存失效时间是1秒,单位分的失效时间是1分钟。
计数器算法连个重要的方法是查询计数和增加计数,Kong里面分别是usage,increment。关键代码如下:
increment = function(conf, api_id, identifier, current_timestamp, value)
local periods = timestamp.get_timestamps(current_timestamp)
for period, period_date in pairs(periods) do
local cache_key = get_local_key(api_id, identifier, period_date, period)
if not cache.rawget(cache_key) then
cache.rawset(cache_key, 0, EXPIRATIONS[period])
end
local _, err = cache.incr(cache_key, value)
end
return true
end,
usage = function(conf, api_id, identifier, current_timestamp, name)
local periods = timestamp.get_timestamps(current_timestamp)
local cache_key = get_local_key(api_id, identifier, periods[name], name)
local current_metric, err = cache.rawget(cache_key)
return current_metric and current_metric or 0
end
Redis限流和本地限流类似,只是将缓存放到了Redis上,除去连接Redis等代码外,关键代码如下:
increment = function(conf, api_id, identifier, current_timestamp, value)
local periods = timestamp.get_timestamps(current_timestamp)
for period, period_date in pairs(periods) do
local cache_key = get_local_key(api_id, identifier, period_date, period)
local exists, err = red:exists(cache_key)
red:init_pipeline((not exists or exists == 0) and 2 or 1)
red:incrby(cache_key, value)
if not exists or exists == 0 then
red:expire(cache_key, EXPIRATIONS[period])
end
local _, err = red:commit_pipeline()
end
return true
end,
usage = function(conf, api_id, identifier, current_timestamp, name)
local periods = timestamp.get_timestamps(current_timestamp)
local cache_key = get_local_key(api_id, identifier, periods[name], name)
local current_metric, err = red:get(cache_key)
return current_metric and current_metric or 0
end
数据库限流(cluster)是将计数器保存在了数据库里,Kong支持两种数据库,PostgreSQL和Cassandra,对于PostgreSQL,为了提升性能,增加计数采用了存储过程:
CREATE OR REPLACE FUNCTION increment_rate_limits(
a_id uuid, i text, p text, p_date timestamp with time zone, v integer)
RETURNS VOID AS $$
BEGIN
LOOP
UPDATE ratelimiting_metrics
SET value = value + v
WHERE api_id = a_id AND identifier = i AND period = p AND period_date = p_date;
IF found then
RETURN;
END IF;
BEGIN
INSERT INTO ratelimiting_metrics(api_id, period, period_date, identifier, value)
VALUES(a_id, p, p_date, i, v);
RETURN;
EXCEPTION WHEN unique_violation THEN
END;
END LOOP;
END;
而Cassandra则是直接采用UPDATE语句。
Kong的这三种限流方式都没有考虑并发情况
-- Load current metric for configured period
local usage, stop, err = get_usage(conf, api_id, identifier, current_timestamp, {
second = conf.second,
minute = conf.minute,
hour = conf.hour,
day = conf.day,
month = conf.month,
year = conf.year
})
if usage then
-- If limit is exceeded, terminate the request
if stop then
return responses.send(429, "API rate limit exceeded")
end
end
-- Increment metrics for all periods if the request goes through
local ok, err = ngx_timer_at(0, incr, conf, api_id, identifier, current_timestamp, 1)
if not ok then
ngx_log(ngx.ERR, "failed to create timer: ", err)
end
当到达限流的临界值(max-1)时,此时有多条请求同时执行get_usage,计算结果全部通过,而我们期望的是仅有一条请求能通过。