Day 12 / 共 20 天 · 第 3 周 插件系统
插件运行 run_plugin(执行引擎)
昨天 filter 选好了插件,今天看它们怎么"跑起来"——run_plugin(plugin.lua:1161)在每个阶段遍历插件、调方法、处理"插件想提前结束请求"(拦截)。这是插件链执行的心脏。
📍 你在整条链的位置
加载/选插件 D11→
run_plugin 执行→
配置合并 D13→
鉴权 D14
L01
run_plugin 总览
💡 一个函数处理所有阶段的插件执行
plugin.lua:1161:run_plugin(phase, plugins, api_ctx)——给定阶段名,遍历 plugins,调每个插件该阶段的方法。Day 04 里 run_plugin("rewrite"/"access")、Day 02 common_phase 调 run_plugin("header_filter"/"log")——都是它。它是 APISIX 请求处理的执行引擎——Day 04 的整个 access 处理,本质就是几次 run_plugin 调用。L02
成对遍历(回收 Day 11)
plugin.lua:1178+:用 for i = 1, #plugins, 2 每次跳 2(回收 Day 11 的"成对存"):
for i = 1, #plugins, 2 do
local phase_func = plugins[i][phase] -- plugins[i] 是插件对象,取它的阶段方法
local conf = plugins[i + 1] -- plugins[i+1] 是该插件的配置
if phase_func then
local code, body = phase_func(conf, api_ctx) -- 调用!传配置和上下文
-- ... 处理返回值(下一节)
end
end
为什么步长是 2
因为 filter 存成
{对象, 配置, 对象, 配置...}。步长 2 每轮拿一对:plugins[i] 对象、plugins[i+1] 配置。调 plugins[i][phase](比如 limit-count.access)传 conf 和 api_ctx。插件方法签名永远是 (conf, ctx)——配置+上下文,够用了。L03
提前结束 = 拦截
plugin.lua:1196-1219:插件方法返回 code, body 表示"我要直接返回响应,别往下跑":
插件返回 code/body → core.response.exit 直接返回 → 拦截(后续插件不跑、不转发上游)。
local code, body = phase_func(conf, api_ctx)
if code or body then
if code >= 400 and conf._meta and conf._meta.error_response then
body = conf._meta.error_response -- 用配置的错误消息,不泄露真实原因
end
core.response.exit(code, body) -- 直接返回,终止后续插件和转发
end
插件怎么"拦截"请求
鉴权发现 key 无效 →
return 401;限流发现超限 → return 429。run_plugin 看到返回了 code,就 core.response.exit 直接发响应,不再跑后面插件、不转发上游。这就是"拦截"。error_response 是安全设计:可用配置的通用错误覆盖真实原因,不让攻击者探测。回想 wasm-go 的 request-block——同样是"返回响应即拦截"。L04
两类阶段的差异
run_plugin 里分两段(:1173 和 :1224):
- 决策阶段(rewrite/access…):插件能返回 code/body 提前结束(拦截)。
- 响应/日志阶段(header_filter/body_filter/log):插件不能拦截(响应已在发送/请求已结束),只处理不中断。
读法:为什么日志阶段不能拦截?因为请求已处理完,"拦"没意义。决策阶段(access 等)才有"放不放行"的语义。所以 run_plugin 对两类阶段用不同遍历逻辑——决策阶段检查返回值,响应/日志阶段只调方法。
L05
rewrite_in_consumer(回收 Day 04)
plugin.lua:1180-1184:处理 Day 04 那个"consumer 二次 rewrite":
if phase == "rewrite_in_consumer" and plugins[i + 1]._skip_rewrite_in_consumer then
goto CONTINUE -- 跳过已经在第一轮 rewrite 跑过的插件
end
local phase_func = phase == "rewrite_in_consumer" and plugins[i]["rewrite"] or plugins[i][phase]
避免两轮 rewrite 重复执行
Day 04:先跑一轮 rewrite(含鉴权识别 consumer)→ 合并 consumer 配置 → 补跑 rewrite_in_consumer。但已跑过 rewrite 的插件不能再跑一遍(会重复执行副作用)。所以 filter 给已跑过的打了
_skip_rewrite_in_consumer 标记(Day 11),这里跳过它们,只跑"consumer 新引入的插件"。L06
meta_filter 条件执行
plugin.lua:1187, 1233:每个插件跑之前先过 meta_filter——插件配置可带 _meta.filter 表达式,只有当前请求满足条件才跑。
📝 "按条件跑插件"
比如"只对匿名用户限流,登录用户不限"就能用它表达。
_meta.filter 写"只对 POST 请求""只对某 header 的请求"——满足才跑这个插件,否则跳过。比如"只对匿名用户限流,登录用户不限"就能用它表达。
读法:这些
_meta.* 机制(disable/priority/filter/error_response)是 APISIX 给所有插件的通用增强。L07
run_global_rules
plugin.lua:1266 的 run_global_rules:跑"全局规则"(Day 08 的 Global Rule)里的插件——对所有请求生效,不绑路由。Day 04 "匹配路由前后都跑 global_rules" 就是它。
读法:全局插件(全站 prometheus、全站限流)通过 global_rules 运行,独立于路由级插件。它在路由匹配前(未匹配也跑)和匹配后各跑相关阶段。机制和 run_plugin 类似,只是插件来源是 global_rules。
L08
今日小结 + 动手
🧠 今天你应该能回答
- run_plugin 怎么用步长 2 成对遍历插件+配置?
- 插件怎么"拦截"请求(返回 code/body)?error_response 的安全意义?
- 为什么决策阶段能拦截、日志阶段不能?
- rewrite_in_consumer 怎么避免重复执行?meta_filter 干什么?
✋ 动手
cd /Users/bitmart/work/codes/github/apisix
sed -n '1161,1240p' apisix/plugin.lua
sed -n '1266,1300p' apisix/plugin.lua
明天预告 · Day 13:插件配置合并——
merge_service_route/merge_consumer_route 怎么把 service、consumer 的插件配置合并进路由,优先级怎么定,让"复用+定制"两全。