Day 05 / 共 20 天 · 第 1 周 全景与请求生命周期
core.ctx 与 api_ctx(性能的钥匙)
第 1 周收官。昨天贯穿全程的 api_ctx 到底是什么?今天拆 core/ctx.lua——尤其 api_ctx.var 那个用 Lua 元表实现的"惰性求值+缓存"魔法,以及 tablepool 复用。这是理解 APISIX 性能的钥匙。
📍 你在整条链的位置(第 1 周最后一块)
请求生命周期 D4→
api_ctx(贯穿全程的袋子)→
W2 路由/配置/上游
L01
api_ctx 是什么
🤔 一个请求经过多个阶段,怎么共享信息?
access、balancer、filter、log 是独立的 Lua 函数调用。它们怎么共享"这个请求匹配了哪条路由、识别出哪个 consumer"?
💡 api_ctx = 一个请求随身带的"档案袋"
access 阶段创建(
tablepool.fetch("api_ctx")),贯穿 balancer/filter/log,装着这个请求的一切:匹配的路由、consumer、plugins、上游配置、各种变量。各阶段开头 local api_ctx = ngx.ctx.api_ctx 取出来用。跟上一站 wasm-go 的 HttpContext 是同一概念。L02
ngx.ctx vs api_ctx:两层容器
init.lua:608-610:ngx_ctx.api_ctx = api_ctx。
OpenResty 的通用容器 + APISIX 的专用袋
ngx.ctx 是 OpenResty 提供的"每请求 Lua 表"(同一请求各阶段共享)。APISIX 在 ngx.ctx.api_ctx 里放自己的上下文。为什么不直接用 ngx.ctx?因为 ngx.ctx 有些跨阶段的坑(子请求、内部跳转),且 APISIX 想用 tablepool 精细管理自己的上下文。所以:ngx.ctx 是 OpenResty 的通用容器,api_ctx 是 APISIX 自己的、可池化复用的档案袋。L03
api_ctx.var 的元表魔法
core/ctx.lua:447-457 的 set_vars_meta 给 api_ctx 装上一个特殊的 var 表:
function _M.set_vars_meta(ctx)
local var = tablepool.fetch("ctx_var", 0, 32)
if not var._cache then var._cache = {} end
var._request = get_request(); var._ctx = ctx
setmetatable(var, mt) -- ★ 关键:装上元表 mt
ctx.var = var
end
💡 什么是"元表"(metatable)
Lua 的元表能"拦截"对一个表的操作。给
var 装元表后,当你写 api_ctx.var.uri——如果 var 里没有 uri 这个键,Lua 会调元表的 __index 函数来"临时计算"。这是下面"惰性求值"魔法的基础。元表是 Lua 实现"动态属性"的核心机制。L04
惰性求值 + 缓存(性能核心)
core/ctx.lua:277-300 的 __index(读 var.xxx 时触发):
读 var.xxx:先查缓存(命中直接返回)→ 未命中现算 → 存缓存。没用到的变量永不计算。
"按需 + 缓存"省了什么
一个请求有几十个变量(uri/host/remote_addr/args/各种 header),但一次可能只用几个。惰性求值 = "用到 var.uri 才算 uri、算完缓存",没用到的不算。加缓存,同一变量被多个插件读也只算一次。对每秒几万请求的网关,省下的计算很可观。你在插件里写
ctx.var.uri 就白享这套机制。L05
register_var:插件注册自定义变量
core/ctx.lua:433 的 register_var(name, getter, opts):让插件注册"自定义变量"。
📝 插件暴露自己算的值
core.ctx.register_var("a6_labels_zone", function(ctx)
return ctx.route and ctx.route.value.labels and ctx.route.value.labels.zone
end)
-- 之后能在配置/日志格式里用 $a6_labels_zone(像内置变量一样)读法:APISIX 把 Nginx 变量系统扩展了——插件能注册新变量,之后在路由/日志配置里像内置变量一样用(getter 在被读时惰性调用)。让插件优雅地"暴露自己算出的值"给配置系统。
L06
tablepool:GC 是性能隐形杀手
🤔 每秒几万请求,每个 new 几个 table,会怎样?
Lua 每创建一个 table 都分配内存,用完 GC 回收。海量分配/回收 → GC 周期性"停顿",拖慢延迟。
💡 tablepool 让 table 循环复用
预先攒一批空 table,用完还回去复用,而不是丢给 GC。请求 A 用完的 api_ctx 清空后给请求 B 用,几乎不产生新分配。这是 APISIX(和很多高性能 Lua 程序)压榨性能的关键手段。
L07
release 前必须 clear
请求结束(log 阶段,init.lua:981-990)回收:
core.ctx.release_vars(api_ctx) -- 清空 var 并归还 ctx_var 池
core.tablepool.release("plugins", api_ctx.plugins) -- 归还 plugins 池
core.tablepool.release("api_ctx", api_ctx) -- 归还 api_ctx 池
release_vars(ctx.lua:459-467):core_tab.clear(ctx.var._cache) 清空缓存 → 归还池。
归还前必须清空——否则串数据(严重 bug)
归还前必
clear(清空内容)——否则下个请求 fetch 到这个 table,会看到上个请求的残留数据(数据串了,是严重的 bug/安全问题)。log 阶段是请求最后一站,负责把所有池化对象清空归还。"fetch → 用 → clear → release"这个循环是 tablepool 正确性的核心。L08
今日小结 + 动手(第 1 周收官)
🧠 第 1 周你应该能回答
- api_ctx 是什么?和 ngx.ctx 的关系?
- 元表
__index怎么实现变量的惰性求值 + 缓存?省了什么? - tablepool 为什么能提性能?和 GC 什么关系?
- release 前为什么必须 clear?不 clear 会怎样?
✋ 动手
cd /Users/bitmart/work/codes/github/apisix
sed -n '277,320p' apisix/core/ctx.lua
sed -n '447,467p' apisix/core/ctx.lua
下周预告 · 第 2 周:进入数据面细节——radixtree 路由怎么高效匹配、config_etcd 怎么 watch 实现热更新、六大抽象对象、schema 校验、upstream 与负载均衡。Day 06 从路由讲起。