插件系统:解析、执行与迭代器的艺术
前置知识
- ›第 1-3 篇文章(架构、启动流程、请求管道)
- ›理解 Lua 元表与闭包
- ›熟悉 API 网关插件概念(身份认证、限流)
插件系统:解析、执行与迭代器的艺术
插件系统是 Kong 存在的核心意义。没有插件的 API 网关不过是一个反向代理。正是插件系统将 Kong 转变为一个可编程平台——身份认证、限流、日志记录、请求转换、AI 代理,这些能力都以可组合、可独立配置的模块形式实现。
本文将重点探讨插件是如何被发现的、迭代器如何判断哪些插件适用于当前请求,以及 8 级优先级解析系统如何选取正确的配置。我们将以 key-auth 和 rate-limiting 插件为具体示例展开说明。
插件发现与加载
插件发现从 kong/constants.lua 中的 BUNDLED_PLUGINS 映射开始——这是一份包含 45 个插件名称的列表。在 Kong.init() 期间,该列表驱动整个加载过程:
assert(db.plugins:load_plugin_schemas(config.loaded_plugins))
schema 加载逻辑位于 kong/db/schema/plugin_loader.lua。对每个插件,load_subschema 会 require kong.plugins.<name>.schema,对其进行 MetaSubSchema 验证,并将其注册为 plugins 实体的子 schema:
function plugin_loader.load_subschema(parent_schema, plugin, errors)
local plugin_schema = "kong.plugins." .. plugin .. ".schema"
local ok, schema = load_module_if_exists(plugin_schema)
-- validate against MetaSubSchema
ok, err_t = MetaSchema.MetaSubSchema:validate(schema)
-- register as subschema
ok, err = Entity.new_subschema(parent_schema, plugin, schema)
return schema
end
这意味着每个插件的配置 schema 都成为数据库中 plugins 实体的子 schema。当你通过 POST /plugins 提交 { "name": "key-auth", "config": { ... } } 时,config 字段会针对 key-auth 的专属 schema 进行验证——但最终都存储在同一张 plugins 表中。
flowchart TD
A[constants.lua: BUNDLED_PLUGINS] --> B[db.plugins:load_plugin_schemas]
B --> C{For each plugin}
C --> D["require kong.plugins.<name>.schema"]
D --> E[MetaSubSchema:validate]
E --> F[Entity.new_subschema]
F --> G[Plugin config validated<br>against its own schema]
C -->|Next plugin| C
Collecting/Collected 迭代器模式
kong/runloop/plugins_iterator.lua 中的插件迭代器实现了一种双模式执行模型,这是 Kong 高性能表现的关键所在。
Collecting 阶段(HTTP 协议对应 access,stream 协议对应 preread)负责解析哪些插件适用于当前请求,并构建执行列表。Collected 阶段(header_filter、body_filter、log、response)则直接重放该列表,无需重新解析。
阶段分类定义于第 29–65 行:
NON_COLLECTING_PHASES = {
"certificate", "rewrite", "response",
"header_filter", "body_filter", "log",
}
COLLECTING_PHASE = "access"
这一设计有其深刻原因:在 access 阶段,Kong 已经知道匹配到的 Route、Service 以及已认证的 Consumer,这正是解析插件配置所需的信息。到了后续阶段(header_filter、body_filter),重新解析不仅多余,更是一种浪费——在 access 阶段运行的插件在下游阶段应当保持一致。
位于第 372–411 行的 collecting 迭代器承担着核心工作:对每个插件,它调用 load_configuration_through_combos 查找适用的配置,然后将插件与配置的组合记录到该插件所实现的每个下游阶段中:
for j = 1, DOWNSTREAM_PHASES_COUNT do
local phase = DOWNSTREAM_PHASES[j]
if handler[phase] then
local n = collected[phase][0] + 2
collected[phase][0] = n
collected[phase][n] = cfg
collected[phase][n - 1] = plugin
end
end
收集到的数据存储在 ctx.plugins 上——这是一个从对象池中分配的表,为每个下游阶段维护独立的子表。每个子表是 [plugin, config, plugin, config, ...] 形式的平铺数组,元素总数存储在索引 [0] 处。
sequenceDiagram
participant Access as access phase (collecting)
participant Iterator as plugins_iterator
participant HeaderFilter as header_filter (collected)
participant BodyFilter as body_filter (collected)
participant Log as log (collected)
Access->>Iterator: get_collecting_iterator(ctx)
Iterator->>Iterator: For each loaded plugin...
Iterator->>Iterator: load_configuration_through_combos()
Iterator->>Iterator: If config found, record in ctx.plugins
Note over Iterator: ctx.plugins.header_filter = [plugin1, cfg1, plugin2, cfg2]
Note over Iterator: ctx.plugins.log = [plugin1, cfg1, plugin2, cfg2]
Iterator-->>Access: yield (plugin, config) for access handler
HeaderFilter->>Iterator: get_collected_iterator("header_filter", ctx)
Iterator-->>HeaderFilter: replay ctx.plugins.header_filter
BodyFilter->>Iterator: get_collected_iterator("body_filter", ctx)
Iterator-->>BodyFilter: replay ctx.plugins.body_filter
Log->>Iterator: get_collected_iterator("log", ctx)
Iterator-->>Log: replay ctx.plugins.log
提示: 如果你好奇为什么
rewrite使用全局迭代器而非 collecting 迭代器——原因在于 rewrite 阶段发生在路由匹配之前。没有匹配到的 Route,就无法解析 route/service 级别的插件配置。因此在 rewrite 阶段,只有全局插件(未绑定任何 Route、Service 或 Consumer 的插件)才会执行。
8 级配置解析
当一个插件适用于某个请求时,其配置可能来自多个插件实例,因此需要从中解析出最终生效的那个。例如,rate-limiting 插件可能在全局、特定 service 以及特定 route+consumer 组合上都有配置。越具体的配置优先级越高。
位于第 215–267 行的 lookup_cfg 函数实现了这套 8 级优先级查找逻辑:
| 优先级 | 组合 | 具体程度 |
|---|---|---|
| 1 | Route + Service + Consumer | 最具体 |
| 2 | Route + Consumer | |
| 3 | Service + Consumer | |
| 4 | Route + Service | |
| 5 | 仅 Consumer | |
| 6 | 仅 Route | |
| 7 | 仅 Service | |
| 8 | 全局(无任何关联) | 最宽泛 |
每种组合通过第 85 行的 build_compound_key 生成复合键:
local function build_compound_key(route_id, service_id, consumer_id)
return format("%s:%s:%s", route_id or "", service_id or "", consumer_id or "")
end
查找在首次命中时即短路返回。如果 Route+Service+Consumer 的配置存在,则不再检查 Route+Consumer。这意味着全局配置充当兜底——只有在找不到更具体的配置时才会生效。
位于第 282–291 行的 load_configuration_through_combos 函数在此之上又增加了一层灵活性:插件处理器可以声明 no_route、no_service 或 no_consumer 标志,在查找前过滤掉对应维度。这种用法较为少见,但允许特殊插件选择性地忽略某些作用域级别。
flowchart TD
A[Request Context] --> B{Route + Service + Consumer?}
B -->|Found| Z[Use this config]
B -->|Not found| C{Route + Consumer?}
C -->|Found| Z
C -->|Not found| D{Service + Consumer?}
D -->|Found| Z
D -->|Not found| E{Route + Service?}
E -->|Found| Z
E -->|Not found| F{Consumer only?}
F -->|Found| Z
F -->|Not found| G{Route only?}
G -->|Found| Z
G -->|Not found| H{Service only?}
H -->|Found| Z
H -->|Not found| I{Global?}
I -->|Found| Z
I -->|Not found| J[Plugin does not apply]
插件处理器结构与优先级排序
每个 Kong 插件都遵循统一的目录结构。以 key-auth 为例,它位于 kong/plugins/key-auth/ 目录下,包含以下文件:
handler.lua— 各阶段方法(access、header_filter、log等)schema.lua— 配置验证 schemadaos.lua(可选)— 自定义数据库实体api.lua(可选)— Admin API 扩展
处理器必须导出一个包含 PRIORITY 和 VERSION 字段的表,以及以 Nginx 阶段命名的方法。以下是 kong/plugins/key-auth/handler.lua 的定义:
local KeyAuthHandler = {
VERSION = kong_meta.version,
PRIORITY = 1250,
}
PRIORITY 决定执行顺序——数值越高,越先执行。这一点至关重要:认证插件(key-auth 为 1250,jwt 为 1250,basic-auth 为 1100)必须在授权插件(acl 为 950)之前运行,而授权插件又必须在限流插件(rate-limiting 为 910)之前运行。
key-auth 插件仅实现了 access 阶段,位于第 262–273 行:
function KeyAuthHandler:access(conf)
if not conf.run_on_preflight and kong.request.get_method() == "OPTIONS" then
return
end
if conf.anonymous then
return logical_OR_authentication(conf)
else
return logical_AND_authentication(conf)
end
end
再来看 kong/plugins/rate-limiting/handler.lua,它同时实现了 access 和 log 两个阶段:
RateLimitingHandler.VERSION = kong_meta.version
RateLimitingHandler.PRIORITY = 910
rate-limiting 插件在 access 阶段检查并执行限流规则,然后在 log 阶段异步同步计数器增量(当使用带有同步频率的 cluster 策略时)。这种多阶段模式十分常见——插件在前期执行核心逻辑,在后期处理账务性工作。
PDK:插件开发工具包 API
插件与 Kong 的所有交互都通过 PDK——即 kong.* 命名空间——进行。该 API 接口定义于 kong/pdk/init.lua:
local MAJOR_MODULES = {
"table", "node", "log", "ctx", "ip", "client",
"service", "request", "service.request", "service.response",
"response", "router", "nginx", "cluster", "vault",
"tracing", "plugin", "telemetry",
}
每个模块从 kong/pdk/<name>.lua 加载后挂载到 kong 全局对象上。核心命名空间说明如下:
| 命名空间 | 用途 | 示例 |
|---|---|---|
kong.request |
读取入站请求 | kong.request.get_header("Authorization") |
kong.response |
向客户端发送响应 | kong.response.exit(403, { message = "Forbidden" }) |
kong.service.request |
修改发往上游的请求 | kong.service.request.set_header("X-Custom", "value") |
kong.service.response |
读取上游响应 | kong.service.response.get_header("Content-Type") |
kong.client |
Consumer 与凭证信息 | kong.client.authenticate(consumer, credential) |
kong.log |
结构化日志 | kong.log.err("something failed") |
kong.ctx |
插件作用域上下文 | kong.ctx.plugin.my_data = "value" |
kong.cache |
数据库缓存 | kong.cache:get(key, opts, loader_fn, ...) |
PDK 内置阶段检查机制——例如在 log 阶段调用 kong.service.request.set_header() 会报错,因为此时请求已经发出。这一机制能有效防止插件开发中的隐性 bug。
提示: 编写插件时,使用
kong.ctx.plugin存储在单次请求的多个阶段间持久存在、但仅限于当前插件实例的数据;使用kong.ctx.shared存储插件间共享的数据。切勿使用模块级变量存储请求级别的状态——Nginx worker 会并发处理多个请求。
完整执行流程
以下是单个请求完整的插件执行管道:
flowchart TD
subgraph "init time"
A[Plugin schemas loaded] --> B[Plugin handlers loaded]
B --> C[Plugins sorted by PRIORITY]
end
subgraph "per-request: access phase"
D[Get collecting iterator] --> E[For each plugin in priority order]
E --> F[lookup_cfg: 8-level resolution]
F --> G{Config found?}
G -->|Yes| H[Record for downstream phases]
H --> I[Execute plugin.access]
G -->|No| J[Skip plugin]
I --> E
J --> E
end
subgraph "per-request: downstream phases"
K[Get collected iterator] --> L[Replay recorded plugin+config pairs]
L --> M[Execute plugin.header_filter]
M --> N[Execute plugin.body_filter]
N --> O[Execute plugin.log]
end
在第 5 篇文章中,我们将深入探讨存储插件配置、路由定义和服务元数据的数据库层——正是这套 schema 系统以单一数据源驱动了验证、序列化以及 Admin API 的自动生成。