Day 04 / 共 20 天 · 第 1 周 全景与请求生命周期

请求生命周期总览(全课最核心)

全课最核心的一天。精读 http_access_phaseinit.lua:602-758)——一个请求进来后 APISIX 做的所有决策。读懂这一个函数,就掌握了 APISIX 处理请求的主干。

📍 你在整条链的位置(放大 access 阶段)
Nginx 阶段 D2 access 阶段逐段拆(本课) api_ctx D5 路由/插件细节 W2-3
L01

入口总览:六步

http_access_phase (init.lua:602) ① 创建 api_ctx(table 池取)+ 设变量元表 ② 校验 HTTPS + uri 归一化(防攻击) ③ router.match 匹配路由(radixtree) ④ 合并 plugin_config/service/consumer ⑤ filter 选插件 → run_plugin(rewrite/access) ⑥ handle_upstream 交给上游 下面 L02-L07 逐段拆这六步(左列 ①②③ → 右列 ④⑤⑥)
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。这就是为什么有 rewriterewrite_in_consumer 两轮——Day 12/13/14 会各自细讲。
L07

⑥ 交给上游

init.lua:756handle_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 性能的钥匙。
← Day 03 Day 05 · api_ctx →