Read OSS

Schema 驱动设计:Kong 数据层从校验到查询的完整解析

高级

前置知识

  • 第 1–4 篇(完整架构与插件机制)
  • 理解 Lua 元表与面向对象模式
  • 具备数据库基础知识(CRUD、迁移、Schema)

Schema 驱动设计:Kong 数据层从校验到查询的完整解析

Kong 架构中最精妙的设计之一,是用一份 Schema 定义驱动一切:数据库表的创建、输入校验、API 端点生成、缓存键计算,以及外键约束。新增一个实体,系统就会自动生成 CRUD 端点、数据库查询逻辑和校验规则,无需编写任何样板代码。

本文将从顶层的元 Schema 出发,逐步深入到具体的实体定义、DAO 层和数据库策略,最终展示这套 Schema 体系如何为 Admin API 提供支撑。

元 Schema:Schema 的 Schema

如何校验一个 Schema?用另一个 Schema。kong/db/schema/metaschema.lua 定义了编写实体 Schema 的规则——包括允许的字段类型、校验器、数据转换方式和约束条件。

元 Schema 在 第 47–60 行 声明了字段属性的完整词汇表:

local validators = {
  { between = { type = "array", elements = { type = "number" }, len_eq = 2 } },
  { eq = { type = "any" } },
  { ne = { type = "any" } },
  { gt = { type = "number" } },
  { len_eq = { type = "integer" } },
  { match = { type = "string" } },
  { starts_with = { type = "string" } },
  -- ...
}

每个校验器都对应 Schema 类中的一个校验函数。当你在实体 Schema 中写下 { type = "string", match = "^[a-z]+$" } 时,元 Schema 会先校验 match 是否是合法的字符串类型字段属性,Schema 类随后再调用对应的 match 校验函数执行实际校验。

元 Schema 还定义了专门用于插件配置 Schema 的 MetaSubSchema 变体。两者的区别很重要:实体 Schema 定义带有主键和外键的数据库表,而子 Schema 定义的是 plugins 实体中的 config 字段结构。

classDiagram
    class MetaSchema {
        +validate(schema_def)
        +fields: validators, types, constraints
    }
    class MetaSubSchema {
        +validate(plugin_schema)
        +plugin-specific rules
    }
    class EntitySchema {
        +name: string
        +fields: field_defs[]
        +primary_key: string[]
        +entity_checks: check[]
    }
    class PluginSubSchema {
        +name: string
        +fields: config_fields[]
    }
    MetaSchema --> EntitySchema : validates
    MetaSubSchema --> PluginSubSchema : validates
    MetaSchema --> MetaSubSchema : derives from

实体 Schema 定义

通过一个具体示例,可以直观感受这套系统的运作方式。kong/db/schema/entities/routes.lua 定义了 routes 实体,包含字段定义、外键关系和实体级校验:

local entity_checks = {
  { conditional = {
    if_field = "protocols",
    if_match = { elements = { type = "string", not_one_of = { "grpcs", "https", "tls", "tls_passthrough" }}},
    then_field = "snis",
    then_match = { len_eq = 0 },
    then_err = "'snis' can only be set when 'protocols' is 'grpcs', 'https', 'tls' or 'tls_passthrough'",
  }},
}

实体检查用于约束单个字段无法表达的跨字段逻辑。在这个例子中,SNI(Server Name Indication)只对基于 TLS 的协议有意义——在 HTTP 路由上设置 SNI 是无效的。

constants.lua 中的加载顺序同样关键。第 137–155 行CORE_ENTITIES 列表按如下顺序排列:workspacesconsumerscertificatesservicesroutessnisupstreamstargetspluginstags……被依赖的实体必须先于依赖它的实体加载。Services 必须在 Routes 之前加载,因为 Routes 持有指向 Services 的外键。

提示: CORE_ENTITIES 表同时兼具数组和集合两种用法。数组填充完成后,第 307–309 行 的循环会追加 CORE_ENTITIES["routes"] = true,从而支持 O(1) 的成员判断。这种模式在 Kong 的 Lua 代码中随处可见。

DAO 自动生成与 DB 模块

kong/db/init.lua 是 Schema 变为可运行实体的关键所在。DB.new() 构造函数依次遍历核心实体 Schema,用元 Schema 对每个 Schema 进行校验,创建 Entity 对象,并实例化对应的 DAO:

for _, entity_name in ipairs(constants.CORE_ENTITIES) do
  local entity_schema = require("kong.db.schema.entities." .. entity_name)
  local ok, err_t = MetaSchema:validate(entity_schema)
  -- ...
  local entity, err = Entity.new(entity_schema)
  schemas[entity_name] = entity
end

接着在 第 108–118 行

for _, schema in pairs(schemas) do
  local strategy = strategies[schema.name]
  daos[schema.name] = DAO.new(self, schema, strategy, errors)
end

第 31–33 行 定义的 __index 元方法使 kong.db.routes 能够透明地解析为 self.daos['routes']

DB.__index = function(self, k)
  return DB[k] or rawget(self, "daos")[k]
end

这意味着调用 kong.db.routes:select({ id = "..." }) 时,请求会自动委派给 Routes DAO——它使用 Routes Schema 进行校验,并通过 Routes strategy 执行实际的数据库操作。

flowchart TD
    A[DB.new] --> B[Load each entity schema]
    B --> C[MetaSchema:validate]
    C --> D[Entity.new - create schema object]
    D --> E[Strategy.new - create DB strategy]
    E --> F[DAO.new - create DAO]
    F --> G["kong.db.routes = DAO(routes_schema, postgres_strategy)"]
    G --> H["kong.db.routes:select() → schema.validate → strategy.select → SQL"]

数据库策略:Postgres 与 DB-less(LMDB)

DAO 层与具体的数据库实现无关。每个 DAO 将操作委托给 strategy 对象,由后者完成实际的数据库读写。Kong 内置两种 strategy:

Postgreskong/db/strategies/postgres/)——传统策略,根据 Schema 定义动态生成 SQL 查询。连接器通过 ngx_lua 的基于 cosocket 的 Postgres 驱动管理连接池,并通过 setkeepalive() 在请求之间复用连接。

Off/LMDBkong/db/strategies/off/)——用于 DB-less 模式和数据面(Data Plane)。该策略不查询远程数据库,而是从 LMDB(Lightning Memory-Mapped Database)这一嵌入式键值存储中读取数据。LMDB 提供内存映射的只读访问和零拷贝语义,非常适合需要快速本地查询的数据面场景——其配置由控制面下发。

flowchart LR
    subgraph "Traditional Mode"
        A1[DAO] --> B1[Postgres Strategy]
        B1 --> C1[(PostgreSQL)]
    end
    subgraph "DB-less / Data Plane"
        A2[DAO] --> B2[Off Strategy]
        B2 --> C2[(LMDB)]
    end
    D[Admin API / Plugin] --> A1
    D --> A2

第 156–174 行ENTITY_CACHE_STORE 映射决定了各实体类型使用哪块共享内存缓存。核心路由相关实体(routesservicespluginsupstreamstargets)存入 core_cache,而非关键实体(consumerstags)则存入普通的 cache。这种分离确保路由数据不会被插件缓存的写入压力所驱逐。

声明式配置与 DB-less 模式

在 DB-less 模式下,Kong 从 YAML/JSON 文件而非数据库中读取全部配置。kong/db/declarative/init.lua 模块负责管理这一过程:

function _M.new_config(kong_config, partial)
  local schema, err = declarative_config.load(
    kong_config.loaded_plugins,
    kong_config.loaded_vaults
  )
  local self = { schema = schema, partial = partial }
  return setmetatable(self, _MT)
end

声明式配置的 Schema 根据已加载的插件动态生成——它涵盖所有核心实体字段,以及插件定义的自定义实体字段。整个加载流程如下:

  1. 解析 YAML/JSON 为 Lua 表
  2. 校验 生成的 Schema(与数据库校验使用同一套 Schema 类)
  3. 展平 为按实体类型索引的实体记录
  4. 写入 LMDB,以单次事务提交

当数据面从控制面接收配置时(将在第 6 篇详细介绍),配置以序列化的声明式配置负载形式传输。load_into_cache_with_events 函数统一处理文件加载和网络接收两种来源,完成后重建 router 和插件迭代器。

flowchart TD
    A["kong.yml (YAML/JSON)"] --> B[Parse to Lua tables]
    B --> C["Validate against declarative schema"]
    C --> D{Valid?}
    D -->|No| E[Error with path to invalid field]
    D -->|Yes| F[Flatten into entity records]
    F --> G[Write to LMDB transaction]
    G --> H[Rebuild router]
    H --> I[Rebuild plugins iterator]

Admin API:Schema 驱动的端点生成

kong/api/init.lua 中的 Admin API 基于实体 Schema 自动生成 CRUD 端点。系统先加载核心路由,再为各实体生成对应端点:

-- Load core routes
for _, v in ipairs({"kong", "health", "cache", "config", "debug"}) do
  local routes = require("kong.api.routes." .. v)
  api_helpers.attach_routes(app, routes)
end

kong/api/endpoints.lua 模块提供自动生成的核心逻辑。针对每个实体 Schema,它会生成以下端点:

  • GET /routes — 列出所有路由(分页)
  • POST /routes — 创建路由
  • GET /routes/:routes — 获取指定路由
  • PATCH /routes/:routes — 更新路由
  • PUT /routes/:routes — 创建或更新路由
  • DELETE /routes/:routes — 删除路由

外键关系会自动生成嵌套路由。由于 Routes 持有指向 Services 的外键,系统会自动生成 GET /services/:services/routes 来查询某个 Service 下的所有路由。

插件可以通过自身的 api.lua 模块扩展 Admin API。第 58 行customize_routes 函数允许插件定义的端点覆盖或包装自动生成的端点,原始函数通过 parent 参数传入。

提示: endpoints.lua 第 25–40 行 将内部错误类型映射到 HTTP 状态码:UNIQUE_VIOLATION → 409,NOT_FOUND → 404,SCHEMA_VIOLATION → 400。这里是 Admin API 错误响应的唯一权威来源。

Schema 的级联效应

这套设计的精妙之处在于其级联特性。一份 Schema 定义可以派生出:

产物 来源
数据库表(DDL) Schema 字段 + 类型
输入校验 Schema 校验器 + entity_checks
缓存键计算 Schema 的 cache_key 字段
Admin API 端点 Schema 名称 + 外键关系
声明式配置格式 Schema 字段(YAML 结构)
插件子 Schema MetaSubSchema 校验

每当 Kong 新增一个实体(例如为 Wasm 支持添加的 filter_chains),一份 Schema 文件便会自动级联生成上述所有产物。这正是集中化数据建模减少跨系统边界不一致性的有力体现。

在第 6 篇中,我们将探讨这套数据层如何融入 Kong 的分布式架构——控制面如何序列化配置并推送至数据面,以及数据面如何通过声明式配置流水线加载并应用这些配置。