Higress 这个词第一次出现在我视野里,是某次排查线上网关的 Wasm 插件内存泄漏问题。当时团队用的还是 Nginx + Lua 那套方案,插件热更新要 reload,长连接抖动得厉害,业务方投诉了好几次。后来有人提了一句"要不看看 Higress",我才开始认真研究这个基于 Envoy 的云原生网关。说实话,刚上手的时候被它的分层架构绕得有点晕,Istio、Envoy、Wasm 这几个东西搅在一起,文档看了一遍还是没搞明白数据到底怎么流的。这篇文章就把我踩过的坑和理清的思路完整写出来,从控制面到数据面,从配置下发到 Wasm 插件加载,尽量把每个环节的"为什么"讲透。如果你也在做 API 网关选型,或者正在被 Envoy 的配置体系折磨,这篇应该能帮你少走点弯路。
1. 从 Nginx 到 Envoy:Higress 为什么选这条技术路线
1.1 传统网关方案的天花板在哪里
先说说为什么会有 Higress 这个东西。早些年做 API 网关,主流选择无非就是 Nginx + Lua(OpenResty)、Spring Cloud Gateway、Kong 这几类。Nginx 系性能确实好,C 语言写的核心,事件驱动模型,单机扛几万 QPS 不成问题。但问题也很明显:Lua 生态相对封闭,写复杂业务逻辑很痛苦,调试基本靠 print,而且插件热更新需要 reload worker 进程,虽然 reload 本身是平滑的,但在高并发场景下还是会有连接抖动。
Spring Cloud Gateway 是 Java 系,开发体验好,生态丰富,但性能上跟 Nginx 差一个量级,而且 JVM 的内存占用和 GC 停顿在网关这种延迟敏感的场景下比较吃亏。Kong 基于 OpenResty,插件生态做得不错,但它的数据面和控制面耦合比较紧,扩展性受限。
这些方案共同的瓶颈在于:配置模型和扩展机制不够云原生。Kubernetes 环境下,服务发现、配置变更、灰度发布这些需求,传统网关要么靠额外的适配层,要么干脆不支持。Higress 选择 Envoy 作为数据面,本质上是为了解决这个"云原生适配"的问题。
1.2 Envoy 作为数据面的核心优势
Envoy 是 Lyft 开源的 L7 代理,后来捐给了 CNCF。它的设计理念跟 Nginx 完全不同——Nginx 是"配置驱动",Envoy 是"API 驱动"。什么意思呢?Nginx 的配置是静态文件,改配置要 reload;Envoy 的配置是通过 xDS 协议动态下发的,运行时就能改,不需要重启。
这个差异在云原生场景下是决定性的。Kubernetes 里 Pod 随时在创建销毁,服务端点每秒都在变,如果每次变更都要 reload 网关,那根本没法用。Envoy 的 xDS 协议支持增量更新,配置变更通过 gRPC 流式推送,数据面收到后只更新变化的部分,连接不受影响。
Envoy 的另一个优势是过滤器链(Filter Chain)模型。请求进来后,会依次经过一系列 Filter,每个 Filter 可以做各种事情——鉴权、限流、日志、协议转换。这个模型非常灵活,Higress 的很多功能就是通过自定义 Filter 实现的。而且 Envoy 原生支持 Wasm,可以把用户写的 Wasm 插件动态加载到 Filter Chain 里,这就解决了扩展性问题。
1.3 Higress 的分层架构设计
Higress 的整体架构可以分成三层:
| 层级 | 组件 | 职责 |
|---|---|---|
| 控制面 | Istio Pilot / Higress Controller | 监听 K8s 资源,生成配置,通过 xDS 下发 |
| 数据面 | Envoy (Higress 定制版) | 接收配置,处理流量,执行 Filter Chain |
| 扩展层 | Wasm 插件 | 用户自定义逻辑,动态加载到 Envoy |
控制面用的是 Istio 的 Pilot 组件,但 Higress 做了不少定制。Istio 原生的 Pilot 主要是为 Service Mesh 场景设计的,配置模型比较复杂,Higress 简化了一部分,同时增加了对 Gateway API、Ingress 等资源的支持。
数据面是 Higress 自己维护的 Envoy 分支,加了一些定制化的 Filter,比如对 Dubbo、Sentinel 的原生支持。扩展层就是 Wasm 插件,这是 Higress 最有特色的部分,后面会详细讲。
注意:Higress 的控制面虽然基于 Istio,但并不是完整的 Istio。它只用了 Pilot 的配置下发能力,没有用 Istio 的 Sidecar 注入、mTLS 那些东西。如果你之前用过 Istio,不要把两者的概念混在一起。
2. 控制面到数据面的配置流转:xDS 协议到底怎么工作
2.1 xDS 协议族的核心概念
xDS 是一组协议的总称,包括 LDS(Listener Discovery Service)、RDS(Route Discovery Service)、CDS(Cluster Discovery Service)、EDS(Endpoint Discovery Service)等。每个协议负责下发一类配置:
- LDS:下发 Listener 配置,决定 Envoy 监听哪些端口、用什么协议
- RDS:下发 Route 配置,决定请求怎么路由到不同的 Cluster
- CDS:下发 Cluster 配置,定义上游服务的连接方式
- EDS:下发 Endpoint 配置,即 Cluster 里具体有哪些实例
这些配置之间有依赖关系:Listener 引用 Route,Route 引用 Cluster,Cluster 引用 Endpoint。Envoy 收到配置后,会按照这个依赖关系组装成完整的路由表。
Higress 的控制面会监听 Kubernetes 的 Ingress、Gateway、HttpRoute 等资源,把它们转换成 xDS 配置。比如你创建一个 Ingress,指定域名example.com路由到my-service:8080,控制面会生成对应的 Listener、Route、Cluster、Endpoint 配置,然后通过 gRPC 流推送给 Envoy。
2.2 配置下发的完整链路
我画不出图(平台限制),但可以用文字把链路描述清楚:
- 用户在 K8s 里创建或修改 Ingress/Gateway 资源
- Higress Controller 监听到资源变更事件
- Controller 把 K8s 资源转换成 Istio 的配置模型(VirtualService、DestinationRule 等)
- Istio Pilot 把配置模型转换成 xDS 资源
- Pilot 通过 gRPC 流把 xDS 资源推送给 Envoy
- Envoy 收到配置后,更新本地的路由表和 Cluster 信息
- 后续请求按照新配置进行处理
这个链路看起来简单,但实际排查问题时,每一层都可能出问题。比如 Ingress 写错了,Controller 可能不报错但生成不了配置;或者 Pilot 推送失败,Envoy 没收到更新。我遇到过好几次"改了 Ingress 但网关没生效"的情况,最后发现是 Controller 的缓存没刷新。
2.3 增量推送与全量推送的取舍
xDS 协议支持增量推送(Delta xDS)和全量推送(SotW,State of the World)。全量推送每次把完整的配置推给 Envoy,实现简单但数据量大;增量推送只推变化的部分,效率高但实现复杂。
Higress 默认用的是全量推送。为什么?因为增量推送虽然省带宽,但在配置依赖复杂的情况下容易出 bug。比如你删了一个 Cluster,但 Route 还引用着它,增量推送可能只推了 Cluster 的删除,Envoy 收到后不知道 Route 也要更新,就会报错。全量推送虽然浪费点带宽,但能保证配置的一致性。
不过全量推送也有问题:配置大了之后,每次推送都是几 MB 的数据,Envoy 解析配置也要时间。Higress 在这方面做了一些优化,比如对配置做分片、压缩,减少推送的数据量。
实操心得:如果你在排查配置不生效的问题,可以先看 Pilot 的日志,确认配置有没有推出去;再看 Envoy 的 config dump(通过 admin 接口
/config_dump),确认配置有没有收到。这两个地方一对,基本就能定位问题在哪一层。
3. Wasm 插件机制:Higress 扩展性的核心
3.1 为什么是 Wasm 而不是 Lua
前面说了,Nginx 系网关用 Lua 做扩展,Higress 用 Wasm。这个选择背后有几个考虑:
第一,安全性。Lua 脚本在 Nginx 里是直接执行的,能访问文件系统、网络等资源,一个恶意插件就能把整个网关搞挂。Wasm 是沙箱执行的,插件只能访问宿主暴露的接口,不能直接操作底层资源。
第二,多语言支持。Lua 只能用 Lua 写,Wasm 可以用 C++、Rust、Go、AssemblyScript 等多种语言编译。这对团队技术栈的兼容性更好。
第三,性能。Wasm 是编译执行的,比 Lua 的解释执行快。虽然 LuaJIT 也很快,但 Wasm 的性能上限更高。
第四,热更新。Wasm 插件可以在不重启 Envoy 的情况下动态加载和卸载,这对网关这种长连接场景非常重要。
Higress 的 Wasm 插件运行在 Envoy 的 Wasm 运行时里。Envoy 支持两种 Wasm 运行时:V8 和 WAMR。V8 性能好但内存占用大,WAMR 轻量但性能稍差。Higress 默认用的是 V8。
3.2 Wasm 插件的加载流程
Wasm 插件的加载流程大致是这样的:
- 用户把 Wasm 文件(.wasm)打包成 OCI 镜像,推送到镜像仓库
- 在 K8s 里创建 WasmPlugin 资源,指定镜像地址和插件配置
- Higress Controller 监听到 WasmPlugin 资源,拉取镜像,解压出 .wasm 文件
- Controller 把 .wasm 文件的内容通过 xDS 下发给 Envoy
- Envoy 收到后,用 Wasm 运行时加载插件,插入到 Filter Chain 的指定位置
- 请求经过时,Wasm 插件的钩子函数被调用
这个流程里,最关键的是第 4 步——.wasm 文件怎么通过 xDS 下发。xDS 协议本身是传配置的,不是传二进制文件的。Higress 的做法是把 .wasm 文件的内容编码成 base64,放在 xDS 配置的vm_config字段里。Envoy 收到后解码,加载到 Wasm 运行时。
这种方式有个问题:.wasm 文件通常有几 MB,每次配置更新都要传一遍,带宽浪费严重。Higress 的优化是只在插件首次加载或更新时传 .wasm 文件,后续配置变更只传插件配置(plugin config),不传 .wasm 本身。
3.3 Go 集成 Wasm 虚拟机的实践
最近有个热词是"go 集成 wasm 虚拟机",这其实跟 Higress 的 Wasm 插件开发有关。Higress 支持用 Go 写 Wasm 插件,通过 TinyGo 编译成 .wasm 文件。但 TinyGo 的生态不如标准 Go 完善,很多库用不了。
另一种方式是在 Go 程序里嵌入 Wasm 运行时(比如 Wasmtime、Wasmer),直接执行 .wasm 文件。这种方式适合在网关之外的地方复用 Wasm 插件逻辑,比如在本地测试插件行为。
我试过用 Wasmtime 的 Go binding 加载 Higress 的 Wasm 插件,基本流程是:
import ( "github.com/bytecodealliance/wasmtime-go" ) func main() { engine := wasmtime.NewEngine() module, _ := wasmtime.NewModuleFromFile(engine, "plugin.wasm") store := wasmtime.NewStore(engine) linker := wasmtime.NewLinker(engine) // 注册宿主函数 linker.FuncWrap(store, "env", "proxy_log", func() {}) instance, _ := linker.Instantiate(store, module) // 调用插件的导出函数 start := instance.GetExport(store, "_start") start.Func().Call(store) }这段代码只是演示基本流程,实际用的时候要处理很多细节,比如内存管理、ABI 兼容性等。Higress 的 Wasm 插件用的是 Proxy-Wasm ABI,宿主需要实现这套 ABI 定义的所有接口,插件才能正常工作。
踩坑记录:TinyGo 编译的 Wasm 插件在 V8 运行时里跑,有时候会遇到内存越界的问题。原因是 TinyGo 的 GC 和 V8 的内存管理机制不兼容。解决办法是尽量少在插件里分配大对象,或者用 Rust 写插件(Rust 的 Wasm 支持更成熟)。
4. 与 Istio 的关系:用多少,怎么用
4.1 Higress 用了 Istio 的哪些部分
Higress 基于 Istio 构建,但只用了 Istio 的一部分能力。具体来说:
- 用了:Pilot 的配置转换和 xDS 下发能力、Istio 的配置模型(VirtualService、DestinationRule、Gateway)
- 没用:Sidecar 注入、mTLS、AuthorizationPolicy、Telemetry 等 Service Mesh 相关功能
为什么这么设计?因为 Higress 的定位是 API 网关,不是 Service Mesh。API 网关的核心需求是南北向流量的管理——入口路由、鉴权、限流、协议转换。Service Mesh 的核心需求是东西向流量的管理——服务间通信、流量治理、可观测性。两者的场景不同,需要的功能也不同。
Higress 复用 Istio 的 Pilot,主要是为了省去自己实现 xDS 协议的工作量。Pilot 的配置转换逻辑很成熟,支持 Ingress、Gateway API、VirtualService 等多种配置源,直接拿来用比自己写划算。
4.2 不用 Sidecar 的网关模式
Istio 的经典模式是 Sidecar 注入——每个 Pod 里注入一个 Envoy Sidecar,拦截所有进出流量。这种模式对 Service Mesh 很合适,但对 API 网关来说太重了。
Higress 用的是独立部署模式——Envoy 作为独立的 Deployment 运行,不注入到业务 Pod 里。流量先到 Envoy,再由 Envoy 转发给后端服务。这种模式的好处是:
- 资源隔离:网关的资源消耗不影响业务 Pod
- 运维简单:网关升级不需要重启业务 Pod
- 性能更好:没有 Sidecar 的额外网络跳数
当然,独立部署也有缺点:网关成为单点,需要做高可用;网关和后端服务之间的网络延迟比 Sidecar 模式稍高。但对于 API 网关场景,这些缺点可以接受。
4.3 配置模型的兼容性与差异
Higress 支持多种配置源:
- Ingress:K8s 原生资源,适合简单路由
- Gateway API:K8s 新一代网关标准,表达能力强
- Istio VirtualService/DestinationRule:Istio 的配置模型,功能最丰富
- Higress 自定义 CRD:比如 McpBridge、Http2Rpc 等
这几种配置源的优先级和转换规则不太一样。比如 Ingress 的注解(annotation)会被转换成 Istio 的配置,但有些注解 Higress 不支持,会被忽略。Gateway API 的支持相对完整,但有些高级功能(比如流量镜像)还在开发中。
我个人的建议是:如果只是简单的路由需求,用 Ingress 就够了;如果需要灰度发布、流量镜像、熔断这些高级功能,用 VirtualService;如果追求标准化,用 Gateway API。不要混着用,容易出问题。
注意:Higress 的 Ingress 实现和 Nginx Ingress 有一些差异。比如 Nginx Ingress 的
nginx.ingress.kubernetes.io/rewrite-target注解,Higress 不支持,需要用higress.io/rewrite-target代替。迁移的时候要特别注意这些差异。
5. 实际部署中的性能调优与常见问题
5.1 Envoy 的线程模型与参数调优
Envoy 的线程模型是"主线程 + 多个 Worker 线程"。主线程负责配置更新、管理接口等,Worker 线程负责处理请求。每个 Worker 线程独立处理连接,互不干扰。
关键参数是--concurrency,决定 Worker 线程的数量。默认是 CPU 核数,但实际设置要看场景:
- CPU 密集型(比如大量 TLS 握手):concurrency 设为 CPU 核数
- IO 密集型(比如大量长连接):concurrency 可以适当降低,减少上下文切换
Higress 的 Envoy 默认配置是 concurrency = CPU 核数。如果网关的 CPU 使用率不高但延迟高,可以试试降低 concurrency,看看有没有改善。
另一个关键参数是--max-obj-name-len和--max-stats,控制统计信息的数量。Envoy 会为每个 Cluster、Route 生成统计信息,配置多了之后统计信息会爆炸,影响性能。Higress 在这方面做了一些优化,比如只统计关键的指标。
5.2 Wasm 插件的性能影响
Wasm 插件虽然灵活,但对性能有影响。每次请求经过 Wasm 插件,都要在 Wasm 运行时里执行一遍钩子函数,这个开销比原生 Filter 大。
实测数据:一个简单的鉴权插件(检查 Header 里的 Token),在 V8 运行时里执行,单次开销大约 0.1-0.2ms。如果 QPS 是 10000,那每秒要多花 1-2 秒的 CPU 时间。这个开销在高并发场景下不能忽略。
优化建议:
- 减少插件数量:能合并的插件尽量合并,减少 Filter Chain 的长度
- 用 Rust 写插件:Rust 编译的 Wasm 比 TinyGo 编译的小且快
- 避免在插件里做重操作:比如不要在插件里查数据库、调外部服务
- 用插件配置缓存:插件的配置(比如鉴权的白名单)可以在插件初始化时加载到内存,避免每次请求都读
5.3 常见问题排查清单
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 配置不生效 | Controller 没监听到资源变更 | 看 Controller 日志,确认有没有收到事件 |
| 配置不生效 | Pilot 推送失败 | 看 Pilot 日志,确认 xDS 推送有没有报错 |
| 配置不生效 | Envoy 没收到配置 | 看 Envoy admin 接口/config_dump |
| 请求 503 | Cluster 没有健康的 Endpoint | 看 Envoy admin 接口/clusters |
| 请求 404 | Route 配置不匹配 | 看 Envoy admin 接口/config_dump里的 Route 配置 |
| Wasm 插件不生效 | 插件加载失败 | 看 Envoy 日志,搜索 "wasm" 关键字 |
| 性能下降 | Wasm 插件开销大 | 用 Envoy 的 tracing 功能,看插件执行时间 |
这个清单是我在实际运维中总结的,大部分问题都能通过这几个步骤定位。关键是先确认配置有没有下发到 Envoy,如果配置都没到,那问题就在控制面;如果配置到了但行为不对,那问题就在数据面。
5.4 高可用部署的注意事项
Higress 作为网关,必须是高可用的。部署时要注意:
- 多副本:至少 2 个副本,避免单点故障
- 反亲和性:副本尽量分散到不同的节点,避免节点故障导致全部挂掉
- PodDisruptionBudget:设置 PDB,避免滚动更新时副本数降到 0
- 资源限制:设置合理的 CPU 和内存 limit,避免 OOM
- 健康检查:配置 liveness 和 readiness 探针,确保流量只打到健康的副本
我遇到过一个问题:Higress 的 Envoy 副本在滚动更新时,旧副本还没完全退出,新副本还没准备好,导致有几秒钟的 503。解决办法是配置preStop钩子,让旧副本在退出前先 sleep 几秒,等新副本准备好再退出。同时 readiness 探针要配置好,确保新副本真正能处理请求后才接入流量。
6. 从 Wasm 街机模拟器看插件的边界
6.1 一个有意思的实验
最近看到有人用 Wasm 写了个街机模拟器,跑在浏览器里。这让我想到:能不能把类似的 Wasm 模块加载到 Higress 里?理论上可以,因为 Higress 的 Wasm 运行时就是个通用的 Wasm 执行环境。但实际上,这么做没有意义——网关的 Wasm 插件是为了处理 HTTP 请求的,不是用来跑游戏的。
不过这个实验说明了一个问题:Wasm 的能力边界很宽。它不仅能做简单的请求处理,还能做复杂的计算。这意味着 Higress 的插件机制有很大的想象空间——比如在网关层做图像处理、数据转换、协议编解码等。
6.2 插件开发的边界与约束
虽然 Wasm 能力很强,但在 Higress 里写插件还是有一些约束:
- 不能阻塞:插件的钩子函数是同步执行的,不能做阻塞操作(比如同步 HTTP 请求)。如果需要调外部服务,要用异步方式。
- 内存有限:Wasm 运行时的内存是隔离的,不能访问宿主的内存。插件里的内存分配要谨慎,避免 OOM。
- ABI 兼容:插件必须实现 Proxy-Wasm ABI 定义的接口,否则宿主无法调用。
- 调试困难:Wasm 插件的调试比原生代码麻烦,日志输出要通过宿主转发。
这些约束决定了 Wasm 插件适合做什么、不适合做什么。适合做:Header 操作、鉴权、限流、日志、简单的数据转换。不适合做:复杂的业务逻辑、数据库操作、大量计算。
6.3 什么时候该用 Wasm,什么时候不该用
我的经验是:
- 用 Wasm:需要动态加载、多语言支持、沙箱隔离的场景
- 不用 Wasm:性能敏感、逻辑复杂、需要访问外部资源的场景
如果只是简单的 Header 改写,用 Envoy 原生的 Filter 就够了,没必要上 Wasm。如果逻辑复杂到需要查数据库,那应该在后端服务里做,而不是在网关层做。网关层的插件应该尽量轻量,只做跟流量处理直接相关的事情。
个人体会:我见过不少团队把网关当成了"万能层",什么逻辑都往插件里塞,最后插件越来越多,性能越来越差,排查问题也越来越难。网关的职责应该是"流量管理",不是"业务处理"。业务逻辑应该下沉到后端服务,网关只做它该做的事。
7. 写在最后的一些零散经验
Higress 这个项目我从去年开始跟进,踩过的坑不算少。有几个点我觉得值得单独拎出来说:
第一,不要被"Istio"这个词吓到。很多人一听 Higress 基于 Istio,就觉得太重、太复杂。实际上 Higress 只用了 Istio 的 Pilot 组件,没有用 Sidecar、mTLS 那些东西。部署起来比完整的 Istio 简单得多。
第二,Wasm 插件不是银弹。它解决了扩展性和热更新的问题,但引入了性能开销和调试复杂度。用之前要评估清楚,是不是真的需要 Wasm。如果只是简单的路由需求,用原生配置就够了。
第三,配置排查要有层次。从 K8s 资源到 Controller 到 Pilot 到 Envoy,每一层都可能出问题。排查的时候要一层一层往下看,不要跳步。我见过有人直接去看 Envoy 配置,结果发现是 Ingress 写错了,白折腾半天。
第四,性能调优要看场景。Envoy 的参数很多,但不是每个都需要调。先压测,找到瓶颈,再针对性调优。盲目调参可能适得其反。
第五,社区很重要。Higress 的社区虽然不如 Nginx 那么庞大,但活跃度不错。遇到问题可以在 GitHub 上提 Issue,响应速度还可以。文档也在不断完善,比早期好多了。
最后说一句:网关选型没有绝对的好坏,只有适不适合。Higress 适合云原生环境、需要动态扩展、团队有多语言背景的场景。如果你的场景是传统的虚拟机部署、需求简单、团队只熟悉 Nginx,那继续用 Nginx 也没什么不好。技术选型要务实,不要为了追新而追新。