Apache APISIX 插件执行顺序
从全局规则到路由插件、从 Priority 排序到阶段调度,逐层拆解 APISIX 插件引擎的执行链路
为什么要理解插件执行顺序
插件执行顺序决定了 API 网关的行为
Apache APISIX 是一个基于 OpenResty 的高性能 API 网关,其核心能力由 插件系统 驱动。一个请求从进入网关到返回响应,可能经过数十个插件的处理。插件的执行顺序直接决定了:认证在限流之前还是之后?请求改写在鉴权之前还是之后?日志记录能否捕获到完整的上下文信息?
理解执行顺序的关键维度
APISIX 插件的执行顺序由以下四个维度共同决定:
Priority 优先级
每个插件定义了一个 priority 数值,数值越大越先执行。这是同一阶段内插件执行顺序的决定因素
Global Rules 全局规则
全局规则中的插件 始终先于 路由插件执行,无论优先级如何
Phases 执行阶段
OpenResty 的请求生命周期分为 rewrite、access、header_filter、body_filter、log 等阶段,每个阶段独立执行插件
Merge 合并策略
Service、Plugin Config、Consumer 的插件通过特定的合并策略与 Route 插件融合
整体架构概览
APISIX 插件引擎在请求处理中的位置
APISIX 的请求处理流程建立在 OpenResty 的阶段模型之上。每个阶段中,插件引擎按照优先级从高到低依次调用各插件对应阶段的处理函数:
-- apisix/init.lua 核心入口
-- OpenResty 阶段 → APISIX Handler → 插件引擎
--
-- access_by_lua → http_access_phase() → 全局规则(rewrite+access) → 路由插件(rewrite+access)
-- header_filter_by_lua → http_header_filter_phase() → 全局规则(header_filter) → 路由插件(header_filter)
-- body_filter_by_lua → http_body_filter_phase() → 全局规则(body_filter) → 路由插件(body_filter)
-- log_by_lua → http_log_phase() → 全局规则(log) → 路由插件(log)
插件引擎的核心数据结构
在 APISIX 内部,插件列表以 扁平数组 的形式存储,每两个元素为一组:plugins[i] 是插件对象(包含处理函数),plugins[i+1] 是该插件的配置。这种设计避免了额外的对象分配开销:
-- plugins 数组结构:
-- plugins[1] = plugin_obj_A (插件对象,包含 rewrite/access/log 等方法)
-- plugins[2] = plugin_conf_A (插件配置,包含用户设定的参数和 _meta)
-- plugins[3] = plugin_obj_B
-- plugins[4] = plugin_conf_B
-- ...
-- 遍历时步长为 2:
for i = 1, #plugins, 2 do
local plugin_obj = plugins[i]
local plugin_conf = plugins[i + 1]
plugin_obj.rewrite(plugin_conf, api_ctx)
end
HTTP 请求完整生命周期
从进入网关到返回响应的完整链路
以下是一个 HTTP 请求在 APISIX 中的完整处理流程,标注了插件在每个环节的执行时机:
http_access_phase() — 请求处理主入口
这是整个插件执行链路的核心函数,定义在 apisix/init.lua:571:
function _M.http_access_phase()
-- 1. 创建请求上下文
local api_ctx = core.tablepool.fetch("api_ctx", 0, 32)
-- 2. 路由匹配
router.router_http.match(api_ctx)
local route = api_ctx.matched_route
-- 3. 即使没有匹配路由,也执行全局规则
if not route then
local global_rules = apisix_global_rules.global_rules()
plugin.run_global_rules(api_ctx, global_rules, nil)
return core.response.exit(404, {error_msg = "404 Route Not Found"})
end
-- 4. 合并 Plugin Config(如果有)
if route.value.plugin_config_id then
local conf = plugin_config.get(route.value.plugin_config_id)
route = plugin_config.merge(route, conf)
end
-- 5. 合并 Service(如果有)
if route.value.service_id then
local service = service_fetch(route.value.service_id)
route = plugin.merge_service_route(service, route)
end
-- 6. ★ 执行全局规则(rewrite + access 阶段)
local global_rules = apisix_global_rules.global_rules()
plugin.run_global_rules(api_ctx, global_rules, nil)
-- 7. ★ 过滤并排序路由插件
local plugins = plugin.filter(api_ctx, route)
-- 8. ★ 执行路由插件的 rewrite 阶段
plugin.run_plugin("rewrite", plugins, api_ctx)
-- 9. ★ 识别 Consumer 并合并插件
if api_ctx.consumer then
route, changed = plugin.merge_consumer_route(route, api_ctx.consumer, ...)
if changed then
-- 重新过滤插件并执行 consumer 中新增插件的 rewrite
api_ctx.plugins = plugin.filter(api_ctx, route, api_ctx.plugins, nil, "rewrite_in_consumer")
plugin.run_plugin("rewrite_in_consumer", api_ctx.plugins, api_ctx)
end
end
-- 10. ★ 执行路由插件的 access 阶段
plugin.run_plugin("access", plugins, api_ctx)
-- 11. 转发到上游
_M.handle_upstream(api_ctx, route, enable_websocket)
end
后续阶段 — common_phase 统一调度
header_filter、body_filter、log 阶段通过 common_phase() 统一调度,定义在 apisix/init.lua:445:
local function common_phase(phase_name)
local api_ctx = ngx.ctx.api_ctx
if not api_ctx then
return
end
-- ★ 先执行全局规则中该阶段的插件
plugin.run_global_rules(api_ctx, api_ctx.global_rules, phase_name)
-- ★ 再执行路由插件中该阶段的插件
return plugin.run_plugin(phase_name, nil, api_ctx)
end
-- 调用示例:
function _M.http_header_filter_phase()
common_phase("header_filter") -- 全局规则 header_filter → 路由插件 header_filter
end
function _M.http_body_filter_phase()
common_phase("body_filter") -- 全局规则 body_filter → 路由插件 body_filter
common_phase("delayed_body_filter")
end
function _M.http_log_phase()
local api_ctx = common_phase("log") -- 全局规则 log → 路由插件 log
-- 清理资源...
end
OpenResty 阶段详解
APISIX 使用的执行阶段
APISIX 的插件可以在以下阶段定义处理函数。每个阶段有不同的特性:
| 阶段 | 对应 OpenResty 阶段 | 特性 | 典型用途 |
|---|---|---|---|
rewrite |
access_by_lua | 可中断请求(返回 code + body) | URI 改写、请求变换、鉴权 |
access |
access_by_lua | 可中断请求(返回 code + body) | 限流、访问控制、代理 |
header_filter |
header_filter_by_lua | 不可中断,仅处理响应头 | 修改响应头、CORS 处理 |
body_filter |
body_filter_by_lua | 不可中断,流式处理响应体 | 响应体改写、压缩 |
delayed_body_filter |
body_filter_by_lua | 不可中断,在 body_filter 之后执行 | 延迟的响应体处理 |
log |
log_by_lua | 不可中断,请求已完成 | 日志记录、指标上报 |
阶段分类:可中断 vs 不可中断
APISIX 的 run_plugin() 函数对两类阶段有不同的处理逻辑(apisix/plugin.lua:1129):
function _M.run_plugin(phase, plugins, api_ctx)
if phase ~= "log"
and phase ~= "header_filter"
and phase ~= "body_filter"
and phase ~= "delayed_body_filter"
then
-- ★ 可中断阶段(rewrite / access)
for i = 1, #plugins, 2 do
local code, body = phase_func(conf, api_ctx)
if code or body then
if code >= 400 then
-- 使用 _meta.error_response 自定义错误响应
if conf._meta and conf._meta.error_response then
body = conf._meta.error_response
end
end
core.response.exit(code, body) -- ★ 直接中断请求
end
end
else
-- ★ 不可中断阶段(header_filter / body_filter / log)
for i = 1, #plugins, 2 do
phase_func(conf, api_ctx) -- 只调用,不处理返回值
end
end
end
插件加载机制
插件加载流程
APISIX 通过 apisix/plugin.lua 中的 load_plugin() 函数加载每个插件。每个插件必须是一个 Lua 模块,并且必须定义以下字段:
-- apisix/plugin.lua:122 - load_plugin 函数
local function load_plugin(name, plugins_list, plugin_type)
local pkg_name = "apisix.plugins." .. name
local ok, plugin = pcall(require, pkg_name)
if not ok then
core.log.error("failed to load plugin [", name, "] err: ", plugin)
return
end
-- ★ 必须有 priority 字段
if not plugin.priority then
core.log.error("invalid plugin [", name, "], missing field: priority")
return
end
-- ★ 必须有 version 字段
if not plugin.version then
core.log.error("invalid plugin [", name, "] missing field: version")
return
end
-- ★ 自动注入 _meta schema(包含 disable / priority / filter / error_response)
local properties = plugin.schema.properties
local plugin_injected_schema = core.schema.plugin_injected_schema
properties._meta = plugin_injected_schema._meta
plugin.name = name
plugin.attr = plugin_attr(name)
core.table.insert(plugins_list, plugin)
end
全局插件列表排序
所有插件加载完成后,会按 priority 降序排列(apisix/plugin.lua:227):
-- 排序函数:priority 越大,在数组中越靠前
local function sort_plugin(l, r)
return l.priority > r.priority
end
-- load() 函数中的排序
local function load(plugin_names, wasm_plugin_names)
-- ... 加载所有插件 ...
-- ★ 按 priority 降序排列
if #local_plugins > 1 then
sort_tab(local_plugins, sort_plugin)
end
-- 调试模式下输出排序结果
for i, plugin in ipairs(local_plugins) do
local_plugins_hash[plugin.name] = plugin
if enable_debug() then
core.log.warn("loaded plugin and sort by priority:",
" ", plugin.priority,
" name: ", plugin.name)
end
end
end
local_plugins 是一个按 priority 降序排列的全局数组。后续为每个请求过滤插件时(plugin.filter()),都按照这个数组的顺序遍历,从而保证高优先级插件先执行。
_meta Schema 自动注入
每个插件的 schema 会被自动注入 _meta 字段(apisix/schema_def.lua:1012),提供以下能力:
plugin_injected_schema = {
_meta = {
type = "object",
properties = {
disable = { -- ★ 禁用插件实例
type = "boolean"
},
error_response = { -- ★ 自定义错误响应
oneOf = {
{ type = "string" },
{ type = "object" }
}
},
priority = { -- ★ 覆盖插件默认优先级
description = "priority of plugins by customized order",
type = "integer"
},
filter = { -- ★ 条件执行过滤器
description = "filter determines whether the plugin ...",
type = "array"
}
}
}
}
Priority 优先级排序
Priority 是同一阶段内插件执行顺序的唯一决定因素
在同一个执行阶段(如 rewrite、access)内,插件按照 priority 值 从大到小 执行。priority 值越大,越先执行,越接近客户端。
Priority 的设计思想
APISIX 的 priority 值设计遵循清晰的分层逻辑:
# Priority 分层设计
#
# 23000-21000 网络层处理 real-ip, client-control, proxy-control
# 12000-12015 请求追踪 request-id, zipkin, skywalking, opentelemetry
# 11000-10000 预处理 fault-injection, mocking, serverless-pre-function
# 4000-3000 安全防护 cors, ip-restriction, ua-restriction, csrf
# 2800-2000 认证鉴权 key-auth, jwt-auth, basic-auth, hmac-auth, authz-keycloak
# 1085-999 流量控制 proxy-cache, proxy-rewrite, limit-conn, limit-count, limit-req
# 966-900 路由控制 traffic-split, redirect, response-rewrite
# 509-501 协议转换 degraphql, kafka-proxy, dubbo-proxy, grpc-transcode
# 500-397 监控日志 prometheus, datadog, http-logger, kafka-logger
# 0 ~ -4000 后处理 serverless-post-function, ext-plugin-post-req/resp
filter() 函数:按全局顺序过滤路由插件
plugin.filter()(apisix/plugin.lua:466)负责为每个请求构建插件执行列表:
function _M.filter(ctx, conf, plugins, route_conf, phase)
local user_plugin_conf = conf.value.plugins
local custom_sort = false
plugins = plugins or core.tablepool.fetch("plugins", 32, 0)
-- ★ 按全局 local_plugins 的顺序(已按 priority 降序排列)遍历
for _, plugin_obj in ipairs(local_plugins) do
local name = plugin_obj.name
local plugin_conf = user_plugin_conf[name]
if type(plugin_conf) ~= "table" then
goto continue -- 路由未启用此插件,跳过
end
if not check_disable(plugin_conf) then -- 未被 _meta.disable 禁用
-- 检测是否有自定义优先级
if plugin_conf._meta and plugin_conf._meta.priority then
custom_sort = true
end
core.table.insert(plugins, plugin_obj) -- 偶数下标:插件对象
core.table.insert(plugins, plugin_conf) -- 奇数下标:插件配置
end
::continue::
end
-- ★ 如果有任何插件设置了 _meta.priority,则重新按自定义优先级排序
if custom_sort then
-- 为未设置 _meta.priority 的插件,用默认 priority 填充
for i = 1, #plugins, 2 do
local plugin_conf = plugins[i + 1]
if not plugin_conf._meta then
plugin_conf._meta = { priority = plugins[i].priority }
elseif not plugin_conf._meta.priority then
plugin_conf._meta.priority = plugins[i].priority
end
end
-- 按 _meta.priority 降序重新排列
sort_tab(tmp_plugin_confs, custom_sort_plugin)
end
return plugins
end
filter() 按照全局 local_plugins 数组的顺序遍历,天然保持 priority 降序。只有当某个插件实例设置了 _meta.priority 时,才会触发重新排序。
内置插件优先级速查表
常用插件 Priority 一览(按执行顺序)
| Priority | 插件名称 | 分类 | 主要阶段 |
|---|---|---|---|
23000 |
real-ip | 网络 | rewrite |
22000 |
client-control | 网络 | rewrite |
21990 |
proxy-control | 网络 | rewrite |
12015 |
request-id | 追踪 | rewrite |
12011 |
zipkin | 追踪 | rewrite + log |
12010 |
skywalking | 追踪 | rewrite + log |
12009 |
opentelemetry | 追踪 | rewrite + log |
12000 |
ext-plugin-pre-req | 外部钩子 | rewrite/access |
11000 |
fault-injection | 测试 | rewrite + access |
10000 |
serverless-pre-function | Serverless | rewrite + access + header_filter + body_filter + log |
4010 |
batch-requests | HTTP | access |
4000 |
cors | 安全 | rewrite + header_filter |
3000 |
ip-restriction | 安全 | access |
2999 |
ua-restriction | 安全 | access |
2990 |
referer-restriction | 安全 | access |
2980 |
csrf | 安全 | access |
2600 |
multi-auth | 认证 | rewrite |
2599 |
openid-connect | 认证 | access |
2530 |
hmac-auth | 认证 | rewrite |
2520 |
basic-auth | 认证 | rewrite |
2510 |
jwt-auth | 认证 | rewrite |
2500 |
key-auth | 认证 | rewrite |
2400 |
consumer-restriction | 授权 | access |
2000 |
authz-keycloak | 授权 | access |
1085 |
proxy-cache | 缓存 | access + header_filter + body_filter |
1008 |
proxy-rewrite | 路由 | rewrite |
1003 |
limit-conn | 限流 | access + log |
1002 |
limit-count | 限流 | access + log |
1001 |
limit-req | 限流 | access |
966 |
traffic-split | 路由 | access |
900 |
redirect | 路由 | rewrite |
899 |
response-rewrite | 变换 | rewrite + header_filter + body_filter |
506 |
grpc-transcode | 协议 | access + header_filter + body_filter |
500 |
prometheus | 监控 | log |
410 |
http-logger | 日志 | log |
403 |
kafka-logger | 日志 | log |
399 |
file-logger | 日志 | log |
-2000 |
serverless-post-function | Serverless | 全阶段 |
-3000 |
ext-plugin-post-req | 外部钩子 | access |
-4000 |
ext-plugin-post-resp | 外部钩子 | header_filter + body_filter |
serverless-post-function(-2000)保证在请求处理的最后阶段运行,ext-plugin-post-resp(-4000)则在最末尾处理响应。
Global Rules 全局规则
全局规则:无条件对所有请求生效的插件
Global Rules(全局规则)是独立于路由定义的插件配置,存储在 etcd 的 /global_rules 路径下。它们对 所有请求 生效,包括未匹配到任何路由的 404 请求。
全局规则的执行机制
run_global_rules() 函数(apisix/plugin.lua:1251)逐条遍历全局规则并执行其中的插件:
function _M.run_global_rules(api_ctx, global_rules, phase_name)
if global_rules and #global_rules > 0 then
-- ★ 保存原始上下文信息
local orig_conf_type = api_ctx.conf_type
local orig_conf_version = api_ctx.conf_version
local orig_conf_id = api_ctx.conf_id
local plugins = core.tablepool.fetch("plugins", 32, 0)
local route = api_ctx.matched_route
-- ★ 遍历每条全局规则
for _, global_rule in config_util.iterate_values(values) do
api_ctx.conf_type = "global_rule"
api_ctx.conf_version = global_rule.modifiedIndex
api_ctx.conf_id = global_rule.value.id
-- ★ 过滤出该规则中启用的插件
core.table.clear(plugins)
plugins = _M.filter(api_ctx, global_rule, plugins, route)
if phase_name == nil then
-- ★ access 阶段入口:同时执行 rewrite + access
_M.run_plugin("rewrite", plugins, api_ctx)
_M.run_plugin("access", plugins, api_ctx)
else
-- ★ 其他阶段:只执行指定阶段
_M.run_plugin(phase_name, plugins, api_ctx)
end
end
-- ★ 恢复原始上下文信息
api_ctx.conf_type = orig_conf_type
api_ctx.conf_version = orig_conf_version
api_ctx.conf_id = orig_conf_id
end
end
全局规则配置示例
# 创建全局规则:对所有请求开启 prometheus 监控和限流
curl http://127.0.0.1:9180/apisix/admin/global_rules/1 -X PUT -d '
{
"plugins": {
"prometheus": {},
"limit-count": {
"count": 1000,
"time_window": 60,
"rejected_code": 429
}
}
}'
Route 路由插件
路由插件的绑定层次
路由插件可以通过多种方式配置,APISIX 在处理请求时会将它们合并为最终的插件列表:
# 插件可以来自以下四个层级:
# 1. Route 直接配置
Route:
uri: /api/v1/*
plugins:
key-auth: {}
proxy-rewrite:
uri: /backend$uri
# 2. Service 绑定的插件(Route 通过 service_id 引用)
Service:
plugins:
limit-count:
count: 100
# 3. Plugin Config 共享配置(Route 通过 plugin_config_id 引用)
Plugin Config:
plugins:
prometheus: {}
http-logger:
uri: "http://log-server/log"
# 4. Consumer 插件(认证后绑定)
Consumer:
username: "user_A"
plugins:
limit-count:
count: 50
group: "user_A_group"
全局与路由的执行关系
核心规则:全局规则始终先于路由插件执行
无论全局规则中的插件 priority 是多少,全局规则 整体 都在路由插件之前执行。这是由 http_access_phase() 中的调用顺序硬编码决定的,而非 priority 比较。
每个阶段的执行时序
-- ========== access_by_lua 阶段 ==========
-- Step 1: 全局规则 rewrite 阶段(所有全局规则的插件按 priority 降序)
-- Step 2: 全局规则 access 阶段(所有全局规则的插件按 priority 降序)
-- Step 3: 路由插件 rewrite 阶段(路由+service+plugin_config 合并后按 priority 降序)
-- Step 4: Consumer 识别 + 合并(认证插件在 rewrite 中识别出 consumer)
-- Step 5: Consumer 新增插件 rewrite_in_consumer 阶段
-- Step 6: 路由插件 access 阶段
-- ========== header_filter_by_lua 阶段 ==========
-- Step 7: 全局规则 header_filter 阶段
-- Step 8: 路由插件 header_filter 阶段
-- ========== body_filter_by_lua 阶段 ==========
-- Step 9: 全局规则 body_filter 阶段
-- Step 10: 路由插件 body_filter 阶段
-- ========== log_by_lua 阶段 ==========
-- Step 11: 全局规则 log 阶段
-- Step 12: 路由插件 log 阶段
全局规则的特殊行为:404 请求也执行
在 http_access_phase() 中,即使没有匹配到路由,全局规则仍然会执行:
local route = api_ctx.matched_route
if not route then
-- ★ 未匹配路由,但全局规则仍然执行
local global_rules = apisix_global_rules.global_rules()
plugin.run_global_rules(api_ctx, global_rules, nil)
return core.response.exit(404, {error_msg = "404 Route Not Found"})
end
这意味着全局规则中的 prometheus、限流等插件可以统计和保护包括 404 在内的所有请求。
Service 与 Route 合并
合并规则:Route 插件优先于 Service 插件
merge_service_route()(apisix/plugin.lua:582)将 Service 和 Route 的插件合并:
local function merge_service_route(service_conf, route_conf)
-- ★ 以 Service 为基础创建深拷贝
local new_conf = core.table.deepcopy(service_conf)
new_conf.value.id = route_conf.value.id
-- ★ Route 的插件覆盖 Service 的同名插件
if route_conf.value.plugins then
for name, conf in pairs(route_conf.value.plugins) do
if not new_conf.value.plugins then
new_conf.value.plugins = {}
end
new_conf.value.plugins[name] = conf -- 直接覆盖!
end
end
return new_conf
end
合并示例
# Service 配置
Service:
plugins:
limit-count: # ← 会被 Route 覆盖
count: 100
prometheus: {} # ← Route 未配置,保留
http-logger:
uri: "http://log/v1" # ← Route 未配置,保留
# Route 配置
Route:
service_id: "svc_1"
plugins:
limit-count: # ← 覆盖 Service 的 limit-count
count: 50
key-auth: {} # ← Route 独有,新增
# 合并结果(最终生效的插件)
Merged:
plugins:
limit-count:
count: 50 # ← 来自 Route(覆盖)
prometheus: {} # ← 来自 Service(保留)
http-logger:
uri: "http://log/v1" # ← 来自 Service(保留)
key-auth: {} # ← 来自 Route(新增)
Plugin Config 合并
Plugin Config:可复用的插件配置集
Plugin Config 允许多个路由共享同一组插件配置。合并逻辑定义在 apisix/plugin_config.lua:56:
function _M.merge(route_conf, plugin_config)
-- ★ 备份 Route 原始插件配置(首次合并时)
if route_conf.orig_plugins then
route_conf.value.plugins = route_conf.orig_plugins -- 恢复
else
route_conf.orig_plugins = route_conf.value.plugins -- 备份
end
route_conf.value.plugins = core.table.clone(route_conf.value.plugins)
-- ★ Plugin Config 中的插件只在 Route 未配置时才生效
for name, value in pairs(plugin_config.value.plugins) do
if not route_conf.value.plugins[name] then
route_conf.value.plugins[name] = value
end
end
return route_conf
end
合并时序
Plugin Config 的合并发生在 Service 合并 之前(init.lua:639-648):
-- 先合并 Plugin Config
if route.value.plugin_config_id then
local conf = plugin_config.get(route.value.plugin_config_id)
route = plugin_config.merge(route, conf)
end
-- 再合并 Service
if route.value.service_id then
local service = service_fetch(route.value.service_id)
route = plugin.merge_service_route(service, route)
end
因此完整的合并优先级为:Route > Plugin Config > Service(Route 的配置最优先)。
Consumer 插件合并
Consumer 合并:认证之后的动态插件注入
Consumer 是 APISIX 中最特殊的插件来源。它在 认证插件执行之后 才被识别,随后将 Consumer 上的插件动态合并到路由插件列表中。
Consumer 合并流程
-- apisix/init.lua:691-722
-- Step 1: 执行路由插件的 rewrite 阶段(包含认证插件如 key-auth)
plugin.run_plugin("rewrite", plugins, api_ctx)
-- Step 2: 认证插件在 rewrite 阶段识别出 consumer
if api_ctx.consumer then
local group_conf
if api_ctx.consumer.group_id then
group_conf = consumer_group.get(api_ctx.consumer.group_id)
end
-- Step 3: 合并 Consumer / Consumer Group 的插件
route, changed = plugin.merge_consumer_route(
route, api_ctx.consumer, group_conf, api_ctx
)
if changed then
-- Step 4: 重新过滤插件列表
api_ctx.plugins = plugin.filter(api_ctx, route, api_ctx.plugins,
nil, "rewrite_in_consumer")
-- Step 5: 执行 Consumer 中新增插件的 rewrite 阶段
plugin.run_plugin("rewrite_in_consumer", api_ctx.plugins, api_ctx)
end
end
-- Step 6: 继续执行 access 阶段(包含 Consumer 的新插件)
plugin.run_plugin("access", plugins, api_ctx)
merge_consumer_route 详解
-- apisix/plugin.lua:697
local function merge_consumer_route(route_conf, consumer_conf, consumer_group_conf)
local new_route_conf = core.table.deepcopy(route_conf)
-- ★ 先合并 Consumer Group 的插件
if consumer_group_conf then
for name, conf in pairs(consumer_group_conf.value.plugins) do
if new_route_conf.value.plugins[name] == nil then
conf._from_consumer = true -- ★ 标记来自 Consumer
end
new_route_conf.value.plugins[name] = conf
end
end
-- ★ 再合并 Consumer 的插件(Consumer 覆盖 Consumer Group)
for name, conf in pairs(consumer_conf.plugins) do
if new_route_conf.value.plugins[name] == nil then
conf._from_consumer = true -- ★ 标记来自 Consumer
end
new_route_conf.value.plugins[name] = conf
end
return new_route_conf
end
1. Consumer Group 插件先合并,然后 Consumer 插件合并(Consumer 覆盖 Consumer Group 的同名插件)
2. 如果 Route 已有同名插件,Consumer 的同名插件会 覆盖 Route 的配置
3. Route 中没有的插件会被标记为
_from_consumer = true
rewrite_in_consumer:避免重复执行
当 Consumer 插件合并后,APISIX 需要执行新增插件的 rewrite 阶段,但不能重复执行已经执行过的路由插件。run_plugin() 中的逻辑处理了这一点:
-- apisix/plugin.lua:1150-1163
if phase == "rewrite_in_consumer" then
-- ★ 认证类插件跳过 consumer 阶段的 rewrite
if plugins[i].type == "auth" then
plugins[i + 1]._skip_rewrite_in_consumer = true
end
-- 实际调用 rewrite 函数
phase_func = plugins[i]["rewrite"]
end
-- ★ 非 Consumer 来源的插件跳过
if phase == "rewrite_in_consumer" and plugins[i + 1]._skip_rewrite_in_consumer then
goto CONTINUE
end
-- apisix/plugin.lua:521-523 (filter 中的标记)
-- 在 rewrite_in_consumer 阶段,非 consumer 来源的插件被标记跳过
if phase == "rewrite_in_consumer" and not plugin_conf._from_consumer then
plugin_conf._skip_rewrite_in_consumer = true
end
_meta.priority 自定义优先级
在路由级别覆盖插件的默认执行顺序
每个插件实例可以通过 _meta.priority 字段覆盖其默认的 priority 值,从而改变该路由中插件的执行顺序。
自定义优先级的排序逻辑
-- apisix/plugin.lua:80-82 — 自定义排序函数
local function custom_sort_plugin(l, r)
return l._meta.priority > r._meta.priority
end
-- apisix/plugin.lua:508-551 — filter() 中的自定义排序逻辑
if custom_sort then
-- ★ 为未设置 _meta.priority 的插件填充默认值
for i = 1, #plugins, 2 do
local plugin_conf = plugins[i + 1]
if not plugin_conf._meta then
plugin_conf._meta = core.table.new(0, 1)
plugin_conf._meta.priority = plugins[i].priority -- 用默认 priority
else
if not plugin_conf._meta.priority then
plugin_conf._meta.priority = plugins[i].priority
end
end
end
-- ★ 按 _meta.priority 降序重新排列
sort_tab(tmp_plugin_confs, custom_sort_plugin)
end
使用示例:让限流在认证之前执行
{
"uri": "/api/v1/heavy-endpoint",
"plugins": {
"key-auth": {
"_meta": {
"priority": 1000
}
},
"limit-count": {
"count": 10,
"time_window": 60,
"_meta": {
"priority": 3000
}
}
}
}
上面的配置将 limit-count 的优先级从默认的 1002 提升到 3000,使其在 key-auth(被降至 1000)之前执行。这在保护高负载端点时非常有用 —— 先限流再认证,避免认证操作消耗过多资源。
_meta.priority 只影响当前路由/全局规则中的执行顺序,不会影响其他路由。如果路由中 任何一个 插件设置了 _meta.priority,则该路由的 所有插件 都会按 _meta.priority 重新排序。
_meta.filter 条件执行
基于请求变量的动态插件开关
_meta.filter 允许插件根据请求变量动态决定是否执行。meta_filter()(apisix/plugin.lua:429)在每次插件执行前被调用:
local function meta_filter(ctx, plugin_name, plugin_conf)
local filter = plugin_conf._meta and plugin_conf._meta.filter
if not filter then
return true -- 没有 filter 配置,总是执行
end
-- ★ 使用 resty.expr 表达式引擎对请求变量求值
local ex, err = expr_lrucache(plugin_name .. ctx.conf_type .. ctx.conf_id,
ctx.conf_version, expr.new, filter)
local ok, err = ex:eval(ctx.var)
return ok -- true 则执行插件,false 则跳过
end
使用示例:仅对特定路径启用限流
{
"uri": "/api/*",
"plugins": {
"limit-count": {
"count": 10,
"time_window": 60,
"_meta": {
"filter": [
["uri", "~~", "^/api/v1/upload"]
]
}
}
}
}
上面的配置使 limit-count 只在请求路径匹配 ^/api/v1/upload 时才执行。
_meta.disable 禁用插件
在路由级别禁用特定插件实例
_meta.disable 可以在不删除插件配置的情况下禁用它。check_disable()(apisix/plugin.lua:84)在 filter() 阶段被调用:
local function check_disable(plugin_conf)
if not plugin_conf then
return nil
end
if not plugin_conf._meta then
return nil
end
if type(plugin_conf._meta) ~= "table" then
return nil
end
return plugin_conf._meta.disable -- ★ 返回 true 则跳过该插件
end
-- 在 filter() 中的使用
if not check_disable(plugin_conf) then
-- 插件未被禁用,加入执行列表
core.table.insert(plugins, plugin_obj)
core.table.insert(plugins, plugin_conf)
end
使用示例
{
"uri": "/api/debug",
"plugins": {
"limit-count": {
"count": 100,
"time_window": 60,
"_meta": {
"disable": true
}
}
}
}
插件配置仍然保留,但不会被执行。将 disable 改为 false 即可重新启用,无需重新配置参数。
Rewrite 阶段
Rewrite 阶段是插件执行的第一站
在路由插件的处理流程中,rewrite 是最先执行的阶段。大多数 认证插件 都在 rewrite 阶段工作,因为它们需要在业务逻辑之前完成身份验证:
-- rewrite 阶段的典型插件执行顺序(按 priority 降序):
-- 23000 real-ip → 获取客户端真实 IP
-- 12015 request-id → 生成请求追踪 ID
-- 12010 skywalking → 创建 Trace Span
-- 4000 cors → 处理 CORS 预检
-- 2520 basic-auth → Basic 认证
-- 2510 jwt-auth → JWT 认证
-- 2500 key-auth → API Key 认证
-- 1008 proxy-rewrite → 改写上游 URI/Host
-- 900 redirect → 重定向
Rewrite 阶段可中断请求
如果认证失败,插件返回 401 状态码,后续所有插件(包括 access 阶段的)都不会执行:
-- 例如 key-auth 插件返回 401
local code, body = phase_func(conf, api_ctx)
-- code = 401, body = {message = "Missing API key"}
if code or body then
if code >= 400 then
core.log.warn(plugins[i].name, " exits with http status code ", code)
if conf._meta and conf._meta.error_response then
body = conf._meta.error_response -- 可自定义错误消息
end
end
core.response.exit(code, body) -- ★ 中断整个请求处理
end
Access 阶段
Access 阶段:业务控制的核心阶段
Access 阶段在 Rewrite 和 Consumer 合并之后执行,此时 Consumer 信息已经可用。大多数 限流、访问控制、代理 插件在此阶段工作:
-- access 阶段的典型插件执行顺序:
-- 3000 ip-restriction → IP 黑白名单
-- 2999 ua-restriction → UA 黑白名单
-- 2400 consumer-restriction → Consumer 级别访问控制
-- 2000 authz-keycloak → Keycloak 授权
-- 1085 proxy-cache → 检查缓存命中
-- 1003 limit-conn → 并发连接限制
-- 1002 limit-count → 请求计数限流
-- 1001 limit-req → 请求速率限流
-- 966 traffic-split → 流量分割/灰度
-- 500 prometheus → 监控指标采集
-- -3000 ext-plugin-post-req → 外部插件后处理
Access 阶段与 Consumer 的关系
由于 Consumer 在 Rewrite 阶段完成认证后合并,Access 阶段可以使用 Consumer 信息做更精细的控制:
-- 执行时序:
-- 1. rewrite 阶段 → key-auth (priority: 2500) 识别出 consumer
-- 2. consumer 合并 → 将 consumer 的 limit-count 合并到路由
-- 3. rewrite_in_consumer → consumer 新增插件的 rewrite(如果有)
-- 4. access 阶段 → limit-count 根据 consumer 身份做个性化限流
-- → consumer-restriction 检查 consumer 权限
Header/Body Filter 阶段
响应处理阶段:不可中断
这两个阶段在上游返回响应后执行,用于修改响应头和响应体。它们通过 common_phase() 调度:
function _M.http_header_filter_phase()
-- 设置 Server 响应头
core.response.set_header("Server", ver_header)
-- ★ 全局规则 header_filter → 路由插件 header_filter
common_phase("header_filter")
end
function _M.http_body_filter_phase()
-- ★ 全局规则 body_filter → 路由插件 body_filter
common_phase("body_filter")
-- ★ 全局规则 delayed_body_filter → 路由插件 delayed_body_filter
common_phase("delayed_body_filter")
end
典型的 header_filter/body_filter 插件
# header_filter 阶段插件:
cors (4000): 修改 CORS 响应头
response-rewrite (899): 重写响应头和状态码
proxy-cache (1085): 设置缓存相关响应头
# body_filter 阶段插件:
response-rewrite (899): 改写响应体内容
gzip (995): 响应体压缩
brotli (996): Brotli 压缩
grpc-transcode (506): gRPC ↔ JSON 转码
ext-plugin-post-resp (-4000): 外部插件处理响应
Log 阶段
Log 阶段:请求处理的终点
Log 阶段在响应发送给客户端之后执行,用于日志记录和资源清理:
function _M.http_log_phase()
-- ★ 全局规则 log → 路由插件 log
local api_ctx = common_phase("log")
if not api_ctx then
return
end
-- 被动健康检查
healthcheck_passive(api_ctx)
-- ★ 资源清理
core.ctx.release_vars(api_ctx)
if api_ctx.plugins then
core.tablepool.release("plugins", api_ctx.plugins)
end
core.tablepool.release("api_ctx", api_ctx)
end
Log 阶段的插件执行顺序
# log 阶段插件(按 priority 降序):
error-log-logger (1091): Nginx 错误日志收集
limit-conn (1003): 释放连接计数
limit-count (1002): 更新计数器
prometheus (500): 指标上报
datadog (495): DataDog 监控
loki-logger (414): Loki 日志推送
elasticsearch-logger (413): ES 日志写入
http-logger (410): HTTP 日志回调
skywalking-logger (408): SkyWalking 日志
kafka-logger (403): Kafka 日志推送
syslog (401): Syslog 输出
file-logger (399): 文件日志
clickhouse-logger (398): ClickHouse 写入
limit-conn 和 limit-count 也有处理逻辑,用于释放连接计数和记录统计数据。它们的 log 阶段函数与 access 阶段函数不同,分别处理不同的逻辑。
案例:全局限流 UID 降级为 IP 的问题
问题背景
在实际业务中,我们使用 limit-req-advanced 插件做限流,支持按 UID(用户ID)和 IP 两个维度。UID 来自 bm-client-auth 鉴权插件设置的 X-BM-USER-ID 请求头。期望全局开启限流以减少逐路由配置的工作量,但发现一旦全局开启,UID 维度的限流就会降级为 IP 维度限流。
插件配置
# 全局开启(global_rules)
limit-req-advanced: # priority: 1000, phase: access
uid_rules: [...] # 需要读取 X-BM-USER-ID header
user_rules: [...]
# 路由维度开启
bm-client-auth: # priority: 3800, phase: rewrite/access
# 鉴权成功后设置:
# ctx.var.uid = login_token.userId
# ngx.req.set_header("X-BM-USER-ID", login_token.userId)
# 全局开启(global_rules)
bm-client-log: # priority: ?, phase: log
# 在 log 阶段读取 ctx.var.uid 记录日志
问题分析:为什么全局限流拿不到 UID?
关键在于全局插件和路由插件是两个独立的执行通道,在同一阶段内全局永远先于路由执行:
# ==================== access_by_lua ====================
# ---- 第一轮:全局插件 rewrite + access ----
[Global] limit-req-advanced (1000) → access
# 此时读取 X-BM-USER-ID → 为空!
# UID 为空 → 降级为 IP 限流 ❌
# ---- 第二轮:路由插件 rewrite + access ----
[Route] bm-client-auth (3800) → rewrite
# 鉴权成功,设置 X-BM-USER-ID = "12345" ✅
# 但为时已晚,全局限流已经执行完了
# ==================== log_by_lua ====================
# ---- 全局插件 log ----
[Global] bm-client-log → log
# 读取 ctx.var.uid = "12345" ✅ 有值!
# 因为 log 阶段在 access 阶段之后,bm-client-auth 早已执行
核心原理
同一阶段:全局先于路由
limit-req-advanced(全局/access阶段)先于 bm-client-auth(路由/rewrite阶段)执行。Priority 只在同一通道内排序,无法跨越全局与路由的边界。
跨阶段:生命周期保证顺序
bm-client-log 虽然也是全局插件,但它在 log 阶段执行。log 阶段在整个请求生命周期的最后,此时路由插件的 access 阶段早已完成,所以能读到 UID。
一图理解:阶段 vs 通道
全局通道 路由通道
┌──────────┐ ┌──────────┐
rewrite 阶段 │ │ → │bm-client- │
│ │ │auth(3800) │ ← 设置 UID
├──────────┤ ├──────────┤
access 阶段 │limit-req-│ → │ │
│advanced │ │ │
│(1000) │ │ │
│UID=空 ❌ │ │ │
├──────────┤ ├──────────┤
│ [请求发往上游,等待响应返回] │
├──────────┤ ├──────────┤
log 阶段 │bm-client-│ → │ │
│log │ │ │
│UID=有值 ✅│ │ │
└──────────┘ └──────────┘
※ 同一行(同一阶段):全局通道 先于 路由通道
※ 不同行(不同阶段):严格按生命周期从上到下
解决方案:Plugin Config 插件模板
核心思路
既然全局开启会导致限流先于鉴权执行,那就不要全局开启。利用 APISIX 的 Plugin Config(插件模板) 机制,将 bm-client-auth 和 limit-req-advanced 打包到同一个模板中,让它们都在路由维度执行。这样 Priority 就能正确控制执行顺序(3800 先于 1000),只需配置一次模板,多个路由引用即可。
方案对比
# ❌ 全局开启限流:UID 降级为 IP
# limit-req-advanced 在全局 access 阶段执行,bm-client-auth 还没跑
# ❌ 逐路由配置:太麻烦,每个路由都要重复配置一遍
# ✅ Plugin Config 插件模板:配置一次,多路由引用
# 两个插件都在路由维度执行,Priority 正常排序
步骤一:创建 Plugin Config
curl http://127.0.0.1:9180/apisix/admin/plugin_configs/1 \
-H "X-API-KEY: $admin_key" -X PUT -i -d '{
"desc": "鉴权 + 限流模板",
"plugins": {
"bm-client-auth": {
"_meta": { "priority": 3800 }
},
"limit-req-advanced": {
"policy": "redis",
"uid_rules": [...],
"user_rules": [...]
}
}
}'
步骤二:路由引用 plugin_config_id
# 每个路由只需要加一行 plugin_config_id,不用重复写插件配置
curl http://127.0.0.1:9180/apisix/admin/routes/1 \
-H "X-API-KEY: $admin_key" -X PUT -i -d '{
"uri": "/api/v1/*",
"plugin_config_id": "1",
"upstream_id": "1"
}'
使用 Plugin Config 后的执行流程
# ==================== access_by_lua ====================
# ---- 全局插件 ----
# (limit-req-advanced 已从全局移除,这里没有限流插件)
# ---- 路由插件 rewrite(来自 plugin_config_id: 1)----
[Route] bm-client-auth (3800) → rewrite ✅ 鉴权成功,设置 UID
[Route] limit-req-advanced (1000) → access ✅ 读到 UID,按用户维度限流
# 两个插件都在路由维度,Priority 正常排序:3800 > 1000
# bm-client-auth 先执行 → limit-req-advanced 后执行 → UID 有值 ✅
Plugin Config 合并规则
合并而非覆盖
如果路由同时有 plugins 和 plugin_config_id,两者的插件会合并。同名插件以路由直接配置的为准。
优先级顺序
Consumer > Consumer Group > Route > Plugin Config > Service。Plugin Config 的优先级低于路由直接配置,但高于 Service。
引用检查
删除 Plugin Config 前需要先解除所有路由的引用。如果引用了不存在的 plugin_config_id,请求会返回 503。
Dashboard 支持
在 apisix-dashboard 的"插件模板"页面创建模板,编辑路由时在第3步选择模板即可,无需手动调 API。
完整请求流转示例
一个真实场景的完整执行链路
假设我们有如下配置:一个全局规则启用了 prometheus 和 ip-restriction,一个路由启用了 key-auth、limit-count、proxy-rewrite、response-rewrite 和 http-logger,对应的 Consumer 上绑定了个性化的 limit-count。
配置
# 全局规则
Global Rule #1:
plugins:
prometheus: {}
ip-restriction:
whitelist: ["10.0.0.0/8"]
# 路由
Route #1:
uri: /api/v1/*
service_id: svc_1
plugins:
key-auth: {}
limit-count:
count: 1000
time_window: 60
proxy-rewrite:
uri: /backend$uri
response-rewrite:
headers:
set:
X-Custom: "hello"
http-logger:
uri: "http://log-server/log"
# Consumer
Consumer user_A:
plugins:
key-auth:
key: "my-secret-key"
limit-count:
count: 50
time_window: 60
完整执行流程
# ==================== access_by_lua ====================
# ---- 全局规则 rewrite 阶段 ----
[Global] ip-restriction (3000) → rewrite ✓ 白名单检查通过
[Global] prometheus (500) → rewrite (无 rewrite 函数,跳过)
# ---- 全局规则 access 阶段 ----
[Global] ip-restriction (3000) → access ✓ IP 允许访问
[Global] prometheus (500) → access (无 access 函数,跳过)
# ---- 路由插件 rewrite 阶段 ----
[Route] key-auth (2500) → rewrite ✓ 认证成功,识别出 consumer: user_A
[Route] proxy-rewrite (1008) → rewrite ✓ URI 改写: /api/v1/data → /backend/api/v1/data
[Route] limit-count (1002) → rewrite (无 rewrite 函数,跳过)
[Route] response-rewrite (899) → rewrite ✓ 记录需要修改的响应头
[Route] http-logger (410) → rewrite (无 rewrite 函数,跳过)
# ---- Consumer 合并 ----
Consumer user_A 的 limit-count 覆盖 Route 的 limit-count (count: 1000 → 50)
# ---- rewrite_in_consumer 阶段 ----
# key-auth 是 auth 类型插件,跳过
# limit-count 来自 consumer,但无 rewrite 函数,跳过
# ---- 路由插件 access 阶段 ----
[Route] key-auth (2500) → access (无 access 函数,跳过)
[Route] proxy-rewrite (1008) → access (无 access 函数,跳过)
[Route] limit-count (1002) → access ✓ 检查计数: 23/50,通过
[Route] response-rewrite (899) → access (无 access 函数,跳过)
[Route] http-logger (410) → access (无 access 函数,跳过)
# → 转发到上游服务 → 上游返回响应 →
# ==================== header_filter_by_lua ====================
# ---- 全局规则 header_filter ----
# (无 header_filter 插件)
# ---- 路由插件 header_filter ----
[Route] response-rewrite (899) → header_filter ✓ 设置 X-Custom: hello
# ==================== body_filter_by_lua ====================
# ---- 全局规则 body_filter ----
# (无 body_filter 插件)
# ---- 路由插件 body_filter ----
# (本例中无 body_filter 插件)
# ==================== log_by_lua ====================
# ---- 全局规则 log ----
[Global] prometheus (500) → log ✓ 记录请求指标
# ---- 路由插件 log ----
[Route] limit-count (1002) → log ✓ 更新计数器统计
[Route] http-logger (410) → log ✓ 发送日志到 http://log-server/log
总结与最佳实践
APISIX 插件执行顺序的完整规则
经过源码分析,我们可以将 APISIX 插件执行顺序总结为以下规则体系:
规则一:全局规则先于路由插件
-- 在每个 OpenResty 阶段中:
-- 1. 先执行全局规则的插件
-- 2. 再执行路由插件
-- 这个顺序是硬编码的,与 priority 无关
规则二:同一列表内按 Priority 降序执行
-- 全局规则内部:按 priority 降序
-- 路由插件内部:按 priority 降序(可被 _meta.priority 覆盖)
-- priority 值越大 → 越先执行 → 越接近客户端
规则三:阶段按 OpenResty 生命周期顺序
rewrite → (consumer merge) → rewrite_in_consumer → access
→ header_filter → body_filter → delayed_body_filter → log
规则四:插件合并的优先级
# 配置优先级(高 → 低):
# 1. Route 直接配置的插件(最高优先级)
# 2. Plugin Config 中的插件(仅补充 Route 未配置的)
# 3. Service 中的插件(被 Route 同名插件覆盖)
# 4. Consumer / Consumer Group 的插件(认证后动态合并,覆盖同名插件)
规则五:可中断阶段 vs 不可中断阶段
-- 可中断阶段(rewrite / access):
-- 插件返回 code >= 400 → 立即中断所有后续处理
-- 常见:认证失败 401、限流 429、IP 封禁 403
-- 不可中断阶段(header_filter / body_filter / log):
-- 所有插件都会执行,返回值被忽略
最佳实践
全局规则用于通用策略
将 prometheus 监控、IP 封禁、全局限流等放入全局规则,确保对所有请求(包括 404)生效
善用 _meta.priority
对高负载端点,可以用 _meta.priority 让限流在认证之前执行,减少认证带来的资源消耗
理解合并优先级
Route 配置 > Plugin Config > Service。Consumer 的同名插件会覆盖 Route 的配置,用于实现个性化策略
认证插件放在 Rewrite
APISIX 的设计要求认证插件在 rewrite 阶段完成,这样 access 阶段才能使用 Consumer 信息做精细控制
一图总结:APISIX 插件执行全景
HTTP Request
│
▼
┌──────────────────────────────────────────────────────┐
│ access_by_lua │
│ │
│ ┌─ Global Rules ──────────────────────────────────┐ │
│ │ rewrite: ip-restriction(3000) → ... │ │
│ │ access: ip-restriction(3000) → prometheus(500)│ │
│ └──────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─ Route Plugins ─────────────────────────────────┐ │
│ │ rewrite: key-auth(2500) → proxy-rewrite(1008) │ │
│ │ ↓ │ │
│ │ [Consumer Merge: user_A 的插件合并到路由] │ │
│ │ rewrite_in_consumer: (consumer 新增插件) │ │
│ │ ↓ │ │
│ │ access: limit-count(1002) → traffic-split(966)│ │
│ └──────────────────────────────────────────────────┘ │
│ ↓ │
│ → Upstream (上游服务) → │
└──────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ header_filter_by_lua │
│ Global Rules → Route: response-rewrite(899) │
└──────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ body_filter_by_lua │
│ Global Rules → Route: gzip(995), response-rewrite(899)│
└──────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ log_by_lua │
│ Global Rules: prometheus(500) │
│ Route: limit-count(1002) → http-logger(410) │
└──────────────────────────────────────────────────────┘
│
▼
HTTP Response → Client