Day 04 / 共 20 天 · 第 1 周 全景与请求生命周期
请求生命周期总览(全课最核心)
全课最核心的一天。精读 http_access_phase(init.lua:602-758)——一个请求进来后 APISIX 做的所有决策。读懂这一个函数,就掌握了 APISIX 处理请求的主干。
📍 你在整条链的位置(放大 access 阶段)
Nginx 阶段 D2→
access 阶段逐段拆(本课)→
api_ctx D5→
路由/插件细节 W2-3
L01
入口总览:六步
http_access_phase 六步:创建 ctx → 归一化 → 匹配路由 → 合并配置 → 选插件+跑插件 → 交给上游。
L02
① 创建 api_ctx
init.lua:608-614:
local api_ctx = core.tablepool.fetch("api_ctx", 0, 32) -- 从 table 池取,不是 new
ngx_ctx.api_ctx = api_ctx
core.ctx.set_vars_meta(api_ctx)
🤔 为什么从"table 池"取,不直接 new?
每秒几万请求,每个都 new 一个 table 再丢弃——GC(垃圾回收)压力巨大,拖慢延迟。
💡 对象池复用,避免海量 GC
请求开始从池里
fetch 一个 table 当 api_ctx,请求结束(log 阶段,Day 2)release 回池复用。api_ctx 是这个请求的"上下文袋子",后面所有阶段共享它(Day 05 细讲)。L03
② uri 归一化:安全细节
init.lua:622-651:按配置删末尾斜杠、servlet 风格归一化,并做安全处理:
-- 防止被不可信 request_uri 攻击:记录归一化后的 uri
api_ctx.var.real_request_uri = api_ctx.var.request_uri
api_ctx.var.request_uri = api_ctx.var.uri .. api_ctx.var.is_args .. (api_ctx.var.args or "")
为什么要归一化 uri?
同一资源可能有多种 uri 写法(结尾带不带
/、含 ..、编码变体)。不归一化,攻击者能用畸形 uri 绕过路由/鉴权。APISIX 先归一化成标准形式再匹配路由——既保证路由准确,又防"路径穿越"攻击。保留 real_request_uri 以便需要时取原始值。安全体现在这些不起眼处。L04
③ 匹配路由
init.lua:654-664:
router.router_http.match(api_ctx) -- radixtree 匹配,结果放 api_ctx.matched_route
local route = api_ctx.matched_route
if not route then
plugin.run_global_rules(api_ctx, apisix_global_rules.global_rules(), nil) -- 先跑全局规则
return core.response.exit(404, {error_msg = "404 Route Not Found"})
end
读法:
match 用 radixtree(前缀树,Day 06)按 uri/host/method 找匹配路由,放 matched_route。没匹配到也不是直接 404——先跑全局规则插件(比如全局日志/指标要记录未匹配请求),再返回 404。细致。L05
④ 合并 service / plugin_config
init.lua:670-706:路由可能引用可复用的 plugin_config 和 service,运行时合并进来:
if route.value.plugin_config_id then
route = plugin_config.merge(route, plugin_config.get(...)) -- 合并插件配置
end
if route.value.service_id then
route = plugin.merge_service_route(service_fetch(...), route) -- 合并 service(插件+上游)
end
🤔 为什么要"合并"?
Day 01 说 Service/Plugin Config 是"可复用零件"。
"复用 + 定制"靠合并解决
一条路由可以只写"我用 service X",X 里定义了插件和上游——运行时把 X 合并进这条路由,得到完整配置。100 条路由共享一个 service,改一处全生效。合并有优先级:路由自己的覆盖 service 的(Day 13 详讲)。合并后 route 就是这个请求最终生效的完整配置。
L06
⑤ 选插件 + consumer 二次合并(巧妙)
init.lua:716-753:
local plugins = plugin.filter(api_ctx, route) -- 选出该路由启用的插件
plugin.run_plugin("rewrite", plugins, api_ctx) -- 跑 rewrite 阶段(含鉴权,识别 consumer)
if api_ctx.consumer then -- 鉴权识别出了消费者
route, changed = plugin.merge_consumer_route(route, api_ctx.consumer, ...) -- 合并 consumer 专属配置
if changed then
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
plugin.run_plugin("access", plugins, api_ctx) -- 跑 access 阶段插件
"先鉴权识别身份、再按身份定制"
鉴权插件(key-auth)在 rewrite 阶段识别出"这是消费者 Alice",此时才知道 Alice 有没有专属配置(如给她单独的限流)。所以识别 consumer 后要重新合并配置、重新选插件、补跑 rewrite。这就是为什么有
rewrite 和 rewrite_in_consumer 两轮——Day 12/13/14 会各自细讲。L07
⑥ 交给上游
init.lua:756:handle_upstream(api_ctx, route, enable_websocket)——确定上游、设置转发。之后 Nginx 进入 balancer 阶段(Day 2)选实例转发,再 header_filter/body_filter/log。
💡 access 阶段结束,请求进入"转发+响应"环节
整个请求生命周期:access(决策,最重)→ balancer(选实例)→ filter(改响应)→ log(收尾释放 ctx)。其中 access 里的"匹配路由→合并配置→选插件→跑插件"是精华,值得反复看。
L08
今日小结 + 动手
🧠 今天你应该能回答
http_access_phase的六个主要步骤?- 为什么用 tablepool 而不直接 new api_ctx?
- uri 归一化防什么攻击?service 为什么要"合并"进路由?
- 为什么有 rewrite 和 rewrite_in_consumer 两轮插件运行?
✋ 动手
cd /Users/bitmart/work/codes/github/apisix
sed -n '602,758p' apisix/init.lua
明天预告 · Day 05:那个贯穿全程的
api_ctx 到底是什么?api_ctx.var.uri 的"变量惰性求值"魔法(用元表按需计算)、tablepool 复用、请求结束怎么回收。这是理解 APISIX 性能的钥匙。