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 级 CommonVmCtxplugin_wrapper.go:75):一个 Wasm 虚拟机一个,持有插件名、日志、所有回调函数、rebuild 参数。活得最久。
Plugin 级 CommonPluginCtx:551):一份配置一个,持有 RuleMatcher(规则)、tick 函数、userContext、Leader 状态。
请求级 HttpContextiface/context.go:41):一个 HTTP 请求一个,持有请求的 Scheme/Host/Path/Method + 一堆能力方法。请求结束就销毁。
用"公司"打比方 VM 级像"公司"(开着就一直在)、Plugin 级像"部门"(一个配置=一个部门)、HttpContext 像"一次会议"(一个请求=一次会议,开完就散)。你要存的数据活多久,就挂在对应级别:全公司共用的挂 VM/Plugin,只这次请求用的挂 HttpContext。挂错了要么泄漏(该销毁的没销毁)要么丢失(该保留的没了)。
三级层层嵌套:上级造下级,寿命一级比一级短 VM 级 CommonVmCtx(航站楼 · 进程在就在) Plugin 级 CommonPluginCtx(安检线配置 · 内嵌 RuleMatcher) 请求级 HttpContext ① 旅客A:Path/Method + SetContext 走完即销毁 请求级 HttpContext ② 旅客B:各自独立、互不串数据 走完即销毁
图注:一座航站楼(VM)里有若干安检线配置(Plugin),每条线同时处理很多旅客(HttpContext)。旅客A、B 各有独立上下文,所以并发也不串数据。
L02

VM 级 CommonVmCtx

plugin_wrapper.go:75-95(Day 03 见过)核心字段:pluginNamelogvmIDparseConfig、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 级。userContextSetContext/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 会精讲这些能力,今天先建立印象。

👶 小白:SetContextSetUserAttribute 都是存数据,有啥区别?

👨‍🏫 老师: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。
📝 举个例子:跨阶段传"用户名"(存哪、取哪) 请求头阶段: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 05SetCtx 与函数式选项——揭开 SetCtx("name", 选项...) 那些选项(ParseConfig/ProcessRequestHeaders…)的实现:CtxOption 接口 + Apply 模式,以及为什么有新旧两套 API。
← Day 03 Day 05 · SetCtx 选项 →