Day 12 / 共 20 天 · 第 3 周 插件系统

插件运行 run_plugin(执行引擎)

昨天 filter 选好了插件,今天看它们怎么"跑起来"——run_pluginplugin.lua:1161)在每个阶段遍历插件、调方法、处理"插件想提前结束请求"(拦截)。这是插件链执行的心脏。

📍 你在整条链的位置
加载/选插件 D11 run_plugin 执行 配置合并 D13 鉴权 D14
L01

run_plugin 总览

💡 一个函数处理所有阶段的插件执行 plugin.lua:1161run_plugin(phase, plugins, api_ctx)——给定阶段名,遍历 plugins,调每个插件该阶段的方法。Day 04 里 run_plugin("rewrite"/"access")、Day 02 common_phaserun_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)传 confapi_ctx插件方法签名永远是 (conf, ctx)——配置+上下文,够用了。
L03

提前结束 = 拦截

plugin.lua:1196-1219:插件方法返回 code, body 表示"我要直接返回响应,别往下跑":

limit-count.access检查是否超限 没超限:return nil 继续下一个插件 超限:return 429,"..." core.response.exit(429) 直接返回 ← 后续插件不跑、不转发上游
插件返回 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:1266run_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 的插件配置合并进路由,优先级怎么定,让"复用+定制"两全。
← Day 11 Day 13 · 配置合并 →