Read OSS

从 `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] 中解析子命令(startstopreloadmigrations 等),并在第 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.luastart 命令的 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 行的函数,按顺序执行以下步骤:

  1. 加载配置——从 prefix 准备阶段写入的 .kong_env 文件中读取配置(第 648 行)
  2. 初始化 PDK——通过 kong_global.init_pdk(kong, config) 完成(第 665 行)
  3. 创建数据库连接器——通过 DB.new(config) 创建并建立连接(第 669–693 行)
  4. 检查迁移状态——若使用 Postgres,验证 schema 是否已是最新版本(第 674–691 行)
  5. 初始化集群——若以 CP 或 DP 角色运行,实例化集群模块,并可选地启动 RPC 同步系统(第 701–713 行
  6. 加载插件 schema——通过 db.plugins:load_plugin_schemas(config.loaded_plugins) 完成(第 718 行)
  7. 构建 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 行的函数负责以下工作:

  1. 启动 timer 系统——将 lua-resty-timer-ng 库挂载到 kong.timer(第 828–832 行)
  2. 初始化 DB worker——调用 kong.db:init_worker()(第 836 行)
  3. worker 事件——通过 Unix socket 实现 worker 间通信(第 856 行)
  4. 集群事件——在 Postgres 模式下用于节点间缓存失效通知(第 864 行)
  5. 缓存初始化——使用共享内存字典分别创建 kong.cache(插件数据)和 kong.core_cache(路由数据)(第 872–886 行)
  6. 加载声明式配置——在 DB-less 模式下,将 init() 阶段解析的配置写入 LMDB(第 912–959 行)
  7. 缓存预热——预先填充高频访问实体的缓存(第 964 行)
  8. 重建 router 和插件迭代器——确保每个 worker 持有最新的 router(第 970–982 行)
  9. 调用插件的 init_worker handler——逐一执行每个插件的 init_worker 方法(第 987–995 行)
  10. 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 中的完整旅程——从 rewritelog——看看初始化阶段搭建的这套基础设施是如何真正处理流量的。