Day 04 / 共 20 天 · 第 1 周 入门与生命周期
三级 Context:数据挂在哪
昨天知道有三级上下文,今天细看它们的字段和职责——以及最重要的:你在插件里想"暂存点数据"(比如请求开始时间、解析出的用户名),该挂在哪一级、活多久。
📍 你在 wasm-go 前 10 天的位置(W1 入门与生命周期 · W2 配置与匹配;W3-4 见 D11-20)
D01 全景·
D02 request-block·
D03 ABI·
D04 三级Context·
D05 SetCtx/选项→
D06 配置解析·
D07 RuleMatcher·
D08 运行时匹配·
D09 全局vs规则·
D10 生命周期→
W3-4 请求处理/高级
🤔 痛点:你在请求头阶段解析出了用户名,请求体阶段却"忘了"
直觉上你可能想拿一个全局变量
var currentUser string 存起来跨阶段用。但机场同时在放行成百上千名旅客(并发请求),大家共用一个全局变量 = 甲的用户名被乙覆盖,数据全串了。那到底该把"临时数据"挂在哪、它能活多久?这就是今天三级 Context 要回答的。💡 延续「机场」类比:三级 = 航站楼 / 安检线 / 一名旅客
VM 级 = 整座航站楼(开着就一直在,最久);Plugin 级 = 一条安检线的"今日配置"(一份配置一条线,持有规则匹配器);请求级 HttpContext = 一名旅客的这一次通行(进站到离站,走完就销毁)。数据该活多久,就挂对应级别:整楼共用挂 VM/Plugin,只这名旅客用挂 HttpContext——挂错级别不是泄漏就是丢失。
L01
三级总览
VM 级 CommonVmCtx(
plugin_wrapper.go:75):一个 Wasm 虚拟机一个,持有插件名、日志、所有回调函数、rebuild 参数。活得最久。Plugin 级 CommonPluginCtx(
:551):一份配置一个,持有 RuleMatcher(规则)、tick 函数、userContext、Leader 状态。请求级 HttpContext(
iface/context.go:41):一个 HTTP 请求一个,持有请求的 Scheme/Host/Path/Method + 一堆能力方法。请求结束就销毁。用"公司"打比方
VM 级像"公司"(开着就一直在)、Plugin 级像"部门"(一个配置=一个部门)、HttpContext 像"一次会议"(一个请求=一次会议,开完就散)。你要存的数据活多久,就挂在对应级别:全公司共用的挂 VM/Plugin,只这次请求用的挂 HttpContext。挂错了要么泄漏(该销毁的没销毁)要么丢失(该保留的没了)。
图注:一座航站楼(VM)里有若干安检线配置(Plugin),每条线同时处理很多旅客(HttpContext)。旅客A、B 各有独立上下文,所以并发也不串数据。
L02
VM 级 CommonVmCtx
plugin_wrapper.go:75-95(Day 03 见过)核心字段:pluginName、log、vmID、parseConfig、8 个 onHttpXxx 回调、rebuildAfterRequests/rebuildMaxMem/maxRequestsPerIoCycle。
它由 NewCommonVmCtx(:519-530)创建,通过 SetCtx 里的 proxywasm.SetVMContext 注册给 proxy-wasm。
读法:VM 级存的是"这个插件是什么样的"——名字、有哪些回调、性能参数。它是所有回调的"登记册",请求处理时从这里取出对应回调来执行。
L03
Plugin 级 CommonPluginCtx
plugin_wrapper.go:551-561:
type CommonPluginCtx[PluginConfig any] struct {
types.DefaultPluginContext
matcher.RuleMatcher[PluginConfig] // 内嵌规则匹配器(第2周主角)
vm *CommonVmCtx[PluginConfig]
onTickFuncs []TickFuncEntry // 注册的定时任务
userContext map[string]interface{} // 插件级自定义数据
fingerPrint string
ruleLevelIsolation bool
isLeader bool // Leader 选举结果(Day 17)
}
它内嵌了 RuleMatcher
注意
CommonPluginCtx 内嵌了 matcher.RuleMatcher——这意味着"配置匹配"能力直接长在 Plugin 级上下文里。因为"用哪份配置"是按请求匹配的,而配置属于 Plugin 级。userContext(SetContext/GetContext)让你在插件级存跨请求的数据(比如启动时算好的东西)。isLeader 用于多实例里选一个"领导"干独占的活(Day 17)。L04
NewPluginContext
plugin_wrapper.go:543-549:VM 级负责"生"出 Plugin 级:
func (ctx *CommonVmCtx[PluginConfig]) NewPluginContext(uint32) types.PluginContext {
return &CommonPluginCtx[PluginConfig]{
vm: ctx, // 反向持有 VM
userContext: map[string]interface{}{},
}
}
读法:proxy-wasm 加载配置时调
NewPluginContext 造一个 Plugin 上下文。它反向持有 vm——这样 Plugin 级能拿到 VM 级的回调。三级之间通过这种"上级造下级、下级持有上级引用"串起来。同理 Plugin 级会 NewHttpContext 造请求级(在文件后半部分)。L05
HttpContext 接口
iface/context.go:41-118 定义 HttpContext 接口,能力很丰富:
type HttpContext interface {
Scheme() string; Host() string; Path() string; Method() string // 请求基本信息
SetContext(key string, value interface{}); GetContext(...) ... // 请求级存取数据
GetUserAttribute(key) / SetUserAttribute(...) // 用户属性(Day 12)
WriteUserAttributeToLog() / ...ToTrace() // 写自定义日志/追踪
DontReadRequestBody() / DontReadResponseBody() // 不读 body
BufferRequestBody() / BufferResponseBody() // 缓冲 body
DisableReroute() // 禁止重新路由
RouteCall(method, url, headers, body, callback) // 调当前路由后端
HasRequestBody() / HasResponseBody() / IsWebsocket() / ... // 各种判断
}
读法:这一长串就是"你在处理请求时能做什么"的清单。请求级上下文不只是"存数据",还是"操作当前请求的遥控器"。Day 12 会精讲这些能力,今天先建立印象。
👶 小白:SetContext 和 SetUserAttribute 都是存数据,有啥区别?
👨🏫 老师:SetContext 是纯内部临时变量——只给你自己在阶段间传值用,请求结束就没了。SetUserAttribute 存的是要"露出去"的属性:它能被 WriteUserAttributeToLog/ToTrace 写进访问日志或链路追踪(Day 12 详讲)。一句话:只想自己传值用 Context,想让运维在日志里看到用 UserAttribute。
L06
SetContext 存数据
三级都有 SetContext/GetContext,但作用域不同:
- Plugin 级
userContext:跨请求存活(插件级共享)。 - 请求级
SetContext:只在这次请求内有效(还有类型化的GetBoolContext/GetStringContext/GetByteSliceContext)。
典型用法
请求头阶段解析出用户名,想在请求体阶段用——存请求级
ctx.SetContext("user", name),body 钩子里 ctx.GetStringContext("user", "") 取出。因为同一个请求的多个阶段共享同一个 HttpContext,这样就能在阶段间传递数据。切记别用全局变量传(并发请求会串数据!),一定用请求级 Context。📝 举个例子:跨阶段传"用户名"(存哪、取哪)
请求头阶段:
请求体阶段(同一 HttpContext):
与此同时旅客B 存的是
name := 从 Authorization 解析出 "alice" → ctx.SetContext("user", name)(挂到这名旅客的上下文)。请求体阶段(同一 HttpContext):
user := ctx.GetStringContext("user", "") → 得到 "alice"。与此同时旅客B 存的是
"bob",两人各取各的——因为用的是各自的请求级 Context,不是共享全局变量。⚠️ 常见误解:以为"反正每个 worker 一份 VM,用全局变量也够隔离了"。错——同一份 VM 会并发/交错处理它那条安检线上的多名旅客,全局变量在旅客之间照样互相覆盖。隔离到"请求"这一粒度,必须靠请求级
SetContext。L07
body 读取控制
HttpContext 提供精细的 body 读取控制(iface/context.go:61-96):
DontReadRequestBody()/DontReadResponseBody():不读 body(没注册 body 钩子时默认就不读)。BufferRequestBody()/BufferResponseBody():缓冲整个 body(没注册 streaming 钩子但注册了 body 钩子时默认缓冲)。SetRequestBodyBufferLimit(byteSize):单请求 body 缓冲上限(影响内存)。
为什么要这么在意 body?
请求/响应体可能很大(上传文件、大 JSON)。全缓冲进内存会吃掉网关大量内存。所以 SDK 让你精细控制:不用就别读、要读可选缓冲或流式、还能设上限。Day 02 的 request-block 就调了
DontReadRequestBody()——没有 body 规则时明确说"别读",省内存。这是写高性能插件的重要意识。L08
今日小结 + 动手
🧠 今天你应该能回答
- 三级 Context 分别活多久?"公司/部门/会议"的比喻?
- 为什么 CommonPluginCtx 内嵌 RuleMatcher?
- 阶段间传数据该用哪级 Context?为什么不能用全局变量?
- body 读取的几种控制(DontRead/Buffer/Limit)各解决什么?
✋ 动手
cd /Users/bitmart/work/codes/github/higress-group/wasm-go
sed -n '551,562p' pkg/wrapper/plugin_wrapper.go
sed -n '543,549p' pkg/wrapper/plugin_wrapper.go
sed -n '29,118p' pkg/iface/context.go
明天预告 · Day 05:SetCtx 与函数式选项——揭开
SetCtx("name", 选项...) 那些选项(ParseConfig/ProcessRequestHeaders…)的实现:CtxOption 接口 + Apply 模式,以及为什么有新旧两套 API。