CatCoding

Kong 源码分析:启动

2017-07-07

Kong 的初始化过程

安装好 Kong 之后我们是用命令sudo ./bin/kong start -c kong.conf -vv来启动。其中 kong.conf 为配置文件,-vv选项打印出一些重要信息展示出来,方便发现问题。

可以看到./bin/kong是一个脚本,是用的#!/usr/local/openresty/bin/resty程序来执行,而 resty 是 OpenResty 的一个 Perl 可执行脚本。kong 的内容很简单,就是一个入口函数调用:
require("kong.cmd.init")(arg)

所以我们可以从 cmd/init.lua 这个文件开始入手看启动过程。一翻开 init.lua 这个文件,发现其实不过是个 wrapper,解析了 args 之后就是调用 start,stop,quit 等命令。然后我们顺藤摸瓜找 cmd/start.lua 文件,整个启动过程就在这里了:

local conf = assert(conf_loader(args.conf, {
  prefix = args.prefix
}))

local err
local dao = assert(DAOFactory.new(conf))
xpcall(function()
  assert(prefix_handler.prepare_prefix(conf, args.nginx_conf))
  assert(dao:run_migrations())
  assert(serf_signals.start(conf, dao))
  assert(nginx_signals.start(conf))
  log("Kong started")
end, function(e)
  err = e -- cannot throw from this function
end)  

从代码上来看很直观,首先 conf_loader 载入配置文件,DAOFactory 构建数据库连接层,prefix_handler.prepare_prefix 是准备一些由程序生成的配置文件。dao:run_migrations 是迁移表结构到数据库,类似其他 Web 框架。serf_signals 是启动 serf 程序,nginx_signals 是启动 nginx 进程。

读取配置文件 conf_loader

conf_loader 读取的当然是命令行里面传入的 kong.conf 文件,打开 conf_loader.lua 看了看,是是用一个 lua 第三方库来做文件解析的。local pl_config = require "pl.config",最开始不太知道这个 pl 是什么,经过搜索后才知道是这里定义的,在 kong.rockspec 里面有定义了该库的依赖"penlight == 1.4.1"。读取配置的整个过程比较琐碎,最后回构建一个解析好的 conf 表。这里学到了 Lua 里面的 setmetatable 设置元表的方法,table 作为 Lua 里面的最基本数据结构,setmetatable 可以方便的绑定一个 key 和其对应的方法。看起来也像是面向对象的风格,在 conf_loader 的最后部分是:

return setmetatable({
  load = load,
  add_default_path = function(path)
    DEFAULT_PATHS[#DEFAULT_PATHS+1] = path
  end,
  ......
}, {
  __call = function(_, ...)
    return load(...)
  end
})

这样其他地方调用的时候local conf, err, errors = conf_loader(args.conf)自然就把 args.conf 传入 load,返回解析后的结果。

prepare_prefix 动态生成 Nginx 和 serf 的配置

prefix_handler.lua 这个文件主要在准备一些 Nginx 的配置文件和 serf 的配置文件。prepare_prefix 函数前半部分在创建各个子目录,logs、serf、pids、以及各个日志文件。关于 Kong 的 config 部分需要参考一下这里。这个函数比较长,重要的部分是生成 Nginx 的配置文件。
可以看到 compile_kong_conf 函数其实是是用 kong/templates 目录下的 nginx_kong.lua 和 nginx.lua 分别生成两个文件,其中 nginx_kong.lua 里面包含了嵌入 Kong 的 Lua 代码的逻辑。

init_by_lua_block {
    require 'luarocks.loader'
    require 'resty.core'
    kong = require 'kong'
    kong.init()
}

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 每次 reload 或者启动的时候会生成新的 Nginx 配置文件,所以我们如果要加入自己的配置可以直接修改 nginx_kong.lua 文件。另外我在使用的时候发现一个小问题,Kong 把 serf 的 node_id 存在一个文件里,如果我们把之前跑过 Kong 的机器做了镜像,然后再启动一个新的实例时,这个 node_id 文件既然存在则没有重新生成,最终导致两台 kong 实例并没有相互通信形成一个集群。我认为这里其实可以再检查一下 node_id 的文件和本机的 ip 是否一致,如果不一致则重新生成。

dao:run_migrations()

初始化过程的下一步则是执行数据库操作,Kong 目前只支持 cassandra 和 Postgres,个人认为应该增加 Redis 的支持。

serf_signals.start

之前提到过 serf 是用来保证 kong instance 之间的通信的,启动的时候的一个很重要参数是--event-handler,参数的内容是一个可执行脚本 (通常叫做 serf_event.sh),文件的内容是前面生成配置文件的时候写入的。默认情况下 serf 会监听在 7946 端口,如果多台 server 需要形成一个集群,这个端口之间需要能相互通信。这里就有一个问题,在一个新的 server 刚启动的时候,该 server 是如何发现其他节点的呢?我们可以看到 serf_signals.lua 里的 start 函数调用了 serf:autojoin() 函数,跟踪到 autojoin 里面看代码,其实是从数据库里读取出其他 nodes 的信息,然后依次告诉对方新同志加入了,然后把自己的节点信息写入到数据库里。自然如果要退出也需要把自己的信息从数据库里删掉。

nginx_signals.start

启动的最后一步即是 Nginx 的启动,其实最终执行的命令就是:

/usr/local/openresty/nginx/sbin/nginx -p /usr/local/kong -c nginx.conf

总结

通过上面的分析,可以总结 Kong 的启动过程即是:解析输入参数,验证参数合法性并生成必要的目录和配置文件,执行数据库操作,启动 serf,启动 Nginx。最终其实就是一个 OpenResty 启动过程,嵌入 Kong 里面的 core 部分的 Lua 代码。后面继续分析其可扩展的插件机制。

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