Day 02 / 共 20 天 · 第 1 周 全景与请求生命周期
OpenResty 与 Nginx 阶段
昨天说"APISIX = 用 Lua 在 OpenResty 各阶段编程"。今天讲清这个"阶段"到底是什么——它是读懂 init.lua 的地图。理解了阶段,APISIX 的骨架就清晰了。
📍 你在整条链的位置
Day1 全景→
Day2 Nginx 阶段(地图)→
启动 D3→
请求生命周期 D4→
api_ctx D5
L01
为什么要懂"阶段"
🤔 一个请求进 Nginx,是"一口气跑完"吗?
不是。Nginx 处理一个请求,像工厂流水线——切成有序的几个"工位"(阶段):先读请求、再决定权限、再选后端、再改响应头、再改响应体、最后记日志。
💡 OpenResty 让你在每个"工位"挂一段 Lua
OpenResty 提供
access_by_lua、header_filter_by_lua、log_by_lua 等钩子——在 Nginx 的每个阶段插入你的 Lua 代码。APISIX 就是把它的网关逻辑分派到这些阶段的 Lua 里。所以 init.lua 里那一堆 http_xxx_phase 函数,就是各阶段的入口。懂了阶段,就懂了 APISIX 的骨架。L02
请求处理的流水线
Nginx 请求处理流水线:access(决策,最重)→ balancer(选后端)→ filter(改响应)→ log(收尾)。APISIX 顺着它编排。
对照昨天的"请求旅程"
Day1 的请求旅程图(路由→选插件→跑插件→负载均衡→上游)就发生在这些阶段里:路由+选插件+跑插件在 access 阶段,负载均衡在 balancer 阶段。和上一站 wasm-go 的"六阶段钩子"异曲同工——都是"请求生命周期分段处理"。
L03
init vs init_worker(master vs worker)
init.lua 里两个启动函数(Day 03 细讲):
_M.http_init(args)(:85):master 进程启动跑一次——全局一次性初始化(DNS、id、配置中心连接)。_M.http_init_worker()(:107):每个 worker 进程启动跑一次——起服务发现、balancer、admin、router、plugin、consumer 的 worker 初始化。
💡 Nginx = 1 主 + N worker 进程
master 管理、worker 真正处理请求(对应 CPU 核数)。
http_init 在 master 跑(全局初始化),http_init_worker 在每个 worker 跑(每个 worker 要自己的 etcd 连接、定时器)。和 Envoy "1 主 + N worker" 线程模型如出一辙——高性能服务器的通用套路。L04
access 阶段:90% 的活在这
_M.http_access_phase()(init.lua:602)是每个请求的主处理入口(Day 04 逐行读)。它干的:创建 api_ctx → 校验 TLS → 匹配路由 → 合并 service/consumer 配置 → 选插件 → 跑 rewrite/access 阶段插件 → 交给上游。
🤔 为什么最重的逻辑都堆在 access 阶段?
因为 access 是"请求已读完、还没转发上游"的时机——正好在这里做所有"决策":能不能过(鉴权)、走哪个后端(路由)、要不要改(改写)、限不限流。决策做完才转发。所以 APISIX 90% 的请求处理逻辑集中在这一个阶段函数里——它是全课最该精读的(Day 04)。
L05
balancer 为何单独一个阶段
_M.http_balancer_phase()(init.lua:993):
function _M.http_balancer_phase()
local api_ctx = ngx.ctx.api_ctx
if not api_ctx then return core.response.exit(500) end
load_balancer.run(api_ctx.matched_route, api_ctx, common_phase) -- 选上游实例
end
💡 balancer 阶段特殊:可能被调用多次
第一次选的上游实例连不上,Nginx 会自动重试,再次进入 balancer 选下一个实例。所以"选哪个后端 + 失败重试"的逻辑放在这个单独阶段。负载均衡不只是"挑一个",还含"挑失败了换一个"的容错(Day 10 详讲)。
L06
filter 与 log:改响应 + 收尾
响应阶段和收尾(都通过 common_phase 统一跑插件,init.lua:470):
function _M.http_header_filter_phase() -- :810 改响应头
core.response.set_header("Server", ver_header)
common_phase("header_filter")
end
function _M.http_log_phase() -- :959 收尾
local api_ctx = common_phase("log")
healthcheck_passive(api_ctx) -- 被动健康检查(据本次成败标记上游)
core.ctx.release_vars(api_ctx) -- ★ 释放上下文(回收 table,Day 05)
core.tablepool.release("api_ctx", api_ctx)
end
log 阶段的隐藏任务:回收内存
common_phase(name) 是各阶段的通用套路——跑全局规则 + 跑该阶段插件。而 log 阶段还负责"把 api_ctx 归还 table 池"——这是 APISIX 性能优化的关键(复用 table 减少 GC,Day 05 讲)。被动健康检查也在这(据这次请求 5xx 与否标记上游健康度)。L07
_M 怎么绑到 Nginx 阶段
init.lua 的 _M.http_xxx_phase 怎么和 Nginx 阶段挂上?答案在生成的 nginx.conf(Day 03)里:
access_by_lua_block { apisix.http_access_phase() }
header_filter_by_lua_block { apisix.http_header_filter_phase() }
body_filter_by_lua_block { apisix.http_body_filter_phase() }
log_by_lua_block { apisix.http_log_phase() }
balancer_by_lua_block { apisix.http_balancer_phase() }
Lua 的 _M 是什么?
Lua 模块惯例:
local _M = {} 是模块的"导出表",最后 return _M。_M.http_access_phase 就是这个模块对外的一个函数。nginx.conf 里 access_by_lua_block 调用它——这就是"Lua 函数挂到 Nginx 阶段"的绑定。所以读 APISIX = 读这些 _M.http_xxx_phase 在各阶段做了什么。L08
今日小结 + 动手
🧠 今天你应该能回答
- Nginx 处理请求为什么分"阶段"?OpenResty 怎么让你在阶段挂 Lua?
- init vs init_worker(master vs worker)的区别?
- 为什么最重的逻辑在 access 阶段?balancer 为什么单独(可重试)?
- log 阶段的隐藏任务(回收 api_ctx)?Lua 的 _M 怎么绑到 nginx.conf?
✋ 动手
cd /Users/bitmart/work/codes/github/apisix
grep -n "^function _M\." apisix/init.lua | grep phase
sed -n '993,999p' apisix/init.lua
grep -n "_by_lua_block" apisix/cli/ngx_tpl.lua | head
明天预告 · Day 03:启动流程——从
apisix start 命令,到根据 config.yaml 生成 nginx.conf、再到 http_init/http_init_worker 初始化各子系统的完整开机过程。