博客 理解 Envoy 的扩展与集成机制:从内置扩展到动态模块
这篇文章详细介绍了 Envoy 支持的多种扩展与集成机制,包括自定义的 C++、Lua 脚本、Wasm 插件、动态模块以及外部集成方式 ext_proc 和 ext_authz,并分享了我个人对动态模块的倾向与判断。
查看本文大纲
随着 Envoy 在云原生网络领域的广泛应用,越来越多开发者开始关注如何扩展它的功能。Envoy 提供了多种扩展机制,不同机制在性能、安全性、开发复杂度和兼容性方面各有差异。
这篇文章是我在调研 Envoy 扩展机制过程中的总结,希望能为你理解和选择合适的扩展方式提供一些参考。
两类机制:扩展 vs 集成
在讨论 Envoy 的“扩展机制”时,我们需要先明确一点:并非所有用于扩展功能的方式都算作 Envoy 的原生扩展(in-process extensions)。从架构上看,Envoy 的机制大致分为两类:
- 进程内扩展机制(in-process extensions):
- 自定义 C++ 过滤器
- Lua 脚本
- Wasm 插件
- 动态模块(Dynamic Modules)
- 进程外集成机制(out-of-process integrations):
- External Processing API(ext_proc)
- External Authorization API(ext_authz)
Ext_proc 和 ext_authz 本质上是通过 Envoy 提供的 gRPC/HTTP API 接口调用外部服务来实现请求处理逻辑,它们运行在独立进程中,并不注册为 Envoy 本地的过滤器链一部分。因此,严格来说,它们是“集成机制”而非“扩展机制”。
不过,为了更完整地覆盖用户常用的方式,在本文中我将它们一并纳入比较和讨论中。毕竟在实际使用中,它们和原生扩展一样,常用于处理 HTTP 请求和响应。
简要概览:六种机制各有优劣
每种机制的使用成本、性能表现、适用范围都不一样。下面是我对它们的简要归纳。
自定义 C++ 扩展:性能至上,但维护成本高
这是一种最原始也最强大的方式,把自定义逻辑直接写入 Envoy 的源码中编译。这种方式拥有最好的性能(包括零拷贝和低延迟),适合对性能极致要求的场景。但缺点是:你需要维护自定义构建流程,分发自己的 Envoy 二进制文件,升级成本高。
Lua 脚本:轻量、灵活、但有限的隔离机制
Lua 是一个成熟的扩展选项。在 LuaJIT 运行时中,它在与 Envoy 相同的进程中运行基于协程的脚本。它易于使用,不需要重新编译,并且可以内联到配置中。虽然 Lua 提供了某种程度的沙箱,但它缺乏 Wasm 或外部集成的强大隔离——Lua 脚本中的错误或资源耗尽可能会潜在地影响 Envoy 本身。因此,Lua 最适合可信环境或诸如头操作之类的轻量级任务。
Wasm 扩展:跨语言支持 + 沙箱隔离,但尚未成熟
Proxy-Wasm 提供了更现代的方式,可以用 Rust、Go 等语言编写逻辑,并以 WebAssembly 模块动态加载到 Envoy 中运行。它运行在沙箱 VM 中,有一定安全隔离,但当前生态仍不成熟,调试困难,性能略逊于 C++ 和动态模块,仍在不断演进中。
动态模块:原生替代与官方 Rust SDK
动态模块是 Envoy 从 v1.34 版本引入的一种扩展机制,允许开发者使用 Rust 或 Go 等语言(需使用 C ABI 编译)编写扩展,并在运行时以 .so
共享库的形式加载。尽管目前官方 SDK 仅提供 Rust 版本,示例仓库中也包含了一个实验性的 Go SDK,展示了未来支持多语言的潜力。与自定义 C++ 扩展相比,动态模块无需重编 Envoy 即可实现近乎原生的性能,非常适合那些对性能要求高、但又不愿维护 Envoy 分支的团队。
External Processing(ext_proc):功能强大但延迟高
ext_proc 提供了一个灵活的方式:你可以在外部服务中完全自定义对请求和响应的处理,包括访问和修改 Body 内容。它通过 gRPC 调用,对数据进行深度检查(如 DLP、病毒扫描等)很有用。但由于是进程外调用,它的性能显然无法与进程内机制相比。
External Authorization(ext_authz):轻量级认证过滤器
ext_authz 的功能类似,只不过它只处理请求路径,不能修改响应内容。典型场景是 OAuth2、JWT 鉴权、Header 检查等访问控制逻辑。它可以远程部署,不影响主流程,是最常用的认证机制之一。
对比表:各机制在不同维度下的能力评估
为了更直观地理解六种机制的异同,我整理了下表,从执行方式、性能、安全性、兼容性等维度进行对比:
维度 | 自定义 C++ 过滤器 | Lua 脚本 | Wasm | 动态模块 | ext_proc | ext_authz |
---|---|---|---|---|---|---|
实现方式/执行环境 | 原生 C++,编译入 Envoy | LuaJIT,进程内协程 | VM 中运行(V8/Wazero) | 共享库形式加载,Envoy 进程内执行 | gRPC/REST,进程外服务 | gRPC/REST,进程外服务 |
性能 | 极致性能,零复制 | 中等,低于 C++ 高于 Wasm | 中等,数据需进出 VM | 高,接近原生性能 | 较低,需跨网络栈复制 | 高效处理元数据,适合轻量鉴权 |
开发语言/SDK | C++ | Lua,无 SDK,使用 stream API | Rust 最佳,支持 Go/C++ | Rust 官方支持,也可用 Go 等构建 | 任意支持 gRPC/REST 的语言 | 任意支持 gRPC/REST 的语言 |
部署/加载方式 | 静态编译 | 配置内联或引用脚本 | 动态加载 Wasm 模块 | 动态加载 .so | 远程服务或 Sidecar 部署 | 可远程部署 |
安全性/信任模型 | 完全受信任 | 有限隔离机制,与 Envoy 等价信任 | 沙箱执行,隔离性好 | 与主进程共享内存,需完全信任 | 进程隔离,独立部署 | 进程隔离,独立部署 |
兼容性 | 与 Envoy 版本强耦合 | 依赖 Lua API 稳定性,兼容性尚可 | ABI 相对稳定,跨版本较友好 | 严格要求 ABI 匹配,版本锁定 | API 稳定,跨版本兼容性好 | API 稳定,跨版本兼容性好 |
适用场景 | 核心路径、流量关键路径 | 快速修改逻辑、请求头部调整 | 跨语言开发、安全执行、原型快速迭代 | 性能敏感 HTTP 扩展,不想自建构建链 | 响应检查、DLP、安全扫描 | 授权认证、用户访问控制 |
选择合适的机制
每种机制都有其优劣,最适合的选择取决于你的具体使用场景。你应综合考虑性能需求、开发资源和运维限制等因素来做出决策。与其偏爱某一种方式,不如理性评估各种权衡,从中选择最契合需求的方案。
例如,我最近对动态模块的研究显示,它们在扩展 Envoy 时具备良好的平衡性。动态模块提供接近 C++ 的执行性能,但无需重编 Envoy,适合那些既追求高性能,又不希望维护 Envoy 分支版本的团队。
与 Wasm 相比,动态模块直接在进程内运行——无需序列化、没有虚拟机开销、也没有内存沙箱,这使它们在处理请求头和请求体时具有天然优势。虽然目前仍属实验性特性,并且在不同版本间缺乏 ABI 兼容性,但在版本固定或可控的环境中,这种限制是可以接受和管理的。
动态模块只是众多扩展选项之一,其是否适合,还需结合你的具体需求来判断。深入理解各种机制的权衡,有助于你做出最符合实际运维需求的明智决策。
从扩展机制演进来看,我认为动态模块很可能会逐步替代大部分 Wasm 的使用场景,特别是在要求高性能和低调试复杂度的企业环境中。
在接下来的博客中,我会分享一个基于 Rust 构建动态模块的完整示例,演示如何快速编写、构建并在 Envoy 中加载一个 HTTP 过滤器,敬请关注。
总结:如何选择合适的机制?
我的建议是:
- 如果你对性能有极致要求,又希望避免维护自定义 Envoy 构建流程,动态模块是我最推荐的选择。它兼具高性能和灵活性,虽然目前仍处于实验阶段,但我认为它将逐步取代大多数 Wasm 的使用场景。
- 内置 C++ 扩展依然是极致性能下的终极方案,但维护成本高,适合掌控力强的团队。
- 如果你需要快速实现简单逻辑,Lua 脚本是最容易上手、部署最轻量的扩展方式。
- 对于关注安全隔离、希望跨语言开发的场景,Wasm 虽然设计先进,但在生产中的性能开销、调试复杂性和生态成熟度仍是现实挑战。相比之下,动态模块以更贴近“原生”的方式解决了这类问题。
- 若你希望将控制逻辑完全从 Envoy 中解耦,ext_proc 和 ext_authz 提供了可靠的集成方式,特别适合做认证、策略检查和请求外部处理。
每种机制都不是完美的银弹,理解它们背后的设计理念和权衡,才能做出最适合自己业务需求的选择。总之,如果你正在评估在 Envoy 中构建扩展的长期方案,我建议你密切关注动态模块的发展,并提前做好技术选型准备。
如你对某种扩展机制有实战经验或深入探索,也欢迎留言交流。欢迎关注我的博客 jimmysong.io,我会持续分享有关 Envoy、Istio 和服务网格的文章。