本文发布已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已不正确。
为 Envoy v2 构建 Kubernetes 边缘(Ingress)控制平面
Kubernetes 已成为基于容器的微服务应用程序的事实运行时,但仅此编排框架并不能提供运行分布式系统所需的所有基础设施。微服务通常通过 HTTP、gRPC 或 WebSockets 等第 7 层协议进行通信,因此能够在此层进行路由决策、操作协议元数据和观察至关重要。然而,传统的负载均衡器和边缘代理主要关注 L3/4 流量。这就是 Envoy Proxy 发挥作用的地方。
Envoy 代理是由 Lyft 工程团队从头开始设计为通用数据平面的,适用于当今以 L7 为中心的分布式世界,它对 L7 协议提供广泛支持、用于管理其配置的实时 API、一流的可观察性以及在小内存占用下的高性能。但是,Envoy 强大的功能集和操作灵活性也使其配置高度复杂 -- 从其丰富但冗长的控制平面语法中可以看出这一点。
通过开源的 Ambassador API 网关,我们希望解决在 Kubernetes 集群中将 Envoy 部署为面向前端的边缘代理的用例,并以 Kubernetes 操作员习惯的方式创建新的控制平面。在本文中,我们将介绍 Ambassador 设计的两个主要迭代版本,以及我们如何将 Ambassador 与 Kubernetes 集成。
2019 年之前的 Ambassador:Envoy v1 API、Jinja 模板文件和热重启
Ambassador 本身作为 Kubernetes 服务部署在容器中,并使用添加到 Kubernetes 服务的注释作为其核心配置模型。这种方法使应用程序开发人员能够管理路由作为 Kubernetes 服务定义的一部分。我们明确决定走这条路线,因为当前 Ingress API 规范存在限制,并且我们喜欢扩展 Kubernetes 服务的简单性,而不是引入另一种自定义资源类型。这里可以看到一个 Ambassador 注释的示例
kind: Service
apiVersion: v1
metadata:
name: my-service
annotations:
getambassador.io/config: |
---
apiVersion: ambassador/v0
kind: Mapping
name: my_service_mapping
prefix: /my-service/
service: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
将这个简单的 Ambassador 注释配置转换为有效的 Envoy v1 配置并非易事。按照设计,Ambassador 的配置不是基于与 Envoy 配置相同的概念模型 -- 我们有意希望聚合和简化操作和配置。因此,在概念集之间进行转换需要 Ambassador 内的大量逻辑。
在 Ambassador 的第一个迭代版本中,我们创建了一个基于 Python 的服务,该服务监视 Kubernetes API 中 Service 对象的更改。当检测到新的或更新的 Ambassador 注释时,会将这些注释从 Ambassador 语法转换为中间表示 (IR),该中间表示体现了我们的核心配置模型和概念。接下来,Ambassador 将此 IR 转换为代表性的 Envoy 配置,该配置保存为与正在运行的 Ambassador k8s Service 关联的 Pod 中的文件。然后,Ambassador“热重启”Ambassador Pod 中运行的 Envoy 进程,从而触发新配置的加载。
此初始实现有很多好处。所涉及的机制从根本上来说很简单,Ambassador 配置到 Envoy 配置的转换是可靠的,并且基于文件的热重启与 Envoy 的集成是可靠的。
但是,此版本的 Ambassador 也存在明显的挑战。首先,虽然热重启对于我们大多数客户的用例都有效,但它速度不是很快,并且一些客户(尤其是那些具有大型应用程序部署的客户)发现它限制了他们更改配置的频率。热重启也可能会丢失连接,特别是像 WebSockets 或 gRPC 流这样的长连接。
更关键的是,IR 的第一个实现允许快速原型制作,但它非常原始,以至于很难进行实质性更改。虽然这从一开始就是一个痛点,但随着 Envoy 转移到 Envoy v2 API,它变成了一个关键问题。很明显,v2 API 将为 Ambassador 提供许多好处 -- 正如 Matt Klein 在他的博客文章“通用数据平面 API”中概述的那样 -- 包括访问新功能和解决上述连接丢失问题,但也很明显,现有的 IR 实现无法实现飞跃。
Ambassador >= v0.50:Envoy v2 API (ADS)、使用 KAT 进行测试以及 Golang
经过与 Ambassador 社区的协商,Datawire 团队在 2018 年对 Ambassador 的内部结构进行了重新设计。这由两个关键目标驱动。首先,我们希望集成 Envoy 的 v2 配置格式,这将支持诸如 SNI、速率限制和 gRPC 身份验证 API 等功能。其次,由于 Envoy 配置的复杂性日益增加(尤其是在大型应用程序部署中运行时),我们也希望对 Envoy 配置进行更强大的语义验证。
初始阶段
我们首先按照多通道编译器的思路重构了 Ambassador 的内部结构。类层次结构更密切地反映了 Ambassador 配置资源、IR 和 Envoy 配置资源之间的关注点分离。Ambassador 的核心部分也经过重新设计,以便促进 Datawire 之外的社区贡献。我们决定采取这种方法有几个原因。首先,Envoy Proxy 是一个快速发展的项目,我们意识到我们需要一种方法,即看似微小的 Envoy 配置更改不会导致在 Ambassador 内进行数天的重新设计。此外,我们希望能够提供配置的语义验证。
当我们开始更紧密地使用 Envoy v2 时,很快就发现了测试挑战。随着 Ambassador 中支持的功能越来越多,Ambassador 在处理不太常见但完全有效的功能组合时出现了越来越多的错误。这促使了新的测试需求的产生,这意味着需要重新设计 Ambassador 的测试套件,以便自动管理多种功能组合,而不是依靠人工来单独编写每个测试。此外,我们希望测试套件能够快速运行,以最大限度地提高工程效率。
因此,作为 Ambassador 重新架构的一部分,我们引入了 Kubernetes 验收测试 (KAT) 框架。KAT 是一个可扩展的测试框架,它
- 将一组服务(以及 Ambassador)部署到 Kubernetes 集群
- 针对启动的 API 运行一系列验证查询
- 对这些查询结果执行一系列断言
KAT 旨在提高性能 -- 它预先批量进行测试设置,然后使用高性能客户端异步运行步骤 3 中的所有查询。KAT 中的流量驱动程序使用 Telepresence 在本地运行,这使得调试问题更容易。
将 Golang 引入 Ambassador 堆栈
在 KAT 测试框架到位后,我们很快遇到了 Envoy v2 配置和热重启的一些问题,这为我们提供了切换到使用 Envoy 的聚合发现服务 (ADS) API 而不是热重启的机会。这完全消除了在配置更改时重启的需求,我们发现这可能会导致在高负载或长连接下丢失连接。
但是,在考虑迁移到 ADS 时,我们面临一个有趣的问题。ADS 并不像人们预期的那么简单:在将更新发送到 Envoy 时,存在显式的排序依赖项。Envoy 项目具有排序逻辑的参考实现,但仅提供 Go 和 Java 版本,而 Ambassador 主要使用 Python。我们为此纠结了一段时间,并决定最简单的方法是接受我们世界的多语言特性,并使用 Go 进行我们的 ADS 实现。
我们还发现,使用 KAT,我们的测试已经达到了 Python 的许多网络连接性能受到限制的程度,因此我们也在这里利用了 Go,主要使用 Go 编写 KAT 的查询和后端服务。毕竟,当你已经冒险一试时,再添加一个 Golang 依赖项又算什么呢?
有了新的测试框架、生成有效 Envoy v2 配置的新 IR 和 ADS,我们认为 Ambassador 0.50 中的主要架构更改已经完成。唉,我们又遇到了一个问题。在 Azure Kubernetes 服务上,不再检测到 Ambassador 注释的更改。
通过与高度响应的 AKS 工程团队合作,我们能够确定问题所在——即 AKS 中的 Kubernetes API 服务器是通过一系列代理暴露的,这要求客户端更新以了解如何使用 API 服务器的 FQDN 进行连接,而 FQDN 是通过 AKS 中的一个变异 webhook 提供的。不幸的是,官方 Kubernetes Python 客户端不支持此功能,因此这是我们选择切换到 Go 而不是 Python 的第三个地方。
这就引出了一个有趣的问题,“为什么不抛弃所有的 Python 代码,直接用 Go 重写整个 Ambassador?”这是一个合理的问题。重写的主要担忧是,Ambassador 和 Envoy 在不同的概念层面上运行,而不是简单地用不同的语法表达相同的概念。确保我们用一种新的语言表达了概念桥梁并非易事,而且如果没有已经非常出色的测试覆盖率,就不应该着手进行。
目前,我们使用 Go 来覆盖非常具体、包含良好的功能,这些功能比我们验证完整的 Golang 重写更容易验证其正确性。未来,谁知道呢?但是对于 0.50.0 版本,这种功能拆分使我们既可以利用 Golang 的优势,又可以对 0.50 中已经进行的所有更改保持更大的信心。
经验教训
在构建 Ambassador 0.50 的过程中,我们学到了很多东西。以下是我们的一些主要收获:
- Kubernetes 和 Envoy 是非常强大的框架,但它们也是快速发展的目标——有时,除了阅读源代码并与维护人员交谈(幸运的是,他们都很容易联系到!)之外,没有其他替代方法。
- Kubernetes / Envoy 生态系统中支持最好的库是用 Go 编写的。虽然我们喜欢 Python,但我们不得不采用 Go,这样我们就不会被迫自己维护太多的组件。
- 有时,重新设计测试工具对于推进您的软件发展是必要的。
- 重新设计测试工具的真正成本通常在于将旧测试移植到新的工具实现中。
- 为边缘代理用例设计(和实现)一个有效的控制平面具有挑战性,来自 Kubernetes、Envoy 和 Ambassador 开源社区的反馈非常有用。
将 Ambassador 迁移到 Envoy v2 配置和 ADS API 是一个漫长而艰难的旅程,需要大量的架构和设计讨论以及大量的编码,但早期结果的反馈是积极的。Ambassador 0.50 现在已可用,因此您可以进行测试运行,并在我们的 Slack 频道或 Twitter 上与社区分享您的反馈。