1. 从一次线上故障说起:为什么我要啃 Higress 的源码
去年双十一大促前夜,我们一个核心交易链路的网关突然出现间歇性 503,平均每五分钟抖一次,每次持续十几秒。运维同学第一反应是后端服务扛不住,但查了服务端监控,CPU、内存、GC 全部正常,QPS 曲线平滑得像一条直线。问题最终定位到网关侧:某个 Wasm 插件在特定请求头组合下会触发一次同步的配置拉取,而这个拉取走的是控制面通道,一旦控制面响应慢,数据面的工作线程就被阻塞,连接池迅速打满,新的请求直接排队超时。
那次故障之后,我把 Higress 从数据面到控制面的代码翻了一遍。这篇文章就是那次“补课”的产物。Higress 是阿里开源的一款云原生 API 网关,底层数据面基于 Envoy,控制面兼容 Istio 的 xDS 协议,同时支持 Wasm 插件做能力扩展。它想解决的问题很明确:传统网关(比如 Nginx + Lua)在云原生场景下配置动态化能力弱、扩展成本高、和 K8s 生态割裂;而纯 Envoy 方案又太底层,业务团队上手门槛高。Higress 试图在“高性能数据面”和“易用的控制面”之间找一个平衡点。
这篇文章适合三类人看:一是正在做网关选型、想搞清楚 Higress 到底强在哪的架构师;二是已经在用 Higress、但遇到性能或插件问题不知道怎么排查的一线工程师;三是对 Envoy、Istio、Wasm 这套技术栈感兴趣、想通过一个真实项目把它们串起来理解的技术爱好者。我会尽量把原理讲透,同时把踩过的坑和实操细节都摊开来说,不玩虚的。
2. 整体架构拆解:Higress 到底由哪几块拼起来
2.1 数据面为什么选 Envoy 而不是自研
Higress 的数据面直接复用了 Envoy,这一点很多人第一反应是“那不就是套壳吗”。但真正做过网关的人知道,自研一个高性能数据面的成本极高。Envoy 经过多年打磨,在连接管理、负载均衡、熔断限流、可观测性这些方面已经非常成熟,尤其是它的线程模型和连接池设计,是经过大规模生产验证的。
Envoy 的核心线程模型是“多线程 + 每线程独立事件循环”。每个 worker 线程绑定一个 CPU 核,独立处理自己负责的连接,线程之间几乎不共享状态。这种设计的好处是避免了锁竞争,坏处是配置更新时需要每个线程各自处理。Higress 选择 Envoy,本质上是选择了一套已经被验证过的数据面基础设施,把精力集中在控制面和插件生态上。
但直接裸用 Envoy 有个问题:Envoy 的配置是静态的 xDS 协议,业务团队要改一个路由规则,得理解 Cluster、Listener、RouteConfiguration 这一整套概念,学习曲线陡峭。Higress 的价值就在于把这层复杂度封装掉了。
2.2 控制面与 Istio 的关系:兼容但不依赖
Higress 的控制面兼容 Istio 的 xDS 协议,这意味着它可以把 Istio 作为控制面来用,也可以用自己的控制面。这里有个常见的误解:很多人以为 Higress 必须依赖 Istio 才能跑。实际上 Higress 的控制面是独立的,它自己实现了一套配置管理逻辑,只是对外暴露的接口遵循 xDS 标准。
为什么这么做?因为 xDS 已经是云原生网关的事实标准,Envoy、Istio、gRPC 都在用。遵循这个标准,意味着 Higress 可以无缝接入现有的服务网格体系,同时又不被 Istio 的复杂度绑架。你可以只用 Higress 做南北向流量网关,也可以把它作为东西向网格的一部分,灵活性很高。
控制面的核心职责是把用户配置(K8s Ingress、CRD、控制台操作)转换成 Envoy 能理解的 xDS 配置,然后通过 gRPC 流推送给数据面。这个转换过程是 Higress 最核心的逻辑之一,后面我会专门拆解。
2.3 Wasm 插件机制:扩展性的关键落子
传统 Nginx 网关扩展靠 Lua 脚本,性能损耗大,而且 Lua 生态和云原生脱节。Higress 选择 Wasm 作为插件运行时,这是一个很有前瞻性的决定。Wasm 的优势在于:语言无关(可以用 C++、Rust、Go 写插件)、沙箱隔离(插件崩溃不影响主进程)、接近原生的性能。
Higress 的 Wasm 插件运行在 Envoy 的 Wasm 运行时里,通过 ABI 和 Envoy 交互。插件可以挂载在请求处理的各个阶段:请求头解析、请求体处理、响应头处理、响应体处理。这种细粒度的挂载能力,让插件可以做鉴权、限流、日志、协议转换等各种事情。
但 Wasm 也不是银弹。我实测下来,一个简单的鉴权插件,Wasm 版本的延迟比原生 C++ 插件高 10% 到 20%,比 Lua 版本低 30% 左右。这个损耗在大多数场景下可以接受,但在极致性能场景下需要权衡。
3. 配置流转全链路:一条路由规则是怎么生效的
3.1 从 K8s Ingress 到 xDS 配置的转换过程
假设你在 K8s 里创建了一个 Ingress 资源,声明了域名api.example.com转发到后端服务order-service。这条规则要生效,需要经过以下步骤:
第一步,Higress 的控制面监听 K8s API Server,感知到 Ingress 资源的创建事件。这里用的是 Informer 机制,不是轮询,所以感知延迟通常在毫秒级。
第二步,控制面把 Ingress 资源转换成内部的数据模型。这个模型比 K8s 原生 Ingress 更丰富,支持灰度、熔断、重试等高级配置。转换过程中会做校验,比如域名格式、后端服务是否存在、端口是否合法。
第三步,内部模型被翻译成 Envoy 的 xDS 配置。具体来说,域名和路由规则会生成 RouteConfiguration,后端服务会生成 Cluster,监听端口会生成 Listener。这一步是 Higress 最核心的逻辑,代码量也最大。
第四步,xDS 配置通过 gRPC 流推送给数据面。Envoy 收到配置后,会在所有 worker 线程中生效。这个过程是增量的,只推送变化的部分,不是全量刷新。
整个链路走下来,从 Ingress 创建到规则生效,实测延迟在 100 毫秒到 500 毫秒之间,取决于配置复杂度和数据面实例数量。
3.2 xDS 增量推送的底层逻辑
Envoy 的 xDS 协议支持全量推送和增量推送两种模式。全量推送是把所有配置重新发一遍,简单但浪费带宽,而且配置量大时会导致数据面短暂抖动。增量推送只发变化的部分,效率高但实现复杂。
Higress 默认用的是增量推送。它的实现逻辑是:控制面维护一份当前配置的快照,每次有变更时,计算新旧快照的差异,只把差异部分推送给数据面。Envoy 收到增量配置后,会做合并操作。
这里有个坑我踩过:如果增量推送的版本号管理出问题,数据面可能会处于一个“半新半旧”的状态,导致路由规则部分生效部分不生效。排查这种问题时,可以看 Envoy 的/config_dump接口,对比控制面的配置快照,看差异在哪里。
3.3 配置生效的时序问题与排查方法
配置生效不是瞬间完成的,它有一个时序:控制面生成配置 -> 推送 -> 数据面接收 -> 校验 -> 应用。任何一个环节出问题,都会导致配置不生效。
我整理了一个排查清单,按顺序检查:
| 排查步骤 | 检查方法 | 常见问题 |
|---|---|---|
| 控制面是否感知变更 | 看控制面日志 | Informer 未启动或权限不足 |
| 配置转换是否成功 | 看控制面错误日志 | 配置格式错误或校验失败 |
| xDS 推送是否成功 | 看控制面推送日志 | gRPC 连接断开或超时 |
| 数据面是否接收 | 看 Envoy 日志 | 版本号冲突或校验失败 |
| 配置是否应用 | 调/config_dump | 应用失败但无日志 |
提示:排查配置问题时,先确认控制面和数据面的版本号是否一致。版本号不一致是很多诡异问题的根源。
4. Wasm 插件深度剖析:从加载到执行的完整生命周期
4.1 插件加载机制与沙箱隔离原理
Higress 的 Wasm 插件加载有两种方式:一种是编译进镜像,随数据面一起启动;另一种是动态加载,通过控制面下发插件二进制。动态加载更灵活,但需要处理版本管理和依赖问题。
插件加载时,Envoy 会为每个插件创建一个独立的 Wasm 运行时实例。这个实例运行在沙箱里,和主进程的内存空间隔离。插件崩溃时,Envoy 会捕获异常并记录日志,不会导致整个数据面挂掉。这是 Wasm 相比原生插件最大的优势。
但沙箱隔离也有代价。插件和 Envoy 之间的数据交换需要通过 ABI 序列化和反序列化,这会产生额外的 CPU 开销。我实测过一个日志插件,Wasm 版本比原生版本多消耗约 15% 的 CPU。
4.2 请求处理各阶段的挂载点与执行顺序
Envoy 的请求处理分为多个阶段,Wasm 插件可以挂载在以下阶段:
onRequestHeaders:请求头解析完成后,此时可以修改请求头、做鉴权onRequestBody:请求体读取时,可以修改请求体、做内容校验onResponseHeaders:响应头生成后,可以修改响应头、做日志onResponseBody:响应体读取时,可以修改响应体、做内容替换
多个插件挂载在同一阶段时,执行顺序由插件优先级决定。优先级高的先执行。这里有个容易忽略的点:请求阶段的插件执行顺序和响应阶段是相反的。请求阶段是 A -> B -> C,响应阶段就是 C -> B -> A,类似栈的结构。
4.3 一个鉴权插件的完整实现与性能实测
我写过一个基于 JWT 的鉴权插件,逻辑很简单:从请求头里取 Token,校验签名和过期时间,校验通过就放行,不通过就返回 401。
核心代码逻辑如下(用 Rust 写,编译成 Wasm):
// 伪代码,展示核心逻辑 fn on_request_headers(&mut self) -> Action { let token = get_header("Authorization"); if token.is_empty() { return Action::Pause; // 返回 401 } match verify_jwt(token) { Ok(claims) => { set_header("X-User-Id", claims.user_id); Action::Continue } Err(_) => Action::Pause, } }性能实测数据:在 4 核 8G 的测试机上,单插件 QPS 约 12000,P99 延迟增加 0.8 毫秒。对比不加插件的基线,延迟增加约 12%。这个损耗在鉴权场景下完全可以接受。
注意:Wasm 插件里不要做阻塞操作,比如同步的 HTTP 请求或文件读写。一旦阻塞,会卡住整个 worker 线程,导致所有请求排队。需要外部调用时,用异步方式或者把逻辑放到控制面。
5. 性能调优实战:从参数到架构的优化路径
5.1 关键参数调优:连接池、线程数、缓冲区
Envoy 的性能调优参数很多,但真正影响大的就那么几个。我按重要性排序:
连接池配置。Envoy 和后端服务之间的连接池大小直接影响吞吐。默认配置偏保守,高并发场景下需要调大。关键参数是max_connections和max_pending_requests。我的经验值是:max_connections设为后端服务实例数乘以 20,max_pending_requests设为max_connections的 2 倍。
Worker 线程数。Envoy 默认使用所有 CPU 核作为 worker 线程数。但在容器环境里,如果 CPU limit 设得比实际核数小,会导致线程争抢。建议把 worker 线程数显式设置为 CPU limit 的值。
缓冲区大小。Envoy 的读写缓冲区默认是 32KB,大请求体场景下需要调大。但调太大又浪费内存,需要根据实际请求体大小分布来定。我一般设为 P99 请求体大小的 2 倍。
5.2 长连接场景下的内存与 GC 优化
长连接场景(比如 WebSocket、gRPC 流)对网关的内存管理要求很高。每个长连接都会占用一定的内存,连接数一多,内存增长很快。
Higress 在这方面的优化主要是连接复用和内存池化。Envoy 内部用内存池管理连接相关的对象,避免频繁分配释放。但 Wasm 插件如果自己管理内存,容易出问题。我踩过一个坑:插件里缓存了请求级别的数据,但没有在连接关闭时清理,导致内存泄漏。后来改成用 Envoy 提供的生命周期钩子来清理,问题才解决。
5.3 压测数据对比:Higress vs Nginx vs 原生 Envoy
我在测试环境做了一组对比压测,场景是简单的反向代理,后端是一个返回固定 JSON 的服务。测试机 8 核 16G,客户端用 wrk,持续压测 5 分钟。
| 网关方案 | QPS | P99 延迟 | CPU 使用率 | 内存占用 |
|---|---|---|---|---|
| Nginx | 28000 | 12ms | 65% | 120MB |
| 原生 Envoy | 25000 | 15ms | 72% | 180MB |
| Higress(无插件) | 24000 | 16ms | 75% | 220MB |
| Higress(带鉴权插件) | 21000 | 19ms | 82% | 260MB |
数据说明几个问题:Higress 相比原生 Envoy 有约 4% 的性能损耗,主要来自控制面交互和插件框架的开销。带插件后性能下降约 12%,这个损耗在可接受范围内。相比 Nginx,Higress 的 QPS 略低,但功能丰富度和动态化能力完全不是一个量级。
提示:压测时一定要用和生产一致的配置,包括 TLS、插件、日志级别。我见过有人用默认配置压测,结果上线后性能差一半,就是因为生产开了 access log 和 TLS。
6. 踩坑实录:那些文档里不会写的经验
6.1 配置不生效的五个常见原因
第一个原因,Ingress 的ingressClassName没配对。Higress 有自己的 IngressClass,如果 Ingress 资源没指定或者指定错了,控制面根本不会处理。
第二个原因,命名空间隔离。Higress 默认只监听特定命名空间的 Ingress,跨命名空间需要额外配置。
第三个原因,配置冲突。多条 Ingress 规则匹配同一个域名和路径时,优先级规则可能导致你预期的规则没生效。
第四个原因,xDS 推送失败但没报错。这种情况最坑,控制面日志显示推送成功,但数据面没应用。通常是版本号管理出了问题。
第五个原因,数据面缓存。Envoy 有配置缓存,极端情况下会用到旧配置。重启数据面可以强制刷新。
6.2 Wasm 插件内存泄漏的排查过程
前面提到过插件内存泄漏的坑,这里详细说一下排查过程。现象是数据面内存持续增长,每天涨 200MB 左右,重启后恢复。
排查步骤:先用 Envoy 的/memory接口看内存分布,发现 Wasm 运行时占用的内存一直在涨。然后逐个禁用插件,定位到是哪个插件的问题。最后看插件代码,发现是缓存没清理。
解决方法是在插件的on_connection_close钩子里清理缓存。这个钩子 Envoy 会保证调用,即使连接异常关闭也会触发。
6.3 灰度发布场景下的路由优先级陷阱
灰度发布是网关的高频场景,但路由优先级很容易踩坑。假设你有两条规则:一条是header: x-version = v2转发到 v2 服务,另一条是默认转发到 v1 服务。如果默认规则的优先级更高,灰度规则就永远不会生效。
Higress 的路由匹配是按优先级排序的,优先级高的先匹配。配置时一定要确保灰度规则的优先级高于默认规则。我建议用显式的优先级字段,不要依赖默认排序。
7. 插件开发进阶:从写一个到写好一个
7.1 插件性能优化的三个关键点
第一个关键点是减少 ABI 调用。每次插件和 Envoy 之间的数据交换都要走 ABI,这是性能瓶颈。优化方法是批量读取需要的头信息,而不是一个一个读。
第二个关键点是避免内存分配。Wasm 里的内存分配比原生慢,频繁分配会拖慢性能。尽量复用缓冲区,或者用栈上分配。
第三个关键点是异步化外部调用。插件里如果需要调用外部服务(比如鉴权中心),一定要用异步方式,不能阻塞。
7.2 插件配置热更新的实现方式
插件配置热更新是个常见需求。比如限流插件的阈值需要动态调整,不能重启数据面。
Higress 支持通过控制面下发插件配置。实现方式是:插件在初始化时注册一个配置回调,控制面配置变更时,回调被触发,插件更新内部状态。这个过程不需要重启插件,是真正的热更新。
但要注意,配置更新时如果有正在处理的请求,可能会出现新旧配置混用的情况。对一致性要求高的场景,需要加版本号或者锁机制。
7.3 多插件协同工作的注意事项
多个插件同时工作时,要注意执行顺序和状态共享。执行顺序前面说过,请求阶段和响应阶段相反。状态共享方面,插件之间不能直接通信,需要通过 Envoy 的元数据机制间接传递。
我遇到过一个场景:鉴权插件解析出的用户信息,需要在日志插件里使用。解决方案是鉴权插件把用户信息写到请求头里,日志插件从请求头读。这种方式简单但不够优雅,请求头会被传到后端服务。更好的方式是用 Envoy 的动态元数据,但配置起来更复杂。
8. 选型建议:什么场景该用 Higress,什么场景不该用
8.1 适合 Higress 的三类典型场景
第一类是 K8s 环境下的南北向网关。Higress 和 K8s 生态集成好,Ingress 资源直接转换,运维成本低。
第二类是需要动态扩展能力的网关。Wasm 插件机制让业务团队可以自己写插件,不用改网关核心代码。
第三类是需要和 Istio 服务网格协同的场景。Higress 兼容 xDS 协议,可以作为网格的入口网关。
8.2 不建议使用 Higress 的情况
如果你的场景是超高性能、超低延迟(比如 P99 要求 1 毫秒以内),Higress 的抽象层会带来额外开销,不如直接用 Nginx 或者原生 Envoy。
如果你的团队没有 K8s 环境,Higress 的优势发挥不出来,传统网关可能更合适。
如果你只需要最简单的反向代理功能,不需要动态配置和插件扩展,用 Higress 是杀鸡用牛刀。
8.3 从 Nginx 迁移到 Higress 的实操路线
迁移不是一蹴而就的,我建议分三步走:
第一步,并行运行。Nginx 和 Higress 同时在线,用 DNS 或者负载均衡把部分流量切到 Higress,验证功能。
第二步,功能对齐。把 Nginx 的配置逐条翻译成 Higress 的 Ingress 或 CRD,确保行为一致。重点检查重写规则、超时配置、限流策略。
第三步,全量切换。确认无误后,把流量全部切到 Higress,Nginx 保留作为回滚方案,观察一周后下线。
迁移过程中最容易出问题的是重写规则和正则匹配,两边的语法不完全一样,需要仔细测试。
9. 最后分享几个我常用的排查命令
排查 Higress 问题时,这几个命令我几乎每次都会用到:
查看 Envoy 当前配置:
kubectl exec -it <higress-pod> -- curl localhost:15000/config_dump查看 Envoy 统计信息:
kubectl exec -it <higress-pod> -- curl localhost:15000/stats查看 Wasm 插件状态:
kubectl exec -it <higress-pod> -- curl localhost:15000/wasm查看控制面推送日志:
kubectl logs <higress-controller-pod> -f | grep xds这几个接口在排查配置不生效、性能异常、插件问题时非常有用。尤其是/config_dump,能看到数据面实际生效的配置,是排查配置问题的第一手资料。
我个人在实际操作中的体会是,Higress 这套东西上手不难,但要用好需要理解它背后的 Envoy 和 xDS 机制。遇到问题时,不要只看 Higress 的日志,要往下钻到 Envoy 层面,很多答案都在那里。另外,Wasm 插件虽然灵活,但不要滥用,能用原生配置解决的问题就不要写插件,每多一层抽象就多一份性能损耗和排查成本。