Read OSS

插件系统:解析、执行与迭代器的艺术

高级

前置知识

  • 第 1-3 篇文章(架构、启动流程、请求管道)
  • 理解 Lua 元表与闭包
  • 熟悉 API 网关插件概念(身份认证、限流)

插件系统:解析、执行与迭代器的艺术

插件系统是 Kong 存在的核心意义。没有插件的 API 网关不过是一个反向代理。正是插件系统将 Kong 转变为一个可编程平台——身份认证、限流、日志记录、请求转换、AI 代理,这些能力都以可组合、可独立配置的模块形式实现。

本文将重点探讨插件是如何被发现的、迭代器如何判断哪些插件适用于当前请求,以及 8 级优先级解析系统如何选取正确的配置。我们将以 key-authrate-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_filterbody_filterlogresponse)则直接重放该列表,无需重新解析。

阶段分类定义于第 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_routeno_serviceno_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 — 各阶段方法(accessheader_filterlog 等)
  • schema.lua — 配置验证 schema
  • daos.lua(可选)— 自定义数据库实体
  • api.lua(可选)— Admin API 扩展

处理器必须导出一个包含 PRIORITYVERSION 字段的表,以及以 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,它同时实现了 accesslog 两个阶段:

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 的自动生成。