Day 17 / 共 20 天 · 第 4 周 扩展/可观测/构建

Wasm 扩展(Higress 的基础)

这是全课最关键的一课之一——因为 Higress 的插件生态就建立在 Envoy 的 Wasm 支持上。今天看 WebAssembly 插件怎么工作、proxy-wasm ABI、Context 双向翻译层。

📍 你在整门课的位置 · 第 4 周「扩展/可观测/构建」(D16 扩展要重编译 C++ → 今天看 Wasm 让插件运行时热插拔,Higress 生态的地基)
D16 扩展机制 D17 Wasm D18 可观测 D19 Buffer/Bazel D20 收官
L01

为什么要 Wasm

Wasm 解决"加插件要重编译 C++"的痛点 Day 16 说 Envoy 扩展是编译进二进制的——加个自定义过滤器要用 C++ 写、重新编译整个 Envoy(几十分钟到几小时),门槛极高。WebAssembly(Wasm)改变了这一切:你用 Go/Rust/C++ 写插件,编译成一个 .wasm 文件,Envoy 运行时动态加载它——不用重编译 Envoy、不用懂 Envoy C++ 内核、还能热更新插件。而且 Wasm 运行在沙箱里(安全隔离,插件崩了不影响 Envoy)。这就是 Higress "插件市场"的技术基础——用户用 wasm-go(本系列后面)写插件,Higress 动态加载。
💡 一句话:Wasm 插件 = 游戏机的"卡带" Envoy 是游戏主机(不用换主机),Wasm 插件是一张卡带——插上就能玩新游戏(新功能),拔下换一张就换功能,全程不用拆主机(不重编译 Envoy)。而且卡带在沙箱里运行,卡带崩了主机不受影响。用 Go/Rust/C++ 都能"烧录"一张 .wasm 卡带。
💡 proxy-wasm = 卡带的"USB 标准插头" 为什么任何语言写的插件都能插到任何支持的代理上?因为 proxy-wasm 规定了一套标准 ABI(就像 USB 接口标准):约定"代理怎么调插件"(onRequestHeaders 等钩子)和"插件怎么调代理"(getHeader/sendResponse 等宿主函数)。遵守这套插头标准,卡带就能到处插。host=Envoy 侧插座、SDK=给插件作者的接线库(wasm-go 就是 Go 版 SDK)、引擎(V8 等)=真正执行卡带字节码的读卡器。
📝 举个例子:一个"加请求头"的插件怎么跑 请求头到达 → Envoy decodeHeaders → Context 翻译成钩子 onRequestHeaders 通知插件 → 插件调宿主函数 addHeaderMapValue("x-user","alice") → Context 把它翻译成对 Envoy header 的真实写入 → 后端收到时就多了这个头。全程不用改 Envoy 一行 C++、不用重编译。
L02

proxy-wasm

Envoy 的 Wasm 支持基于 proxy-wasm(代理的 Wasm 规范)。higress-group 这个 fork 用的是 Higress 定制版依赖(bazel/repository_locations.bzl):proxy_wasm_cpp_host(宿主,来自 github.com/higress-group)、proxy_wasm_cpp_sdk(SDK)。运行时引擎有 v8/wamr/wasmtime/null 四种(source/extensions/wasm_runtime/)。

proxy-wasm 是"代理和插件之间的合同" proxy-wasm 定义了一套标准 ABI(应用二进制接口)——规定"代理怎么调插件"(onRequestHeaders 等钩子)和"插件怎么调代理"(getHeader/sendResponse 等宿主函数)。只要遵守这套合同,任何语言写的插件都能跑在任何支持 proxy-wasm 的代理上(Envoy、其他代理)。host(宿主)= Envoy 侧的实现;SDK = 给插件作者用的库(wasm-go 就是 Go 版 SDK)。引擎(v8 是 Google 的 JS/Wasm 引擎)负责真正执行 .wasm 字节码。
L03

Wasm filter 工厂

// source/extensions/filters/http/wasm/config.h(name="envoy.filters.http.wasm")
// createFilterFactoryFromProtoTyped 返回闭包:
return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) -> void {
  auto filter = filter_config->createContext();   // 每条请求创建一个 Context
  callbacks.addStreamFilter(filter);              // config.h:62 作为 HTTP filter
  callbacks.addAccessLogHandler(filter);          // config.h:63
};
读法:Wasm 就是一个普通的 HTTP 过滤器扩展(Day 16 的工厂模式)——REGISTER_FACTORY 注册、配置里写 envoy.filters.http.wasm 就启用。每个请求创建一个 Context(L04)。所以 Wasm 插件对 Envoy 来说就是"一个特殊的 L7 过滤器",无缝融入 Day 08 的过滤器链。
L04

Context 翻译层

// source/extensions/common/wasm/context.h:107
class Context : public proxy_wasm::ContextBase,   // proxy-wasm ABI 上下文
                public Http::StreamFilter,        // 同时是 Envoy HTTP filter
                public Network::Filter, ... { };
Context = "双面翻译官" Context 一身两职:对 Envoy 它是一个 Http::StreamFilter(Day 08 的 L7 过滤器);对 Wasm 插件它是 proxy_wasm::ContextBase(ABI 上下文)。它坐在中间做双向翻译:Envoy 的过滤器事件(decodeHeaders 等)→ 翻译成 proxy-wasm 的钩子(onRequestHeaders)调用插件;插件调宿主函数(getHeader)→ 翻译成操作 Envoy 的数据。就像同声传译:一边是 Envoy 的"语言",一边是 Wasm 插件的"语言",Context 让它们能对话。每个请求一个 Context 实例。
EnvoyHTTP 过滤器链decode/encode Context双面翻译官StreamFilter ⇄ContextBase Wasm 插件.wasm 卡带wasm-go 等 下行:decodeHeaders → onRequestHeaders(通知插件"头来了") 上行:getHeader / sendLocalResponse / httpCall(插件反过来操作 Envoy)
Context 坐在中间做双向翻译:下行(绿)把 Envoy 事件翻成插件钩子;上行(黄)把插件请求翻成对 Envoy 的操作。
💬 小白 vs 老师:插件的能力上限由谁决定? 👶 小白:Wasm 插件是不是想干啥都行?
👨‍🏫 老师:不是。插件被关在沙箱里,它能做的一切都取决于宿主暴露了哪些 host functions(上行 ABI)。宿主没给的能力,插件碰不到——这既是安全边界,也是为什么 Higress 要"加料"(L07 给插件开了 redisCall、setBuffer 等更多 host functions)。
⚠️ 常见误解:以为"下行/上行"是网络方向。其实这里指调用方向:下行 = 宿主(Envoy)调插件的钩子;上行 = 插件调宿主的函数。和请求/响应的流向无关。
L05

双向 ABI

下行(Envoy → Wasm,filter 钩子)decodeHeaderscontext.h:179)→ 调插件的 onRequestHeaders;decodeData/encodeHeaders/encodeData/log 同理。

上行(Wasm → 宿主,host functions):插件调这些,Context 实现:

getHeaderMapValue(:240)/ addHeaderMapValue(:238)  // 读写 header
getBuffer(:252)                                     // 读 body
sendLocalResponse(:217)                             // 直接返回响应
httpCall(:262)/ grpcCall(:281)                    // 外呼其他服务
defineMetric(:275)/ incrementMetric(:276)         // 指标(映射到 Envoy stats,Day 18)
读法:下行 = Envoy 通知插件"请求头来了、body 来了";上行 = 插件反过来操作 Envoy"给我这个 header、帮我发个 HTTP 请求、加个指标"。这套双向 ABI 就是 Wasm 插件的全部能力边界——插件能做什么,取决于宿主暴露了哪些 host functions。Buffer 桥接类零拷贝挂接 Envoy buffer(Day 19)。
L06

VM 与运行时

Wasmwasm.h:44)是一个 Wasm 执行实例;PluginConfigwasm.h:224)是配置载体,getOrCreateThreadLocalPlugin:216按线程惰性克隆 VM。运行时工厂 WasmRuntimeFactorywasm_runtime_factory.h:15)决定用哪个引擎(默认 V8)。

"按线程克隆 VM"呼应线程模型 还记得 Day 03 的线程模型吗?每个 worker 线程独立、无锁。Wasm 也遵守这个规矩:每个 worker 线程有自己的 Wasm VM 实例(getOrCreateThreadLocalPlugin 按线程克隆),插件状态线程隔离——不用加锁。这就是为什么 Wasm 能无缝融入 Envoy 的高性能架构。WasmRuntimeFactory 是又一个 Day 16 的工厂模式应用——V8/WAMR/Wasmtime 各是一个可选引擎实现(REGISTER_FACTORY(V8RuntimeFactory, WasmRuntimeFactory))。
L07

Higress 定制

higress-group fork 对 Wasm 做了增强(#if defined(HIGRESS)):setBuffer(改请求/响应体)、redisCallcontext.h:270,插件直接调 Redis)、getUpstreamHosts/setUpstreamOverrideHost:223/:224,插件干预负载均衡选主机)、VM 失败时按 FailurePolicy + 退避热重载。

这就是 Higress 插件比原生 Envoy Wasm 更强的地方:Higress 给 Wasm 插件开了更多 host functions(调 Redis、改 body、干预路由)。所以 Higress 的 Wasm 插件能做更丰富的业务逻辑——AI 网关的很多能力(如调用模型 API、改写请求)就靠这些定制 ABI。本系列后面的 wasm-go 就是用 Go 调用这套 ABI 写插件的 SDK。理解了今天的双向 ABI,就理解了 wasm-go 插件的运行原理。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • Wasm 解决 C++ 扩展的什么痛点?(重编译/门槛/热更新/沙箱)
  • proxy-wasm 是什么?host / SDK / 引擎各是什么?
  • Context 为什么是"双面翻译官"?
  • 双向 ABI:下行和上行各是什么方向?
  • Wasm VM 怎么呼应 Envoy 线程模型?Higress 定制了什么 ABI?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/envoy
sed -n '107,120p' source/extensions/common/wasm/context.h   # Context 多继承
grep -n 'getHeaderMapValue\|httpCall\|redisCall\|setUpstreamOverrideHost' source/extensions/common/wasm/context.h | head
ls source/extensions/wasm_runtime/
明天预告 · Day 18可观测性 + Admin——stats(无锁指标,呼应线程模型)、access log、tracing,以及 Admin 接口(/stats /clusters /config_dump)怎么内省运行状态。
← Day 16 扩展机制 Day 18 · 可观测性 + Admin →