我曾经在前面的文章中系统性的描述了下 Kong 的插件加载机制,这篇我将通过源码解析的方式呈现其数据走向。剔除掉第三方依赖,Kong 的核心代码结构如下:
kong/
├── api/
├── cluster_events/
├── cmd/
├── core/
├── dao/
├── plugins/
├── templates/
├── tools/
├── vendor/
│
├── cache.lua
├── cluster_events.lua
├── conf_loader.lua
├── constants.lua
├── init.lua
├── meta.lua
├── mlcache.lua
└── singletons.lua
执行 kong start
启动 Kong 之后,Kong 会将解析之后的配置文件保存在 $prefix/.kong_env
,同时生成 $prefix/nginx.conf
、$prefix/nginx-kong.conf
供 OpenResty 的使用。需要注意的是,这三个配置文件在每次 Kong 启动后均会被覆盖,所以这里是不能修改自定义配置的。如果需要自定义 OpenResty 配置,需要自己准备配置模板,然后启动的时候调用:kong start -c kong.conf --nginx-conf custom_nginx.template
即可。
在 nginx-kong.conf
里包含了 Kong 的 Lua 代码加载逻辑:
init_by_lua_block {
kong = require 'kong'
kong.init()
}
init_worker_by_lua_block {
kong.init_worker()
}
upstream kong_upstream {
server 0.0.0.1;
balancer_by_lua_block {
kong.balancer()
}
keepalive 60;
}
# ... 省略若干
location / {
rewrite_by_lua_block {
kong.rewrite()
}
access_by_lua_block {
kong.access()
}
header_filter_by_lua_block {
kong.header_filter()
}
body_filter_by_lua_block {
kong.body_filter()
}
log_by_lua_block {
kong.log()
}
}
这里的 kong
就是 kong/init.lua
,是 Kong 的入口模块,基本上是这样定义的:
local Kong = {}
function Kong.init()
-- ... 省略若干
end
function Kong.init_worker()
-- ... 省略若干
end
function Kong.rewrite()
-- ... 省略若干
end
function Kong.access()
-- ... 省略若干
end
function Kong.header_filter()
-- ... 省略若干
end
function Kong.log()
-- ... 省略若干
end
-- ... 省略若干
也就是在 OpenResty 的不同执行阶段,调用其不同的 handler
。下面我将按照其不同执行阶段,逐个解析它的加载过程。
这个阶段主要的工作就是完成 Kong 的初始化。比如:数据层(DAO
)的创建、路由的创建、插件的预加载等等:
-- retrieve kong_config
local conf_path = pl_path.join(ngx.config.prefix(), ".kong_env")
local config = assert(conf_loader(conf_path))
local dao = assert(DAOFactory.new(config)) instantiate long-lived DAO
local ok, err_t = dao:init()
if not ok then
error(tostring(err_t))
end
鉴于这个阶段是 OpenResty 执行最早的阶段,其也要完成 init.lua
中顶层作用域中的 require("kong.core.globalpatches")()
功能。主要就是重写 math.randomseed
,让它优先使用 OpenSSL 生成的种子,如果失败了再用 ngx.now()*1000 + ngx.worker.pid()
替代,主要是为了解决 PRNG 随机数的问题,背景知识可以参考这里正确认识随机数。另外还做了些小 hack,比如:将 init_worker
阶段中的 sleep
替换为阻塞式的 sleep
以解决这个阶段不能 yield 的问题。
插件的预加载也是一项重要的工作,像之后阶段运行的「phase 循环」均依赖这个时候装载完成的插件。需要注意的是,这里预装载的插件是 Kong 所有已安装的插件(也包含自定义的插件),即使没有启用也会被装载。装载插件的动作由 load_plugins()
这个函数完成,其中有一些关键步骤:
-- sort plugins by order of execution
table.sort(sorted_plugins, function(a, b)
local priority_a = a.handler.PRIORITY or 0
local priority_b = b.handler.PRIORITY or 0
return priority_a > priority_b
end)
-- add reports plugin if not disabled
if kong_conf.anonymous_reports then
local reports = require "kong.core.reports"
local db_infos = dao:infos()
reports.add_ping_value("database", kong_conf.database)
reports.add_ping_value("database_version", db_infos.version)
reports.toggle(true)
sorted_plugins[#sorted_plugins+1] = {
name = "reports",
handler = reports,
schema = {},
}
end
会将插件按照优先级排序,之后阶段运行的「phase 循环」将会按照这个顺序来完成。需要注意的是,Kong 默认会添加一个插件用来匿名发送使用统计,如果不想启用,可以在 Kong 配置文件中关闭这个选项:
anonymous_reports = off
完成上述工作之后,Kong 会把他们缓存在 singletons
中。这个 singletons
可以理解为一个容器,用于承载之后各个 worker 中的需要模块,主要是为了解决不同模块之间的数据共享问题。
-- populate singletons
singletons.ip = ip.init(config)
singletons.dns = dns(config)
singletons.loaded_plugins = assert(load_plugins(config, dao))
singletons.dao = dao
singletons.configuration = config
值得一提的是,Kong 路由信息其实是缓存在 core
里的,也就是由 core/handler.lua
来提供。路由的加载由 build_router()
完成:
assert(core.build_router(dao, "init"))
这个函数原型接受两个参数:
DAO
数据层 DAO
对象,用于从数据库中查找 API 信息version
版本号参数,缓存版本信息,主要是为了后续 worker 判断当前路由是否为最新版本。在这个阶段话完成后,Kong 会将路由版本信息置为 init
,可以通过下面这样来确认:
curl -i -s http://localhost:8001/cache/router:version
这个阶段的第一个动作就是设置 worker 的随机种子,利用上个阶段被重写的 math.randomseed()
函数来完成。至于为什么不在上个阶段就完成种子的设置,是因为在 init 阶段还没有开始 fork worker,如果设置了种子,根据 fork 的 COW 特性会导致之后所有的 worker 的种子都是一样的。因此这个动作只能在这个阶段来完成。
接下来便是初始化 Kong 事件,Kong 的事件分为两部分:
worker_events
来处理cluster_events
来处理-- init inter-worker events
local worker_events = require "resty.worker.events"
local ok, err = worker_events.configure {
shm = "kong_process_events", -- defined by "lua_shared_dict"
timeout = 5, -- life time of event data in shm
interval = 1, -- poll interval (seconds)
wait_interval = 0.010, -- wait before retry fetching event data
wait_max = 0.5, -- max wait time before discarding event
}
if not ok then
ngx_log(ngx_CRIT, "could not start inter-worker events: ", err)
return
end
-- init cluster_events
local dao_factory = singletons.dao
local configuration = singletons.configuration
local cluster_events, err = kong_cluster_events.new {
dao = dao_factory,
poll_interval = configuration.db_update_frequency,
poll_offset = configuration.db_update_propagation,
}
if not cluster_events then
ngx_log(ngx_CRIT, "could not create cluster_events: ", err)
return
end
需要注意的是,从这个阶段开始需要执行一些 hook
,定义在 core/handler.lua
大概是这个样子:
init_worker = {
before = function()
-- ... 省略若干
end
},
rewrite = {
before = function(ctx)
ctx.KONG_REWRITE_START = get_now()
end,
after = function (ctx)
ctx.KONG_REWRITE_TIME = get_now() - ctx.KONG_REWRITE_START -- time spent in Kong's rewrite_by_lua
end
},
access = {
before = function(ctx)
-- ... 省略若干
end
},
header_filter = {
before = function(ctx)
-- ... 省略若干
end,
after = function(ctx)
-- ... 省略若干
end
},
-- ... 省略若干
基本就是定义在各个阶段开始前、结束后执行的一些任务,多数是用来统计执行耗时。比如,这个阶段要执行的 core.init_worker.before()
就是用来注册上面的 worker 和 cluster 事件。
接下里就是初始化 Kong 缓存,并将路由版本信息的缓存置为 init
。这里为什么不把路由信息缓存?很简单,无法解决序列化问题,所以只能缓存在 Lua Land 下。
local cache, err = kong_cache.new {
cluster_events = cluster_events,
worker_events = worker_events,
propagation_delay = configuration.db_update_propagation,
ttl = configuration.db_cache_ttl,
neg_ttl = configuration.db_cache_ttl,
resty_lock_opts = {
exptime = 10,
timeout = 5,
},
}
if not cache then
ngx_log(ngx_CRIT, "could not create kong cache: ", err)
return
end
local ok, err = cache:get("router:version", { ttl = 0 }, function()
return "init"
end)
由于这个阶段只会执行一次,所以最后也要执行所有预加载插件的 init_worker()
handler:
for _, plugin in ipairs(singletons.loaded_plugins) do
plugin.handler:init_worker()
end
鉴于这个循环并没有完成任何插件生效策略的筛选,所以我没有把它归为「phase 循环」。
这个阶段的逻辑比较简单,就是在开始前、和结束后分别执行两个 hook
将 Kong 处理耗时注入到 ctx
中:
local ctx = ngx.ctx
core.rewrite.before(ctx)
-- we're just using the iterator, as in this rewrite phase no consumer nor
-- api will have been identified, hence we'll just be executing the global
-- plugins
for plugin, plugin_conf in plugins_iterator(singletons.loaded_plugins, true) do
plugin.handler:rewrite(plugin_conf)
end
core.rewrite.after(ctx)
中间完成的那个遍历,就是上面提到过多次的「phase 循环」,它的工作就是完成插件生效策略的筛选,并执行对应的 handler
。不过这个阶段的筛选,只能筛选出启用的全局插件(因为路由匹配尚未开始,无法确定 API、Consumer)。
筛选的工作由 plugins_iterator
这个迭代器完成,定义在 core/plugins_iterator.lua
,其函数原型是:
local function iter_plugins_for_req(loaded_plugins, access_or_cert_ctx)
local ctx = ngx.ctx
if not ctx.plugins_for_request then
ctx.plugins_for_request = {}
end
local plugin_iter_state = {
i = 0,
ctx = ctx,
api = ctx.api,
loaded_plugins = loaded_plugins,
access_or_cert_ctx = access_or_cert_ctx,
}
return setmetatable(plugin_iter_state, plugin_iter_mt)
end
接收两个参数:
loaded_plugins
前面预加载的所有插件,用于按优先级大小遍历插件。access_or_cert_ctx
根据参数命名也可以看出是标识当前阶段是否属于 access 或者是 ssl_certificate 阶段,以判断是否需要运行插件生效策略。不过 ssl_certificate 阶段未必都用,所以这里在 rewrite 阶段用来提前加载全局插件。其实这个函数真正起作用的是 get_next
这个 metamethod,是个递归函数:
local function get_next(self)
-- load the plugin configuration in early phases
if self.access_or_cert_ctx then
-- ... 省略若干
ctx.plugins_for_request[plugin.name] = plugin_configuration
end
-- return the plugin configuration
local plugins_for_request = ctx.plugins_for_request
if plugins_for_request[plugin.name] then
return plugin, plugins_for_request[plugin.name]
end
return get_next(self) -- Load next plugin
end
可以看到这个阶段加载完成的全局插件会缓存在 ctx.plugins_for_request
,这个对象用来保存当前请求启用的插件(包括下个阶段的局部插件)。
值得一提的是,纵观请求的整个处理过程,plugins_iterator
会被调用多次以完成整个插件生效策略的筛选。这个阶段的筛选只是第一个阶段,甚至都可以理解为不算筛选,而仅仅是为了加快全局插件的加载罢了。
事实上目前默认 Kong 自带的插件均没有需要这个阶段运行的
handler
。