插件开发最佳实践
本指南汇总了让自定义 Lua 插件表现得像一等内置插件的编码实践:可预期的配置、正确的请求与响应处理,以及在高负载下的良好性能。
其中最重要的一条习惯,是基于 apisix.core 库来开发,而不是直接调用底层的 OpenResty(ngx.*)API。每个内置插件都是这样做的。apisix.core 是网关的插件工具箱。它在底层 OpenResty 原语之上封装了请求级缓存、一致的错误处理,以及一系列帮你处理好边界情况的辅助函数,省去你重新踩坑的成本。
local core = require("apisix.core")
使用 apisix.core,而非裸 ngx API
裸 ngx.* 调用往往在快速测试时能用,却会在生产环境中引发隐蔽的问题。core.* 封装之所以存在,是因为它们:
- 缓存请求级读取。
core.request.headers(ctx)和ctx.var.<name>只计算一次并在整个请求生命周期内复用,而反复调用ngx.req.get_headers()每次都会重新解析。 - 与网关其余部分保持一致。 当某个插件通过
core.request.set_header设置了 header,其他插件读到的缓存视图仍然正确;混用裸ngx.req.set_header可能会留下过期的缓存值。 - 替你处理别扭的默认值。 例如
core.request.get_uri_args读取参数时不做截断,而ngx.req.get_uri_args()会在 100 个参数处静默截断。 - 收敛行为。 日志、JSON 编码与 schema 校验都走统一入口,因此级别过滤、错误处理和格式化都是一致的。
ngx 到 core 的对照
优先选用 core 等价物。下表覆盖了你最常用到的替换。
裸 ngx API | 优先使用的 core API | 说明 |
|---|---|---|
ngx.log(ngx.ERR, ...) | core.log.error(...)(.warn、.info、.debug) | 自动按级别过滤;传多个参数而不是先拼接。 |
ngx.req.get_headers() | core.request.headers(ctx) | 在 ctx 上缓存;key 转小写以支持大小写不敏感查找。 |
ngx.req.get_headers()[name] | core.request.header(ctx, name) | 大小写不敏感;自动展开多值 header。 |
ngx.req.set_header(k, v) | core.request.set_header(ctx, k, v) | 同时会让缓存的 header 视图失效。 |
ngx.req.get_uri_args() | core.request.get_uri_args(ctx) | 不做截断;结果被缓存。 |
ngx.req.set_uri_args(a) | core.request.set_uri_args(ctx, a) | 接受 table 或字符串。 |
ngx.req.read_body() + ngx.req.get_body_data() | core.request.get_body() | 也会读取落到临时文件的 body;仍可能返回 nil。 |
ngx.req.get_method() | core.request.get_method() | |
ngx.var.<name> | ctx.var.<name> | 惰性且带缓存;还暴露 route_id、consumer_name 等网关变量。 |
ngx.header[k] = v | core.response.set_header(k, v) | 会防护 header 已发送的情况。 |
ngx.say / ngx.print / ngx.exit | core.response.exit(code, body) | table 类型的 body 会被 JSON 编码;调用必须被 return。 |
cjson.encode / cjson.decode | core.json.encode / core.json.decode | decode 在输入非法时返回 nil, err。 |
日志中的 cjson.encode | core.json.delay_encode(data) | 仅当日志行真正输出时才编码。 |
| JSON Schema 校验 | core.schema.check(schema, conf) | 缓存编译后的校验器。 |
table.insert / table.new / table.clear | core.table.insert / core.table.new / core.table.clear | core.table 也代理了标准 table 库。 |
ngx.timer.every(周期任务) | core.timer.new(name, cb, opts) | 在 pcall 中运行回调以隔离错误。 |
core 库速览
core 把辅助函数按子模块归类。插件作者最常用的有:
| 子模块 | 用途 |
|---|---|
core.request | 读取和修改进入的客户端请求。 |
core.response | 设置响应 header 并短路请求。 |
core.ctx / ctx.var | 访问请求上下文以及 Nginx 或网关变量。 |
core.log | 带级别的日志。 |
core.json | 编解码 JSON,含用于日志的惰性编码。 |
core.schema | 用 JSON schema 校验插件配置。 |
core.table | table 辅助:预分配、清空、计数、深拷贝、合并。 |
core.string | 快速的纯文本字符串辅助(前缀、后缀、查找)。 |
core.utils | 杂项辅助:变量替换、DNS、UUID。 |
core.lrucache | 每 worker 的 LRU 缓存 ,按插件配置作为键。 |
core.timer | 受管理的后台定时器。 |
读取和修改请求
core.request 中的大多数函数第一个参数都是 ctx。
function _M.access(conf, ctx)
-- 读取一个 header(大小写不敏感)和一个查询参数。
local token = core.request.header(ctx, "Authorization")
local args = core.request.get_uri_args(ctx)
-- 在请求被代理到上游前设置一个请求 header。
core.request.set_header(ctx, "X-Consumer-Tier", conf.tier)
end
关键函数:
core.request.header(ctx, name)—— 单个 header 值,大小写不敏感。core.request.headers(ctx)—— 所有 header,返回 key 为小写的 table。core.request.set_header(ctx, name, value)—— 设置 header;传value = nil可删除它。core.request.get_uri_args(ctx)—— 解析后的查询参数。core.request.get_body()—— 原始请求 body 字符串。它可能返回nil(例如没有 body 时),使用前务必判空。core.request.get_method()—— HTTP 方法。core.request.get_remote_client_ip(ctx)—— 下游客户端 IP。
以 JSON 读取请求 body 很常见,有专门的辅助函数:
local body, err = core.request.get_json_request_body_table()
if not body then
core.log.warn("failed to read JSON body: ", core.json.delay_encode(err))
return 400, { message = "invalid request body" }
end
设置响应并短路
core.response 上的 header setter 不接受 ctx。
要在插件内部终止请求并返回响应,直接返回状态码和 body。插件运行器会把返回的这对值转成响应,因此你很少需要直接调用 core.response.exit:
function _M.access(conf, ctx)
if not is_authorized(ctx) then
core.response.set_header("WWW-Authenticate", "ApiKey")
return 401, { message = "unauthorized" }
end
end
table 类型的 body 会自动 JSON 编码。如果你自己调用 core.response.exit,必须把它的结果 return:
return core.response.exit(429, { message = "rate limit exceeded" })
要在 header_filter 阶段修改响应 header,使用 core.response.set_header 和 core.response.add_header。如果插件重写了响应 body,请在 header_filter 中调用 core.response.clear_header_as_body_modified(),以便在客户端看到与之不再匹配的 body 之前,清除 Content-Length、Content-Encoding、ETag 和 Last-Modified。
访问上下文变量
ctx.var.<name> 通过一个惰性、带缓存的统一接口读取 Nginx 变量和网关特有变量。优先用它而非 ngx.var:
function _M.log(conf, ctx)
core.log.info("request to ", ctx.var.uri,
" matched route ", ctx.var.route_id,
" for consumer ", ctx.var.consumer_name)
end
除了原始 Nginx 变量,ctx.var 还暴露 route_id、route_name、service_id、service_name、consumer_name、balancer_ip、balancer_port 等网关值。首次访问后结果被缓存,重复查找的开销很低。
用合适的级别打日志
core.log 提供 error、warn、info、debug(以及较少用的 notice、crit、alert)。网关会过滤掉低于配置的 error-log 级别的消息,因此当级别设为 warn 时,生产环境中一次 core.log.info 调用几乎没有开销。
把各个值作为独立参数传入,而不是先拼接;对 table 使用 core.json.delay_encode,这样只有在日志行真正被写出时才生成 JSON:
-- 推荐:关闭 info 日志时不会有任何开销。
core.log.info("plugin conf: ", core.json.delay_encode(conf))
-- 避免:即使该行会被丢弃,也会在每个请求上编码并拼接。
core.log.info("plugin conf: " .. core.json.encode(conf))
对需要运维关注的情况用 error,对可恢复的问题用 warn,对诊断信息用 info 或 debug。
编解码 JSON
core.json.encode(data)—— 编码为 JSON 字符串。传core.json.encode(data, true)可强制序列化通常无法编码的值。core.json.decode(str)—— 解码;失败时返回nil, err,因此要同时检查两个返回值。core.json.delay_encode(data)—— 用于日志行的惰性编码。core.json.canonical_encode(data)—— 确定性的、按 key 排序的输出,适合做缓存键或签名。
处理 table 和字符串
core.table 代理标准 table 库并增加了一些辅助:
core.table.new(narr, nrec)—— 已知大小时提前分配 table。core.table.clear(t)—— 复用 table 而不重新分配。core.table.nkeys(t)—— 统计 key 数量。core.table.insert/core.table.insert_tail—— 追加一个或多个值。core.table.deepcopy(t)和core.table.clone(t)—— 拷贝你不想改动的配置。core.table.try_read_attr(t, "a", "b", "c")—— 安全地读取嵌套字段,任一层缺失则返回nil。
core.string 代理标准 string 库并增加了快速的纯文本辅助:
core.string.has_prefix(s, prefix)和core.string.has_suffix(s, suffix)。core.string.find(haystack, needle)—— 纯文本查找,而非 Lua 模式匹配。
用 schema 校验配置
每个插件都必须定义一个 JSON schema 和一个 check_schema 函数。schema 是插件的契约:它在写入时拒绝非法配置、应用默认值,并说明可接受的字段。
local schema = {
type = "object",
properties = {
tier = { type = "string", enum = { "free", "pro" }, default = "free" },
timeout = { type = "integer", minimum = 1, default = 30 },
},
required = { "tier" },
}
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
建议:
-
用
enum、minimum、maximum、minLength、pattern、required约束取值。schema 约束得越多,handler 里需要的防御性代码就越少。 -
设置合理的
default,让运维只需配置必要的字段。 -
让 schema 承担校验。一个值一旦通过了
check_schema