首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Kong 插件加载机制源码解析(上)

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

作者头像
poslua
发布2019-08-19 14:50:38
2.9K1
发布2019-08-19 14:50:38
举报
文章被收录于专栏:posluaposlua

前言

我曾经在前面的文章中系统性的描述了下 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。下面我将按照其不同执行阶段,逐个解析它的加载过程。

1. init

这个阶段主要的工作就是完成 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

2. init_worker

这个阶段的第一个动作就是设置 worker 的随机种子,利用上个阶段被重写的 math.randomseed() 函数来完成。至于为什么不在上个阶段就完成种子的设置,是因为在 init 阶段还没有开始 fork worker,如果设置了种子,根据 fork 的 COW 特性会导致之后所有的 worker 的种子都是一样的。因此这个动作只能在这个阶段来完成。

接下来便是初始化 Kong 事件,Kong 的事件分为两部分:

  • worker 之间的事件,由 worker_events 来处理
  • cluster 节点之间的事件,由 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 循环」。

3. rewrite

这个阶段的逻辑比较简单,就是在开始前、和结束后分别执行两个 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

To be continued…

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-05-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 poslua 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • 1. init
      • 2. init_worker
        • 3. rewrite
        • To be continued…
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档