CatCoding

Kong 源码分析:插件

2017-07-16

插件的强大之处

在我自己使用 Kong 的过程中,最方便的还是在于 Kong 的强大的插件机制。Nginx 本身提供了提供模块开发机制,但是相对来说更底层一些,并且需要使用 C/C++ 来开发,对于很多开发人员来说 Nginx 仍为一个黑盒。OpenResty 集成了很多好用插件,并提供了通过 Lua 扩展 Nginx 的机制,所以 OpenResty 相对来说更灵活。而 Kong 在 OpenResty 基础上提供的插件机制更灵活,在于:

​ 复用:OpenResty 的复用在于函数级别,我们可以把一些通用的 Lua 函数引入各个项目。而 Kong 的插件复用可以通过 API 修改一下配置即可。是否启用某个插件,这只是数据配置问题,启用与否不会涉及到代码的改动。

抽象、统一:Kong 实现了基础的插件配置的存储和更新机制,所以我们只需按照要求定义插件配置的数据类型,插件实现的时候不用再去关心这些细节。

灵活、组合:OpenResty 的一些处理部分有限制,比如 access_by_lua 在同一个 location 能调用一次,当然我们可以把多个处理逻辑都放在这里,这又涉及到代码改动。而 Kong 可以依次调用各个插件对应的 phase,并且通过引入优先级来解决前后顺序问题。

插件开发的原则是提供机制,而非实现,在做插件开发的时候一定需要考虑这个插件能否满足一类相似的需求,这样我们只需要做一下参数的配置就能把插件启动在另外一个站点上。

对于插件这块我的疑问在于这套机制如何运行的?如何找到站点对应的插件?如此多的插件是否会有性能问题?​

Kong 插件的运行机制

在上一文 Kong 初始化分析中,我们看到 nginx_kong.lua 模板文件里面有这么一段代码:

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.lua 文件里面,kong.access 的实现是这样的:

function Kong.access()
  core.access.before()

  for plugin, plugin_conf in 
      plugins_iterator(singletons.loaded_plugins, true) do
    plugin.handler:access(plugin_conf)
  end

  core.access.after()
end

从这里可以看出 Kong 的插件运行机制就是从 loaded_plugins 里面依次执行。学习 Kong 插件开发的方法是参考现有的一些插件实现,学着写几个就会了。用户自己定义的插件是在 base_plugin 基类上继承而来。Kong 里面使用的了 这套 class 机制,可以看到使用 Lua 实现面向对象还是很简单的。

singletons.loaded_plugins

singletons.loaded_plugins在这里初始化的,在具体实现过程中就是从数据库里面把插件配置读出,

local ok, handler = utils.load_module_if_exists("kong.plugins." .. plugin .. ".handler")

在每一个插件在 handler.lua 的最后都是 return XXXXHandler,所以在调用 handler()后我们在内存中导入了插件的对象。另外在初始化后需要按照优先级来排序,以此来保证各个插件之间的执行顺序。

从上面的分析上看出,插件导入后都会在内存中的全局对象中存储,后面的开销在于依次迭代插件。

plugins_iterator

我们再来看看某个站点是否启用某个插件是如何处理的,最主要的实现在于 plugins_iterator 这个函数。首先我们得理解如何确定当前 request 对应的唯一标识符,在 core.handler.access 的过程中保存了经过路由后的 api 在 ngx.ctx 里,这个 ngx.ctx 会在整个 request 处理过程中反复被使用。再回到 plugins_interator 函数,这个函数的参数有两个,后一个叫 access_or_cert_ctx,因为对于一个 request 处理中 plugins_iterator 会调用多次,这个参数的作用在于判断是否是第一个调用这个函数。第一次调用可能发生在ssl_certificate或者access 阶段,因为在 ctx 里面 Kong 还是初始化了一个叫做ctx.plugins_for_request的变量来存储当前 request 启用的插件,这样后续 iterator 阶段就完全不会去重复 load 插件配置,这样做当然是为了性能上的考虑。

读取插件配置的函数调用是:

if api then
   plugin_configuration = load_plugin_configuration(api.id, consumer_id, plugin.name)
end

load_plugin_configuration也会首先尝试从内存缓存中取,如果取不到再从数据库中取出,然后存储在缓存中。

从上面的分析看出,插件相关的读取和执行在大部分时间里是完全不会去读数据库的,所以性能损失并不会大。

错误处理

Kong 的插件部分并没有错误处理部分,从现有代码上看错误处理分两个部分:

一种方式是responses.lua,如果是在 Kong 的 Lua 代码部分检查出来的错误一般使用类似responses.send(500)这样的方式来向客户端返回错误码。

第二种是通过 kong_error_handler。这种错误可能是执行了 ngx.exit(500) 之类的代码或者是 Nginx 内部触发的。

这在某些情况下对用户不友好,我们不能只简单地返回一个错误信息,有的时候我们需要展示一个漂亮些的错误页面或者是把请求转到别的降级站点,对于这个需求我做了一个分支来扩展错误处理。目前实现还未完整,不过已经可以定制化错误页面了。这里增加了一个 ngx.var.api_id,这个变量的初始化也在 core.access 阶段。因为存储在 ngx.ctx 里的这些信息在执行了 ngx.exit 之后已经释放了,所以我需要一个 ngx.var 级别的变量存储 api_id,然后使用这个变量来判断 error-handler 插件是否启用。

公号同步更新,欢迎关注👻
Tags: Lua