从 `kong start` 到处理流量:启动序列全解析
前置知识
- ›第 1 篇:架构与 Nginx 集成(理解 phase 模型)
- ›熟悉 Lua 模块系统与元表(metatable)
- ›了解进程管理基础(master/worker 模型)
从 kong start 到处理流量:启动序列全解析
第 1 篇已经确认,Kong 运行在 Nginx 内部。那么,它究竟是如何进入 Nginx 的?答案涉及一条相当长的流水线:shell 脚本 → resty CLI → Lua 命令分发 → 配置加载 → 模板渲染 → Nginx 启动 → Lua phase 初始化。每个阶段都依赖上一阶段的输出,任何一步出错都会终止整个启动过程。
本文将端到端地追踪这条流水线,从你在终端输入 kong start 的那一刻,一直到第一个请求可以被正常处理。
CLI 分发:从 Shell 到 Lua
入口是 bin/kong,这是一个通过 resty CLI(OpenResty 的命令行工具)执行的 Lua 脚本。文件头部的 shebang #!/usr/bin/env resty 表明它运行在一个由 resty 管理的临时 Nginx 进程中。
脚本从 arg[1] 中解析子命令(start、stop、reload、migrations 等),并在第 19–35 行的硬编码命令表中进行校验。随后,它构造一段内联 Lua 字符串并通过 resty 执行:
local inline_code = string.format([[
setmetatable(_G, nil)
package.path = (os.getenv("KONG_LUA_PATH_OVERRIDE") or "") .. "./?.lua;./?/init.lua;" .. package.path
require("kong.cmd.init")("%s", %s)
]], cmd_name, args_str)
这段内联代码(第 135–141 行)会连同注入的 Nginx 配置指令一起传递给一个新的 resty 进程。之所以要绕这一圈,是因为某些命令即便在 CLI 执行阶段也需要特定的 Nginx 指令(如 lmdb_* 或 lua_ssl_*)。
sequenceDiagram
participant Shell
participant bin/kong as bin/kong (resty)
participant cmd/init as kong.cmd.init
participant cmd/start as kong.cmd.start
Shell->>bin/kong: kong start -c kong.conf
bin/kong->>bin/kong: parse args, validate "start"
bin/kong->>bin/kong: inject_confs.compile_confs()
bin/kong->>Shell: resty -e 'require("kong.cmd.init")("start", {...})'
Shell->>cmd/init: dispatch("start", args)
cmd/init->>cmd/start: require("kong.cmd.start")
cmd/start->>cmd/start: execute(args)
kong/cmd/init.lua 中的分发器非常精简——它 require 对应的命令模块并调用其 execute 函数:
return function(cmd_name, args)
local cmd = require("kong.cmd." .. cmd_name)
-- ... xpcall(function() cmd_exec(args) end, ...)
end
配置加载流水线
kong/cmd/start.lua 中 start 命令的 execute 函数,第一步就是加载配置:
local conf = assert(conf_loader(args.conf, {
prefix = args.prefix
}, { starting = true }))
kong/conf_loader/init.lua 中的 conf_loader 实现了一套多阶段合并流水线:
flowchart TD
A["kong_defaults.lua<br>(hardcoded defaults)"] --> E[Merged Config]
B["kong.conf file<br>(user overrides)"] --> E
C["KONG_* env vars<br>(highest precedence)"] --> E
D["custom_conf table<br>(programmatic overrides)"] --> E
E --> F["check_and_parse()<br>(validation & type coercion)"]
F --> G["aliased_properties()<br>(backward compat)"]
G --> H["deprecated_properties()<br>(warnings)"]
H --> I["dynamic_properties()<br>(Nginx directive injection)"]
I --> J["process_secrets.resolve()<br>(vault/secret resolution)"]
J --> K["Frozen immutable<br>configuration table"]
默认值来自 kong/templates/kong_defaults.lua,这是一个模拟 INI 格式的 Lua 字符串,解析后构成配置的基础层。
接下来,kong.conf 中的用户配置会覆盖默认值,环境变量则拥有最高优先级。按照 KONG_ 前缀约定,KONG_DATABASE=off 会覆盖 database 属性。第 265–280 行的三层合并逻辑还负责处理动态 Nginx 指令——nginx_http_lua_shared_dict 这类属性会被解析为结构化的指令表。
kong.conf_loader.parse 中的 check_and_parse 函数会依据 kong/conf_loader/constants.lua 中的类型定义对所有属性进行校验,并将字符串转换为布尔值、数字和数组等对应类型。若有非法值,会给出清晰的错误提示。
提示: 排查配置问题时,可以设置
KONG_LOG_LEVEL=debug,然后留意日志中reading config file at的输出。Kong 会记录配置流水线每一步的详细信息,包括搜索过的默认路径。
模板渲染与 Nginx 启动
拿到经过校验的配置之后,start.lua 会准备 prefix 目录并启动 Nginx。prefix 目录(默认为 /usr/local/kong)用于存放渲染后的 nginx.conf、PID 文件、日志文件和 Unix socket。
第 59 行的调用:
assert(prefix_handler.prepare_prefix(conf, args.nginx_conf, nil, nil,
args.nginx_conf_flags))
这里会渲染 kong/templates/nginx_kong.lua 模板——一个使用 ${{VARIABLE}} 插值语法和 > if condition then 控制流来生成最终 nginx.conf 的 Lua 字符串。模板中包含根据角色(traditional/CP/DP)、已启用的监听器、SSL 配置等条件动态生成的片段。
模板渲染完成后,第 99 行通过 nginx_signals.start(conf) 启动 Nginx。Nginx master 进程随即 fork 出 worker 进程,每个 worker 开始执行第 1 篇中介绍的 Lua phase hook。
sequenceDiagram
participant start.lua
participant prefix_handler
participant nginx_signals
participant NginxMaster as Nginx Master
participant Worker as Nginx Workers
start.lua->>start.lua: conf_loader(kong.conf)
start.lua->>prefix_handler: prepare_prefix(conf)
prefix_handler->>prefix_handler: render nginx_kong.lua template
prefix_handler->>prefix_handler: write nginx.conf to prefix/
start.lua->>nginx_signals: start(conf)
nginx_signals->>NginxMaster: exec("nginx -p prefix/")
NginxMaster->>NginxMaster: init_by_lua_block → Kong.init()
NginxMaster->>Worker: fork workers
Worker->>Worker: init_worker_by_lua_block → Kong.init_worker()
init 阶段:Kong.init()
init_by_lua_block 指令会在 Nginx master 进程 fork worker 之前执行 Kong.init()。这意味着这里完成的工作可以通过写时复制(copy-on-write)内存在所有 worker 之间共享。
Kong.init() 是一个约 175 行的函数,按顺序执行以下步骤:
- 加载配置——从 prefix 准备阶段写入的
.kong_env文件中读取配置(第 648 行) - 初始化 PDK——通过
kong_global.init_pdk(kong, config)完成(第 665 行) - 创建数据库连接器——通过
DB.new(config)创建并建立连接(第 669–693 行) - 检查迁移状态——若使用 Postgres,验证 schema 是否已是最新版本(第 674–691 行)
- 初始化集群——若以 CP 或 DP 角色运行,实例化集群模块,并可选地启动 RPC 同步系统(第 701–713 行)
- 加载插件 schema——通过
db.plugins:load_plugin_schemas(config.loaded_plugins)完成(第 718 行) - 构建 router 和插件迭代器(第 751–763 行)——或在 DB-less 模式下解析声明式配置(第 724–745 行)
角色检测逻辑定义在第 201–218 行,非常直观:
is_data_plane = function(config) return config.role == "data_plane" end
is_control_plane = function(config) return config.role == "control_plane" end
is_dbless = function(config) return config.database == "off" end
这些标志决定了初始化走哪条路径。Control Plane 会跳过 router 构建(因为它不负责代理流量);DB-less 模式下的 Data Plane 则从 YAML 文件解析声明式配置,而不是连接 Postgres。
flowchart TD
A[Kong.init] --> B[Load config from .kong_env]
B --> C[Init PDK]
C --> D[Create DB connector]
D --> E{DB-less?}
E -->|Yes| F[Parse declarative config]
E -->|No| G[Check migrations]
G --> H[Connect to DB]
F --> I{CP or DP?}
H --> I
I -->|CP/DP| J[Init clustering module]
I -->|Traditional| K[Skip clustering]
J --> L[Load plugin schemas]
K --> L
L --> M[Build router + plugins iterator]
M --> N[Close DB connection]
init_worker 阶段:Kong.init_worker()
master fork 出 worker 之后,每个 worker 会独立运行 Kong.init_worker()。与 init() 不同,这段代码在每个 worker 进程中单独执行,可以使用 Nginx 的 timer 和事件 API。
第 813–1024 行的函数负责以下工作:
- 启动 timer 系统——将
lua-resty-timer-ng库挂载到kong.timer(第 828–832 行) - 初始化 DB worker——调用
kong.db:init_worker()(第 836 行) - worker 事件——通过 Unix socket 实现 worker 间通信(第 856 行)
- 集群事件——在 Postgres 模式下用于节点间缓存失效通知(第 864 行)
- 缓存初始化——使用共享内存字典分别创建
kong.cache(插件数据)和kong.core_cache(路由数据)(第 872–886 行) - 加载声明式配置——在 DB-less 模式下,将
init()阶段解析的配置写入 LMDB(第 912–959 行) - 缓存预热——预先填充高频访问实体的缓存(第 964 行)
- 重建 router 和插件迭代器——确保每个 worker 持有最新的 router(第 970–982 行)
- 调用插件的 init_worker handler——逐一执行每个插件的
init_worker方法(第 987–995 行) - RPC 与同步初始化——用于支持增量同步的混合模式(第 1001–1013 行)
router 和插件重建的最终一致性模型是其中的关键设计。在 traditional(Postgres)模式下,后台 timer 会定期检查配置是否发生变更,并在必要时重建 router。这一逻辑位于 runloop handler 的 init_worker.before() 中,对应第 925–1030 行。
提示:
stash_init_worker_error函数(第 168–183 行)是 Kong 的安全兜底机制。一旦init_worker的某个步骤失败,错误会被暂存下来,并在后续每次请求时以 ALERT 级别写入日志。节点会继续运行,但会持续提醒运维人员需要重启。
整体回顾
启动序列横跨两个进程和多个 Lua 模块边界,但整体设计思路清晰:每个阶段的输出都是下一阶段的输入。
| 阶段 | 进程 | 关键产出 |
|---|---|---|
| CLI 分发 | resty(临时 Nginx) |
解析后的参数、内联 Lua 代码 |
| 配置加载 | resty(临时 Nginx) |
经过校验的不可变配置表 |
| 模板渲染 | resty(临时 Nginx) |
prefix 目录中的 nginx.conf |
| Nginx 启动 | Nginx master | 运行中的 master 进程 |
Kong.init() |
Nginx master(fork 前) | DB 连接就绪、schema 加载完成、router 构建完毕 |
Kong.init_worker() |
每个 Nginx worker | timer、缓存、事件、插件全部初始化完毕 |
第 3 篇将跟踪一个请求在 runloop 中的完整旅程——从 rewrite 到 log——看看初始化阶段搭建的这套基础设施是如何真正处理流量的。