这个阶段就比较重要了,首先要执行的就是 core.access.before(ctx)
这个 hook
,主要是完成路由的匹配。不过匹配前需要判断当前路由是否是最新版本,否则的话需要重建路由:
local version, err = singletons.cache:get("router:version", {
ttl = 0
}, utils.uuid)
if err then
log(ngx.CRIT, "could not ensure router is up to date: ", err)
elseif router_version ~= version then
-- router needs to be rebuilt in this worker
log(DEBUG, "rebuilding router")
local ok, err = build_router(singletons.dao, version)
if not ok then
router_err = err
log(ngx.CRIT, "could not rebuild router: ", err)
end
end
接下来就是运行「phase 循环」了,来彻底完成插件生效策略的筛选(因为这个时候已经完成了路由查找,之后通过 API 可以找到 auth 插件,进而确定 Consumer,这也是为什么 auth 插件的优先级普遍比较高的原因)。插件的筛选还是由 get_next
来完成,关键代码如下:
if consumer then
-- ... 省略若干
if api then
plugin_configuration = load_plugin_configuration(api.id, consumer_id, plugin.name)
end
if not plugin_configuration then
plugin_configuration = load_plugin_configuration(nil, consumer_id, plugin.name)
end
end
if not plugin_configuration then
-- Search API specific, or global
if api then
plugin_configuration = load_plugin_configuration(api.id, nil, plugin.name)
end
if not plugin_configuration then
plugin_configuration = load_plugin_configuration(nil, nil, plugin.name)
end
end
ctx.plugins_for_request[plugin.name] = plugin_configuration
因此,最终的生效策略优先级就是:api & consumer
> consumer
> api
> global
需要注意的是,这个过程可能会覆盖上个阶段的全局插件,这正是插件生效策略的作用;同时该过程结束之后,当前请求需要启用的插件就已经最终确定,并被缓存在 ctx.plugins_for_request
中,直至该请求生命周期的结束。至于之后阶段运行的「phase 循环」其实就是直接读取的 ctx.plugins_for_request
而已。
另外在这个阶段有个非常巧妙的设计,就是 ctx.delay_response
这个参数。它的原理就是把要执行的 handler
wrap 在一个 coroutine
中,如果执行到一个插件需要 ngx.say
来提前执行 Nginx 的 content handler,那么它就会 yield
当前 coroutine
,来延迟 content handler 的执行,并跳过之后需要执行的所有插件。这么做主要基于两点:
ctx.delay_response = true
for plugin, plugin_conf in plugins_iterator(singletons.loaded_plugins, true) do
if not ctx.delayed_response then
local err = coroutine.wrap(plugin.handler.access)(plugin.handler, plugin_conf)
if err then
ctx.delay_response = false
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
end
end
end
if ctx.delayed_response then
return responses.flush_delayed_response(ctx)
end
但是目前 Kong 在实现这块的时候也是有缺陷的,就是插件执行过程中如果 ngx.say
被触发,虽然将不会执行接下来的插件,但是依然在运行一个 hot 的迭代。这里其实完全可以避免,就像下面这样:
if not ctx.delayed_response then
-- ... 省略若干
else
break
end
执行完插件的 access()
handler 之后,就通过 flush_delayed_response
将延迟发送(如果需要的话)的 content 响应给客户端:
if ctx.delayed_response then
return responses.flush_delayed_response(ctx)
end
如果通过了插件的 access()
handler,却没有触发 content handler。那么接下来就是执行 core.access.after
这个 hook 了。其中的关键步骤:
local ok, err, errcode = balancer.execute(ctx.balancer_address)
if not ok then
if errcode == 500 then
err = "failed the initial dns/balancer resolve for '" ..
ctx.balancer_address.host .. "' with: " ..
tostring(err)
end
return responses.send(errcode, err)
end
主要就是获取 upstream
,并完成相关 DNS 的解析,其实这个事儿更应该是由 balancer
阶段来完成。只不过由于 balancer
不允许被 yield
,因此就放在了这里。最后请求将会被 proxy_pass
到 kong_upstream,正式进入到 balancer
阶段。
这个阶段不会运行任何插件,当然也不会有「phase 循环」。
这个阶段的主要工作其实就是完成重试逻辑,因为在上个阶段的 core.access.before
hook 已经完成了第一次节点的选取,这个阶段只是简单做了下 ngx_balancer.set_current_peer
和 ngx_balancer.set_more_tries
。
重试依然使用的是 balancer.execute
,关键步骤为:
if addr.try_count > 1 then
-- only call balancer on retry, first one is done in `core.access.after` which runs
-- in the ACCESS context and hence has less limitations than this BALANCER context
-- where the retries are executed
-- record failure data
local previous_try = tries[addr.try_count - 1]
previous_try.state, previous_try.code = get_last_failure()
-- Report HTTP status for health checks
if addr.balancer then
if previous_try.state == "failed" then
addr.balancer.report_tcp_failure(addr.ip, addr.port)
else
addr.balancer.report_http_status(addr.ip, addr.port, previous_try.code)
end
end
local ok, err, errcode = balancer_execute(addr)
if not ok then
ngx_log(ngx_ERR, "failed to retry the dns/balancer resolver for ",
tostring(addr.host), "' with: ", tostring(err))
return ngx.exit(errcode)
end
else
-- first try, so set the max number of retries
local retries = addr.retries
if retries > 0 then
set_more_tries(retries)
end
end
值得一提的是 Kong 使用的 Ring-balancer 是自己实现的 lua-resty-dns-client,target 的选取默认使用的是 round-robin 算法,当 upstream
开启了 hash 时,则会换为一致性 hash。
这个阶段将会接着执行「phase 循环」,只不过经过了 access 阶段之后,当前请求应该被执行的插件已经确定,并被缓存在自身中。这个阶段只是在遍历所有插件时将直接从上面的缓存中查找,并执行相应的 header_filter 方法,而不再经过生效策略的筛选,这当然也是出于性能上的考量。
local ctx = ngx.ctx
core.header_filter.before(ctx)
for plugin, plugin_conf in plugins_iterator(singletons.loaded_plugins) do
plugin.handler:header_filter(plugin_conf)
end
core.header_filter.after(ctx)
最后执行的 core.header_filter.after
hook,用于将 Kong 的处理时间注入到 header 中:
if singletons.configuration.latency_tokens then
-- balancer 阶段执行时间
header[constants.HEADERS.UPSTREAM_LATENCY] = ctx.KONG_WAITING_TIME
-- access 阶段执行时间
header[constants.HEADERS.PROXY_LATENCY] = ctx.KONG_PROXY_LATENCY
end
这个阶段其实和上个阶段差不多,只不过是在「phase 循环」中执行的 handler 为 body_filter
而已。这里就不再赘述了。
local ctx = ngx.ctx
for plugin, plugin_conf in plugins_iterator(singletons.loaded_plugins) do
plugin.handler:body_filter(plugin_conf)
end
core.body_filter.after(ctx)
log 阶段依旧和上面的 filter 阶段差别不大:
local ctx = ngx.ctx
for plugin, plugin_conf in plugins_iterator(singletons.loaded_plugins) do
plugin.handler:log(plugin_conf)
end
core.log.after(ctx)
不过是在 core.log.after
hook 中需要更新 balancer 的被动健康检查状况而已。
Kong 通过其插件扩展机制,提供了超越核心平台的额外功能和服务。同时由于插件的启用是基于每请求的,会随着生命周期的结束而被销毁。其被诟病的内存碎片问题,我猜测多少也和这一点的设计有些关系。