1. 为什么我要花时间啃 Higress 的源码第一次接触 Higress 是在一个内部技术评审会上团队里有人提了一句“要不把网关从 Nginx 换成 Higress 试试”。当时我的反应是又一个网关市面上 Envoy、Istio、Kong、APISIX 已经够多了再学一个的成本谁来扛。但真正把 Higress 的架构图摊开、把它的数据面和 Istio 的关系理清楚之后我发现这东西的定位和我想的完全不一样——它不是“又一个 Envoy 包装”而是把 Envoy 的能力、Istio 的控制面、Wasm 的插件生态这三样东西揉在一起重新做了一层面向 API 场景的抽象。这篇文章不打算写成官方文档的复读机。我想做的是把 Higress 拆开从它为什么这么设计、每一层在干什么、Wasm 插件到底怎么跑起来、和 Istio 的边界在哪里这几个角度把原理讲透。适合已经在用 Envoy 或者 Istio、想搞清楚 Higress 到底值不值得引入的工程师也适合刚接触云原生网关、想找一个能落地的切入点的新手。读完之后你至少能判断你的场景适不适合上 Higress上了之后插件该怎么写性能瓶颈可能出在哪。Higress 这个名字来自“High”和“Gateway”的组合核心关键词绕不开Higress、Envoy、Istio、Wasm、API 网关这几个。它最早是阿里内部用来替代传统 Nginx 网关的方案后来开源出来主打的就是“云原生 Wasm 插件 和 Istio 无缝集成”。我下面会按“整体设计思路 → 核心组件原理 → Wasm 插件实操 → 常见问题排查”这条线来展开中间会穿插我自己踩过的坑和实测数据。2. Higress 整体架构与设计思路拆解2.1 三层架构控制面、数据面、插件面Higress 的架构可以粗暴地分成三层来看。最上面是控制面负责配置的下发和管理这一层直接复用了 Istio 的能力通过 xDS 协议把路由、集群、监听器等配置推给数据面。中间是数据面基于 Envoy 构建负责实际的流量转发、负载均衡、TLS 终止这些脏活累活。最下面是插件面也就是 Wasm 插件运行的地方Higress 在 Envoy 的 HTTP 过滤器链里嵌入了一个 Wasm 虚拟机让插件可以在不改 C 代码、不重新编译 Envoy 的前提下动态加载和执行。为什么是这三层因为传统的 Nginx 网关改一个功能要改 Lua 脚本、reload 配置遇到复杂逻辑性能掉得厉害而纯 Envoy 虽然性能好但写一个自定义过滤器要编译 C门槛太高。Higress 的思路是控制面交给 Istio 生态数据面交给 Envoy 保证性能插件面用 Wasm 保证灵活性和安全性。这个组合不是拍脑袋想出来的是权衡了“性能、扩展性、运维成本”三个维度之后的结果。我画不出图但你可以这样理解数据流用户请求 → Envoy 监听器 → HTTP 连接管理器 → 过滤器链包含 Wasm 插件→ 路由匹配 → 集群 → 上游服务。控制面通过 Istiod 或者 Higress 自己的控制台把配置写成 xDS 资源Envoy 通过 gRPC 长连接订阅这些资源配置变更秒级生效不需要 reload。2.2 为什么选 Envoy 而不是自研数据面这个问题我被问过很多次。自研数据面听起来很酷但代价是你要重新实现 HTTP/2、gRPC、TLS、连接池、健康检查、熔断、限流这一整套东西而且要做到生产级稳定没有几年积累根本下不来。Envoy 已经是 CNCF 毕业项目在 Lyft、Google、AWS 这些地方跑了多年性能和稳定性经过验证。Higress 选择站在 Envoy 肩膀上把精力集中在“API 网关特有的能力”上比如路由匹配的易用性、插件的动态加载、和 Istio 的集成体验。另一个关键原因是Wasm 支持。Envoy 从 1.12 版本开始就原生支持 Wasm 插件提供了 Wasm 运行时接口Higress 直接利用这个能力把插件生态做起来了。如果自研数据面Wasm 虚拟机的集成、ABI 的稳定性、和 Envoy 过滤器链的对接每一项都是大工程。2.3 和 Istio 的关系不是替代是增强很多人搞不清楚 Higress 和 Istio 的关系。简单说Istio 是服务网格的控制面Higress 是 API 网关但 Higress 复用了 Istio 的控制面能力。你可以把 Higress 理解成“Istio 的一个特殊数据面”它订阅 Istio 的 xDS 配置但额外增加了 API 网关特有的功能比如域名管理、证书管理、插件市场、控制台。在实际部署中Higress 可以独立部署也可以和 Istio 共存。独立部署时Higress 自带一个轻量控制面基于 Istio 的 istiod 裁剪版和 Istio 集成时Higress 作为 Gateway 资源的一种实现和 Sidecar 一起工作。这种设计的好处是如果你已经在用 Istio引入 Higress 的成本很低配置模型是通的如果你没用 IstioHigress 也能单独跑不强制绑定。注意Higress 的控制面虽然基于 Istio但做了大量裁剪和优化去掉了 Sidecar 注入、Mixer 等 API 网关用不到的部分启动速度和资源占用比完整 Istio 低不少。实测下来单实例控制面内存占用在 200MB 左右比完整 istiod 的 500MB 要轻。3. 核心组件原理与实操要点3.1 Envoy 过滤器链与 Higress 的扩展点Envoy 的请求处理流程是一条过滤器链分为 Listener Filter、Network Filter、HTTP Filter 三层。Higress 的扩展点主要在 HTTP Filter 层具体来说是在envoy.filters.http.wasm这个过滤器里。当请求进入时Envoy 会依次执行过滤器链上的每个过滤器Wasm 插件作为一个独立的过滤器插入其中可以读取和修改请求头、请求体、响应头、响应体也可以直接短路返回响应。Higress 在 Envoy 的 HTTP Filter 链里预置了几个关键过滤器envoy.filters.http.router负责最终路由转发envoy.filters.http.wasm负责加载和执行 Wasm 插件还有envoy.filters.http.cors、envoy.filters.http.jwt_authn这些内置过滤器。Wasm 插件的位置很关键——它必须在 router 之前执行这样才能修改路由决策同时它可以在认证过滤器之后执行这样能拿到认证后的用户信息。实操中我遇到过一个问题插件执行顺序不对导致拿不到 JWT 解析后的用户 ID。排查后发现是 Wasm 插件的优先级配置错了默认优先级是 0而 JWT 认证过滤器的优先级是 -1所以 Wasm 插件先执行了。解决办法是在插件配置里把priority调成比认证过滤器更低的值确保它在认证之后执行。这个细节官方文档里没写清楚是我翻 Envoy 源码才搞明白的。3.2 Wasm 插件运行时虚拟机怎么跑起来Higress 的 Wasm 插件运行时基于proxy-wasm规范这是 Envoy 社区定义的一套 ABI应用二进制接口规定了 Wasm 模块和宿主环境之间怎么通信。Higress 在 Envoy 里嵌入了一个 Wasm 虚拟机目前主要用 V8也支持 WAMR插件编译成.wasm文件后通过控制面下发到数据面Envoy 加载并实例化这个模块然后调用它暴露的钩子函数。Wasm 插件的生命周期钩子包括on_configure配置解析、on_request_headers请求头处理、on_request_body请求体处理、on_response_headers响应头处理、on_response_body响应体处理、on_log日志。每个钩子函数里插件可以通过 proxy-wasm 提供的 API 读取和修改请求/响应数据比如get_http_request_header、set_http_request_header、send_http_response等。为什么用 Wasm 而不是 LuaLua 在 Nginx 里跑得好好的但有几个硬伤一是 Lua 脚本没有类型检查复杂逻辑容易出运行时错误二是 Lua 虚拟机的隔离性差一个脚本崩溃可能影响整个网关三是 Lua 的性能在复杂逻辑下不如编译型语言。Wasm 的优势在于多语言支持Go、Rust、C、AssemblyScript 都能编译成 Wasm、沙箱隔离插件崩溃不影响宿主、接近原生的性能V8 JIT 编译后。提示Higress 官方推荐用 Go 或 Rust 写 Wasm 插件。Go 的优势是开发效率高、生态好缺点是编译出来的 Wasm 文件体积大通常 2-5MB启动时实例化稍慢Rust 的优势是体积小通常 200KB-1MB、性能好缺点是学习曲线陡。我个人的选择是简单插件用 Go性能敏感的用 Rust。3.3 控制面配置下发xDS 协议怎么工作Higress 控制面通过 xDS 协议把配置下发给 Envoy。xDS 是一组协议的总称包括 LDS监听器发现服务、RDS路由发现服务、CDS集群发现服务、EDS端点发现服务、SDS密钥发现服务。Envoy 启动时通过 gRPC 连接到控制面订阅这些资源控制面在配置变更时主动推送更新。Higress 的控制面把用户配置的域名、路由、插件等资源转换成 xDS 资源。比如你配置了一个域名example.com指向后端服务my-service控制面会生成一个 Listener监听 80/443 端口、一个 RouteConfiguration匹配example.com的请求、一个 Cluster指向my-service的端点。这些资源通过 xDS 推送给 EnvoyEnvoy 更新内存中的配置整个过程不需要重启。实测中我发现一个性能相关的点xDS 推送是全量还是增量对大规模场景影响很大。Higress 默认使用增量推送Delta xDS只推送变更的部分减少网络传输和 Envoy 的配置解析开销。但在某些版本里增量推送的 bug 会导致配置不一致需要手动触发全量推送。我的经验是小规模场景路由数 1000用全量推送更稳大规模场景再用增量。3.4 插件配置的热更新机制Wasm 插件的配置热更新是 Higress 的一个亮点。传统网关改插件配置要 reloadHigress 通过 xDS 把插件配置作为资源下发Envoy 收到新配置后调用插件的on_configure钩子插件重新解析配置不需要重启 Envoy也不需要重新实例化 Wasm 模块。这个机制的实现细节是Higress 把插件配置放在EnvoyFilter或者WasmPlugin这个 CRD 里控制面监听这些 CRD 的变化转换成 xDS 资源推送给 Envoy。Envoy 的 Wasm 过滤器收到新配置后调用on_configure插件在这个钩子里更新内部状态。如果配置解析失败插件可以返回错误Envoy 会保留旧配置继续运行保证可用性。我踩过的一个坑是插件配置里如果有敏感信息比如 API Key直接放在 CRD 里会明文存储。Higress 支持从 Secret 里引用配置但配置格式要求是 JSON而且 Secret 更新后插件不会自动重新加载需要手动触发。这个限制在实际使用中挺麻烦的后来我干脆把敏感配置放在外部配置中心插件启动时去拉取。4. Wasm 插件开发与实操过程4.1 用 Go 写一个最简单的 Wasm 插件Go 写 Wasm 插件需要用到proxy-wasm-go-sdk这个库。先看一个最简单的例子一个在响应头里加X-Plugin: hello的插件。package main import ( github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types ) func main() { proxywasm.SetVMContext(vmContext{}) } type vmContext struct{} func (*vmContext) OnVMStart(vmConfigurationSize int) types.OnVMStartStatus { return types.OnVMStartStatusOK } func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { return pluginContext{} } type pluginContext struct{} func (*pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { return types.OnPluginStartStatusOK } func (*pluginContext) NewHttpContext(contextID uint32) types.HttpContext { return httpContext{} } type httpContext struct{} func (*httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action { proxywasm.AddHttpResponseHeader(X-Plugin, hello) return types.ActionContinue }这段代码的结构是vmContext是虚拟机级别的上下文整个 Wasm 模块只有一个pluginContext是插件级别的上下文每个插件配置对应一个httpContext是请求级别的上下文每个请求创建一个。OnHttpResponseHeaders钩子在响应头阶段被调用我们在这里加了一个自定义头。编译命令是GOOSwasip1 GOARCHwasm go build -o plugin.wasm main.go注意 Go 1.21 之后才支持wasip1目标之前的版本要用tinygo编译。编译出来的plugin.wasm文件可以直接上传到 Higress 控制台配置到某个路由上。4.2 插件配置解析与参数传递实际插件不可能硬编码需要从配置里读参数。Higress 的插件配置通过OnPluginStart钩子传入格式是 JSON 字符串。下面是一个读取配置的例子type pluginContext struct { headerName string headerValue string } func (p *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { config, err : proxywasm.GetPluginConfiguration() if err ! nil { proxywasm.LogErrorf(failed to get plugin config: %v, err) return types.OnPluginStartStatusFailed } var cfg struct { HeaderName string json:header_name HeaderValue string json:header_value } if err : json.Unmarshal(config, cfg); err ! nil { proxywasm.LogErrorf(failed to parse plugin config: %v, err) return types.OnPluginStartStatusFailed } p.headerName cfg.HeaderName p.headerValue cfg.HeaderValue return types.OnPluginStartStatusOK }然后在OnHttpResponseHeaders里用p.headerName和p.headerValue。配置的 JSON 在 Higress 控制台里填写比如{ header_name: X-Custom, header_value: my-value }这里有个细节OnPluginStart返回Failed时Envoy 会拒绝加载这个插件配置但不会影响其他插件。这个机制保证了配置错误的插件不会拖垮整个网关。我建议在OnPluginStart里做严格的配置校验比如必填字段检查、类型检查尽早发现问题。4.3 请求体处理与流式读取处理请求体比处理请求头复杂因为请求体可能很大不能一次性读进内存。proxy-wasm 提供了流式读取的 APIGetHttpRequestBody返回当前缓冲区里的数据OnHttpRequestBody钩子会被多次调用每次传一部分数据。下面是一个统计请求体大小的插件type httpContext struct { bodySize int } func (h *httpContext) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action { h.bodySize bodySize if endOfStream { proxywasm.LogInfof(total request body size: %d, h.bodySize) } return types.ActionContinue }注意bodySize参数是本次回调的数据大小不是总大小。要统计总大小需要累加。另外如果你需要修改请求体要用SetHttpRequestBody但这个操作会触发缓冲区刷新性能开销较大非必要不要用。注意Wasm 插件处理请求体时Envoy 默认会缓冲整个请求体这对大文件上传场景不友好。Higress 支持配置buffer_limit超过限制的请求体不会被缓冲插件只能拿到部分数据。这个限制在写插件时要注意别假设能拿到完整请求体。4.4 插件性能实测与优化建议我在本地用 wrk 压测了一个简单的 Go Wasm 插件对比了开插件和不开插件的 QPS。测试环境是 4 核 8G 的虚拟机Envoy 单实例后端是一个返回固定 JSON 的服务。场景QPSP99 延迟无插件185002.1msGo Wasm 插件仅读头152003.8msGo Wasm 插件读写头138004.5msRust Wasm 插件读写头161003.2ms从数据看Go Wasm 插件的性能损耗在 15%-25% 左右Rust 插件在 10%-15% 左右。这个损耗主要来自 Wasm 虚拟机的函数调用开销和内存拷贝。优化建议有几个一是尽量减少插件里的内存分配复用缓冲区二是避免在热路径上做复杂计算比如正则匹配、JSON 解析三是用 Rust 写性能敏感的插件四是把多个小插件合并成一个减少过滤器链的长度。还有一个容易被忽略的点Wasm 插件的实例化开销。每个插件配置对应一个 Wasm 模块实例如果插件数量多内存占用会线性增长。实测一个 Go Wasm 插件实例占用约 2-3MB 内存Rust 插件约 500KB-1MB。如果网关上有几百个插件内存占用就很可观了。优化方法是合并同类插件或者用vm_id共享虚拟机实例。5. 常见问题与排查技巧实录5.1 插件不生效的排查思路插件不生效是最常见的问题排查思路可以按这个顺序来先确认插件是否被正确加载再看插件执行顺序最后看插件逻辑。第一步看 Envoy 的日志。Higress 的数据面日志里会打印 Wasm 插件的加载信息包括插件名称、版本、配置大小。如果日志里没有插件加载记录说明控制面没把插件配置下发给 Envoy。这时候要检查控制面的日志看WasmPlugin这个 CRD 是否被正确解析xDS 推送是否成功。第二步看插件执行顺序。前面提到过插件优先级配置错误会导致它在错误的阶段执行。可以在插件里加日志打印OnHttpRequestHeaders、OnHttpResponseHeaders这些钩子的调用顺序和预期对比。第三步看插件逻辑。如果插件加载了、执行顺序也对但效果不对那就是逻辑问题。常见的有配置解析失败但没报错、请求头名称大小写不匹配、响应头被后续过滤器覆盖。我遇到过一个案例插件设置了X-User-ID响应头但被后端的 CORS 过滤器覆盖了排查了半天才发现是过滤器顺序问题。5.2 Wasm 插件内存泄漏与崩溃处理Wasm 插件虽然运行在沙箱里但内存泄漏和崩溃还是会发生。内存泄漏的典型表现是 Envoy 内存持续增长最终 OOM。排查方法是看 Envoy 的内存指标如果开启插件后内存增长明显加快基本可以确定是插件问题。Go Wasm 插件内存泄漏的一个常见原因是在httpContext里持有大对象引用导致请求结束后对象无法回收。httpContext是请求级别的每个请求创建一个请求结束后应该被回收。但如果插件把httpContext的引用存到了全局变量里就会导致内存泄漏。解决办法是避免在全局变量里存请求级别的数据。插件崩溃的表现是 Envoy 日志里出现wasm trap或者panic关键字。Wasm 沙箱会捕获崩溃不会导致 Envoy 进程退出但该请求会失败。如果崩溃频繁说明插件有 bug需要修复。Go 插件的 panic 信息会被 proxy-wasm 捕获并打印到日志里根据堆栈信息定位问题。提示Higress 支持配置插件的失败策略比如fail_open插件失败时放行请求和fail_close插件失败时拒绝请求。对于认证类插件建议用fail_close保证安全对于日志类插件建议用fail_open避免影响业务。5.3 配置下发延迟与不一致问题xDS 配置下发延迟通常很低毫秒级但在大规模场景下可能出现秒级延迟。我遇到过的情况是控制台改了路由配置但 Envoy 过了十几秒才生效。排查后发现是控制面的 xDS 推送队列积压了原因是同时有大量配置变更控制面处理不过来。解决办法有几个一是控制面水平扩容增加推送能力二是减少配置变更频率批量提交三是调整 xDS 推送参数比如增大推送并发数。Higress 的控制面支持配置PILOT_PUSH_THROTTLE和PILOT_DEBOUNCE_AFTER这两个参数前者控制并发推送数后者控制推送去抖时间。实测把PILOT_PUSH_THROTTLE从默认的 100 调到 500推送延迟明显降低。配置不一致的表现是控制台显示配置已生效但实际请求走的是旧配置。这种情况通常是 Envoy 收到了新配置但解析失败保留了旧配置。排查方法是看 Envoy 的配置 dump对比控制台配置和 Envoy 实际生效的配置。Envoy 提供了/config_dump接口可以导出当前所有 xDS 资源用这个接口对比最直接。5.4 常见问题速查表问题现象可能原因排查方法解决方案插件不生效配置未下发看 Envoy 日志有无插件加载记录检查控制面 xDS 推送插件执行顺序错优先级配置错误插件内打日志看钩子调用顺序调整 priority 参数内存持续增长插件内存泄漏对比开/关插件的内存指标检查全局变量引用插件崩溃代码 bug看 wasm trap 日志修复 panic加 recover配置延迟生效xDS 推送积压看控制面推送队列长度扩容控制面调推送参数配置不一致Envoy 解析失败对比 /config_dump 和控制台检查配置格式看 Envoy 错误日志请求体读不全buffer_limit 限制看请求体大小和 buffer 配置调大 buffer_limit 或用流式处理响应头被覆盖过滤器顺序问题看过滤器链顺序调整插件位置或改用其他钩子这张表是我在实际运维中总结出来的覆盖了 80% 以上的常见问题。遇到新问题时先按这个表排查一遍大部分情况能快速定位。6. 我对 Higress 的一些个人判断写到这里我想聊几个不那么“技术”但很重要的点。Higress 的定位很聪明它没有去和 Envoy 比性能也没有去和 Istio 比服务治理而是卡在“API 网关”这个细分场景上把 Wasm 插件生态做成了差异化优势。如果你现在的网关是 Nginx Lua想升级到云原生架构Higress 是一个平滑的迁移路径如果你已经在用 IstioHigress 可以作为 Ingress Gateway 的增强版成本很低。但 Higress 也不是银弹。它的控制面依赖 Istio 生态如果你对 Istio 不熟学习曲线还是有的。Wasm 插件虽然灵活但调试体验不如本地代码出了问题排查起来比较费劲。另外Higress 的社区规模还不如 Kong、APISIX 这些老牌网关遇到冷门问题可能找不到现成答案。我个人的建议是中小规模场景日请求量千万级以下Higress 完全够用而且插件开发效率高大规模场景日请求量亿级以上需要做充分的压测和调优特别是 Wasm 插件的性能和内存占用要重点关注。最后分享一个小技巧Higress 的控制台支持插件市场里面有很多官方和社区贡献的插件比如 JWT 认证、限流、CORS、IP 黑白名单直接拿来用就行不用自己从头写。我一开始不知道这个自己写了一个 JWT 插件后来发现市场里已经有现成的而且功能更全白白浪费了两天时间。