专栏首页posluaKong 插件加载机制源码解析(下)

Kong 插件加载机制源码解析(下)

4. access

这个阶段就比较重要了,首先要执行的就是 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 的执行,并跳过之后需要执行的所有插件。这么做主要基于两点:

  • 如果请求被插件拦截就尽快退出「phase」循环。
  • 可以在输出 content 前,做一些自定义的操作。
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 阶段。

5. balancer

这个阶段不会运行任何插件,当然也不会有「phase 循环」。

这个阶段的主要工作其实就是完成重试逻辑,因为在上个阶段的 core.access.before hook 已经完成了第一次节点的选取,这个阶段只是简单做了下 ngx_balancer.set_current_peerngx_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。

6. header_filter

这个阶段将会接着执行「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

7. body_filter

这个阶段其实和上个阶段差不多,只不过是在「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)

8. log

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 通过其插件扩展机制,提供了超越核心平台的额外功能和服务。同时由于插件的启用是基于每请求的,会随着生命周期的结束而被销毁。其被诟病的内存碎片问题,我猜测多少也和这一点的设计有些关系。

本文分享自微信公众号 - poslua(poslua)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-05-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 老司机 iOS 周报 #77 | 2019-07-29

    你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Is...

    用户2932962
  • Chrome方便快捷的插件

    一直都对Chrome情有独钟,第一次接触她只因她的icon就喜欢上她了,后来便一发不可自拔,现在几乎成为程序员最喜欢用的浏览器,至于优点很明显:强大的Devel...

    php007
  • iOS GPS 准确度哪些事

    今天遇到这么一个问题,Android App使用百度地图定位上传服务器的时候,发现有几段不同时间但是经纬相同的情况,Android调查是因为有的手机GPS定位关...

    赵哥窟
  • Android Jetpack - ViewModel

    ViewModel 旨在以生命周期感知的形式存储和管理 UI 控制器(Activity/Fragment 等)相关的数据,可以解决 UI 控制器中数据无法正确保...

    SkyRiN
  • AdGuard Premium v7.1.2872 广告拦截器中文版

    AdGuard 是摆脱恼人广告,在线跟踪,保护您远离恶意软件的最佳方式。AdGuard 使您网络冲浪更快速,更安全,更安逸!AdGuard for Window...

    萌海无涯
  • “过时”的SpringMVC到底在用什么?深入分析DispatchServlet源码

    本文先简述下目前SpringMVC的使用情况,然后通过Demo的简单让大家有一个初步的使用印象,然后带着印象去看其中执行的分发源码。

    Zack说码
  • WebFlux定点推送、全推送灵活websocket运用

            WebFlux 本身提供了对 WebSocket 协议的支持,处理 WebSocket 请求需要对应的 handler 实现 WebSocket...

    边鹏 [进阶者]
  • 《Android FFmpeg 播放器开发梳理》第零章 基础公共类的封装

    在开始介绍播放器开发之前,我们首先对posix库进行一定的封装,得到我们想要的 Mutex、Condition、Thread等类。

    glumes
  • 爬取TOP100的电影

    最近在学习requests库和正则表达式,今天就利用这两个知识点来抓取猫眼电影TOP100的相关内容。

    stormwen
  • 移动设备(手机)的唯一ID详解

    平台支持:Android - 2.2+ (支持): 与设备的imei号一致。注意:如果无法获取设备imei则使用设备wifi的mac地址,如果无法获取设备mac...

    php007

扫码关注云+社区

领取腾讯云代金券