Kong的路由匹配策略基本上是Kong最核心的部分了,逻辑虽然不是很复杂,但是代码还是比较复杂的。
整个路由匹配流程分为两部分:第一部分是系统启动初始化时对路由分类,建立索引。第二部分时请求到来时,根据请求的特征去查找相关路由。具体过程如下:
1. 路由初始化
Kong的路由初始化是在init()
阶段,调用kong.core.handler中的build_router()
local function build_router(dao, version)
local apis, err = dao.apis:find_all() --从数据库中加载所有的API
if err then
return nil, "could not load APIs: " .. err
end
for i = 1, #apis do
-- alias since the router expects 'headers' as a map
if apis[i].hosts then
apis[i].headers = { host = apis[i].hosts }
end
end
table.sort(apis, function(api_a, api_b)
return api_a.created_at < api_b.created_at -- 根据api的创建时间进行排序
end)
router, err = Router.new(apis) -- 创建路由
if not router then
return nil, "could not create router: " .. err
end
if version then
router_version = version
end
return true
end
首先启动时从数据库中加载所有的API,并根据创建时间对API进行排序,然后调用router
创建一个新的路由器。
-- kong/core/router.lua
local plain_indexes = {
hosts = {},
uris = {},
methods = {},
} -- 存储普通的host、uri和methods
local uris_prefixes = {} -- 存储前缀匹配的uri
local uris_regexes = {} -- 存储含有正则的uri
local wildcard_hosts = {} --存储带有正则的host
local categories = {} -- 对API进行分类
for i = 1, #apis do
-- 第一步:解析API
local api_t, err = marshall_api(apis[i])
if not api_t then
return nil, err
end
-- 第二步:对API进行分组
categorize_api_t(api_t, api_t.match_rules, categories)
-- 第三步:创建快速索引
index_api_t(api_t, plain_indexes, uris_prefixes, uris_regexes,
wildcard_hosts)
end
-- uri按照从长到短进行排序
table.sort(uris_prefixes, function(a, b)
return #a > #b
end)
table.sort(uris_prefixes, function(uri_t_a, uri_t_b)
return #uri_t_a.value > #uri_t_b.value
end)
创建路由器的过程分为三个步骤:解析API,对API进行分组,建立快速索引
1.1 解析API
解析API做的事情就是把数据库中的格式转换成自己的数据结构,并标记uri是否是前缀匹配,host是否带有通配符等
-- Kong主要采用host uri 和method来匹配路由,每种元素都有自己的编码
local MATCH_RULES = {
HOST = 0x01,
URI = 0x02,
METHOD = 0x04,
}
local function marshall_api(api)
if not (api.headers or api.methods or api.uris) then
return nil, "could not categorize API"
end
-- 解析后的api_t数据结构
local api_t = {
api = api, --原始api
strip_uri = api.strip_uri, -- 是否strip uri
preserve_host = api.preserve_host, --传给upstream是否保留host
match_rules = 0x00, --当前api所属的匹配策略码,亦即所属类别
hosts = {},
uris = {},
methods = {},
upstream_url_t = {},
}
解析hosts:
Kong将host、uri和method各分配了一个策略码,然后对他们进行按位与从而达到分类的目的
if api.headers then
---省略校验代码
local host_values = api.headers["Host"] or api.headers["host"]
if #host_values > 0 then
-- 采用按位 或 确定策略
api_t.match_rules = bor(api_t.match_rules, MATCH_RULES.HOST)
for _, host_value in ipairs(host_values) do
if find(host_value, "*", nil, true) then
-- 查看是否带有通配符
local wildcard_host_regex = host_value:gsub("%.", "\\.")
:gsub("%*", ".+") .. "$"
insert(api_t.hosts, {
wildcard = true,
value = host_value,
regex = wildcard_host_regex,
})
else
insert(api_t.hosts, {
value = host_value,
})
end
api_t.hosts[host_value] = host_value
end
end
end
解析uri和method的逻辑和解析host的逻辑类似。如果一个api同时配置了host、uri 和method,那么类别为7。
1.2. API分组
API分组比较简单,主要将上一步解析出来的API按照类别重新组织一下数据结构
local function categorize_api_t(api_t, bit_category, categories)
local category = categories[bit_category]
if not category then
category = {
apis_by_hosts = {},
apis_by_uris = {},
apis_by_methods = {},
all = {},
}
categories[bit_category] = category
end
insert(category.all, api_t)
for _, host_t in ipairs(api_t.hosts) do
if not category.apis_by_hosts[host_t.value] then
category.apis_by_hosts[host_t.value] = {}
end
insert(category.apis_by_hosts[host_t.value], api_t)
end
-- 省略uri 和method分组逻辑,和host相同
end
1.3 建立索引
这里说建立索引其实是将host
、uri
和method
分别拆出来放到单独的数组里面
local function index_api_t(api_t, plain_indexes, uris_prefixes, uris_regexes,
wildcard_hosts)
for _, host_t in ipairs(api_t.hosts) do
if host_t.wildcard then
insert(wildcard_hosts, host_t)
else
plain_indexes.hosts[host_t.value] = true
end
end
for _, uri_t in ipairs(api_t.uris) do
if uri_t.is_prefix then
plain_indexes.uris[uri_t.value] = true
insert(uris_prefixes, uri_t)
else
insert(uris_regexes, uri_t)
end
end
for method in pairs(api_t.methods) do
plain_indexes.methods[method] = true
end
end
路由的建立上面已经分析完了,主要做的事情是解析API,然后将API根据特征分类,最后再对host、uri和method建立快速索引
2. 路由匹配
Kong匹配API是在access阶段的
function Kong.access()
local ctx = ngx.ctx
core.access.before(ctx)
-- 省略后面执行插件的逻辑
最终会到router的exec方法中
function self.exec(ngx)
local method = ngx.req.get_method()
local request_uri = ngx.var.request_uri
local uri = request_uri
do
local idx = find(uri, "?", 2, true)
if idx then
uri = sub(uri, 1, idx - 1)
end
end
local req_host = ngx.var.http_host or ""
local match_t = find_api(method, uri, req_host, ngx)
if not match_t then
return nil
end
return match_t
end
再到find_api方法
首先检查缓存中是否存在
local cache_key = fmt("%s:%s:%s", req_method, req_uri, req_host)
do
local match_t = cache:get(cache_key)
if match_t then
return match_t
end
end
如果缓存中不存在,根据刚开始建立的索引来判断当前请求所属的类别
if plain_indexes.hosts[req_host] then
-- 通过 按位或计算类别
req_category = bor(req_category, MATCH_RULES.HOST)
elseif req_host then
for i = 1, #wildcard_hosts do
local from, _, err = re_find(req_host, wildcard_hosts[i].regex, "ajo")
if err then
log(ERR, "could not match wildcard host: ", err)
return
end
if from then
hits.host = wildcard_hosts[i].value
-- 通过 按位或计算类别
req_category = bor(req_category, MATCH_RULES.HOST)
break
end
end
end
-- 省略uri和method的匹配过程,和上面的类似
计算出所属的类别后,根据类别值判断出其在应该查找的API集合中的索引。比如一个请求带有host uri和methoud三个参数,那么其应该从 {host、uri、method}(API类别7) {host、uri}{API类别3}依次往下寻找匹配的API。查找时先从一个精简的集合中查找,如果查不到再全量查找。
local CATEGORIES = {
bor(MATCH_RULES.HOST, MATCH_RULES.URI, MATCH_RULES.METHOD),
bor(MATCH_RULES.HOST, MATCH_RULES.URI),
bor(MATCH_RULES.HOST, MATCH_RULES.METHOD),
bor(MATCH_RULES.METHOD, MATCH_RULES.URI),
MATCH_RULES.HOST,
MATCH_RULES.URI,
MATCH_RULES.METHOD,
}
local categories_len = #CATEGORIES
local CATEGORIES_LOOKUP = {}
for i = 1, categories_len do
CATEGORIES_LOOKUP[CATEGORIES[i]] = i
end
local category_idx = CATEGORIES_LOOKUP[req_category]
local matched_api
-- 根据类别值判断出其在应该查找的API集合中的索引。比如一个请求带有host uri和methoud三个参数,那么其应该
-- 从 {host、uri、method}(API类别7) {host、uri}{API类别3}依次往下寻找匹配的API
while category_idx <= categories_len do
local bit_category = CATEGORIES[category_idx]
local category = categories[bit_category]
if category then
-- 从一个精简结合中查找
-- 如果精简集合中查不到,则全量查找此类别的所有API
end
end
精简集合查找过程
local reduced_candidates, category_candidates = reduce(category, bit_category, ctx)
if reduced_candidates then
for i = 1, #reduced_candidates do
if match_api(reduced_candidates[i], ctx) then
matched_api = reduced_candidates[i]
break
end
end
end
do
local reducers = {
[MATCH_RULES.HOST] = function(category, ctx)
return category.apis_by_hosts[ctx.hits.host]
end,
[MATCH_RULES.URI] = function(category, ctx)
return category.apis_by_uris[ctx.hits.uri or ctx.req_uri]
end,
[MATCH_RULES.METHOD] = function(category, ctx)
return category.apis_by_methods[ctx.req_method]
end,
}
reduce = function(category, bit_category, ctx)
-- run cached reducer
if type(reducers[bit_category]) == "function" then
return reducers[bit_category](category, ctx), category.all
end
-- build and cache reducer
local reducers_set = {}
-- 采用按位与将非1、2、4的API类别转为1、2、4的精简子集
for _, bit_match_rule in pairs(MATCH_RULES) do
if band(bit_category, bit_match_rule) ~= 0 then
reducers_set[#reducers_set + 1] = reducers[bit_match_rule]
end
end
reducers[bit_category] = function(category, ctx)
local min_len = 0
local smallest_set
-- 选择集合元素最小的作为结果集返回
-- 比如当前类别为7,其中国 host集合中有5个API,uri有3个API,method有8个API,那么返回uri这个集合中的API作为查找集
for i = 1, #reducers_set do
local candidates = reducers_set[i](category, ctx)
if candidates ~= nil and (not smallest_set or #candidates < min_len)
then
min_len = #candidates
smallest_set = candidates
end
end
return smallest_set
end
return reducers[bit_category](category, ctx), category.all
end
end
得到应该查找的API集合后,就采用匹配器进行比较
for i = 1, #reduced_candidates do
if match_api(reduced_candidates[i], ctx) then
matched_api = reduced_candidates[i]
break
end
end
do
local matchers = {
[MATCH_RULES.HOST] = function(api_t, ctx)
local host = api_t.hosts[ctx.hits.host or ctx.req_host]
if host then
ctx.matches.host = host
return true
end
end,
-- 省略uri和method的比较过程
}
match_api = function(api_t, ctx)
-- run cached matcher
if type(matchers[api_t.match_rules]) == "function" then
clear_tab(ctx.matches)
return matchers[api_t.match_rules](api_t, ctx)
end
local matchers_set = {}
-- 仍然转化为对host、method和uri的比较
for _, bit_match_rule in pairs(MATCH_RULES) do
if band(api_t.match_rules, bit_match_rule) ~= 0 then
matchers_set[#matchers_set + 1] = matchers[bit_match_rule]
end
end
matchers[api_t.match_rules] = function(api_t, ctx)
clear_tab(ctx.matches)
for i = 1, #matchers_set do
if not matchers_set[i](api_t, ctx) then
return
end
end
return true
end
return matchers[api_t.match_rules](api_t, ctx)
end
从精简集合中如果找不到的话就全量查找此类别中的所有API
if not matched_api then
for i = 1, #category_candidates do
if match_api(category_candidates[i], ctx) then
matched_api = category_candidates[i]
break
end
end
end
综上所述,Kong的路由匹配主要采用请求的host
、url
和method
三元组来对请求进行分组,从而找到应该匹配的API。第一次匹配可能比较慢,后续存入缓存后,匹配速度就很快了。这种匹配策略有一个缺点,就是无法匹配哪些不仅仅靠host、uri和method区分的API,比如OpenAPI。针对OpenAPI类型的匹配还需要进行定制。