使用 AppSwitch 对 Istio 进行分层

使用 AppSwitch 实现自动应用程序入驻和延迟优化。

2018 年 7 月 30 日 | 作者:Dinesh Subhraveti - AppOrbit 和哥伦比亚大学

边车代理方法带来了很多很棒的功能。边车位于微服务之间的直接数据路径上,可以准确地识别应用程序正在执行的操作。它可以监控和检测协议流量,不是在网络层的深处,而是在应用程序级别,以实现深层可见性、访问控制和流量管理。

但是,如果仔细观察,我们会发现,在执行应用程序流量的高价值分析之前,数据必须经过许多中间层。这些层中的大多数都是基础管道基础设施的一部分,它们的存在仅仅是为了将数据推送到下一个节点。在执行此操作时,它们会增加通信延迟并增加整个系统的复杂性。

多年来,人们在网络数据路径各层实施积极的细粒度优化方面做出了许多共同努力。每次迭代都可能减少几微秒。但人们从未质疑这些层本身的必要性。

不要优化层,而是移除它们

我相信,优化某些东西是删除其需求的糟糕的权宜之计。这是我最初在操作系统级虚拟化方面的工作(失效链接:https://apporbit.com/a-brief-history-of-containers-from-reality-to-hype/)的目标,该工作导致了 Linux 容器,它们有效地移除了虚拟机,通过直接在主机操作系统上运行应用程序,而无需中间访客操作系统。长期以来,该行业一直在错误地进行战斗,被优化虚拟机所分心,而不是完全移除额外的层。

我看到微服务的连接,以及网络的普遍模式都出现了这种现象。网络经历了物理服务器十年前经历的变化。正在引入一组新的层和结构。它们被深深地嵌入到协议栈甚至硅片中,却没有充分考虑低接触替代方案。也许有一种方法可以完全移除这些额外的层。

我一直思考这些问题,并相信可以将与容器概念类似的方法应用到网络栈中,从根本上简化应用程序端点之间的连接方式,而无需经过许多中间层的复杂性。我已经将容器的最初工作中的相同原则重新应用到 AppSwitch 的创建中。与容器提供应用程序可以直接使用的接口的方式类似,AppSwitch 直接插入应用程序当前使用的定义明确且无处不在的网络 API,并将应用程序客户端直接连接到相应的服务器,跳过所有中间层。最终,这就是网络的本质。

在深入探讨 AppSwitch 如何有望从 Istio 栈中移除不必要的层的细节之前,让我简单介绍一下其架构。更多详细信息可在文档页面上找到。

AppSwitch

与容器运行时类似,AppSwitch 由一个客户端和一个守护进程组成,它们通过 REST API 通过 HTTP 进行通信。客户端和守护进程都构建为一个独立的二进制文件,ax。客户端透明地插入应用程序并跟踪其与网络连接相关的系统调用,并将它们的发生情况通知守护进程。例如,假设应用程序对 Kubernetes 服务的服务 IP 发出connect(2)系统调用。AppSwitch 客户端拦截连接调用,将其置空,并将发生情况以及包含系统调用参数的一些上下文信息通知守护进程。然后,守护进程将处理系统调用,可能通过代表应用程序直接连接到上游服务器的 Pod IP 来处理。

重要的是要注意,AppSwitch 客户端和守护进程之间不会转发任何数据。它们被设计为通过 Unix 域套接字交换文件描述符 (FD),以避免复制数据。还要注意,客户端不是一个单独的进程。相反,它直接在应用程序本身的上下文中运行。应用程序和 AppSwitch 客户端之间也没有数据复制。

对栈进行分层

现在我们已经了解了 AppSwitch 的功能,让我们看看它从标准服务网格中优化掉了哪些层。

网络去虚拟化

Kubernetes 为其运行的微服务应用程序提供简单且定义明确的网络结构。但是,为了支持它们,它对底层网络施加了特定的要求。满足这些要求通常并不容易。添加另一层的常用解决方案通常被用来满足这些要求。在大多数情况下,附加层由位于 Kubernetes 和底层网络之间的网络覆盖网络组成。应用程序生成的流量在源头进行封装,并在目标处进行解封装,这不仅会消耗网络资源,还会占用计算核心。

由于 AppSwitch 通过其与平台的接触点来仲裁应用程序所看到的内容,因此它向应用程序呈现了一个与覆盖网络类似的底层网络一致的虚拟视图,但不会在数据路径上引入额外的处理层。为了与容器进行比较,容器内部看起来和感觉起来像一个虚拟机。但是,底层实现不会干预低级中断等高发生率的控制路径。

可以将 AppSwitch 注入标准 Kubernetes 清单(类似于 Istio 注入),这样应用程序的网络就会直接由 AppSwitch 处理,绕过底层网络覆盖网络。更多详细信息将在稍后提供。

容器网络的工件

将网络连接从主机扩展到容器一直是一个重大挑战。为实现这一目的,专门发明了新的网络管道层。因此,在容器中运行的应用程序仅仅是主机上的一个进程。但是,由于应用程序期望的网络抽象与容器网络命名空间公开的抽象之间的根本性错位,该进程无法直接访问主机网络。应用程序将网络视为套接字或会话,而网络命名空间则公开了一个设备抽象。一旦被放置在网络命名空间中,该进程就会突然失去所有连接。veth 对的概念和相应的工具的出现只是为了弥合这一差距。现在,数据必须从主机接口进入虚拟交换机,然后通过 veth 对进入容器网络命名空间的虚拟网络接口。

AppSwitch 可以有效地移除连接两端的虚拟交换机和 veth 对层。由于连接是由运行在主机上的守护进程使用主机上已经可用的网络建立的,因此不需要额外的管道来将主机网络桥接到容器中。在主机上创建的套接字 FD 被传递给在 pod 网络命名空间中运行的应用程序。当应用程序收到 FD 时,所有控制路径工作(安全检查、连接建立)都已经完成,并且 FD 已准备就绪以进行实际 I/O。

跳过用于协同定位端点的 TCP/IP

TCP/IP 是几乎所有通信都发生的通用协议介质。但是,如果应用程序端点恰好位于同一主机上,是否真的需要 TCP/IP?毕竟,它确实做了很多工作,而且相当复杂。Unix 套接字专为主机内通信而设计,AppSwitch 可以透明地切换通信以在协同定位端点之间通过 Unix 套接字进行。

对于应用程序的每个侦听套接字,AppSwitch 都维护两个侦听套接字,一个用于 TCP,另一个用于 Unix。当客户端尝试连接到恰好位于同一主机的服务器时,AppSwitch 守护进程将选择连接到服务器的 Unix 侦听套接字。每个端点上的生成的 Unix 套接字被传递到各自的应用程序。一旦返回了完全连接的 FD,应用程序将简单地将其视为一个位管道。协议并不重要。应用程序偶尔可能会进行协议特定的调用,例如getsockname(2),AppSwitch 将根据情况处理这些调用。它将提供一致的响应,以便应用程序可以继续运行。

数据推送代理

当我们继续寻找要移除的层时,我们也应该重新考虑代理层本身的需求。有时,代理的作用可能会退化为一个简单的“数据推送者”

在所有这些情况下,代理与任何低级管道层没有什么区别。事实上,由于代理无法获得相同的优化级别,因此引入的延迟可能要高得多。

为了用示例说明这一点,请考虑下面显示的应用程序。它由一个 Python 应用程序和一组位于其后面的 memcached 服务器组成。根据连接时间路由选择上游 memcached 服务器。这里速度是主要关注点。

Proxyless datapath
对延迟敏感的应用程序场景

如果我们查看此设置中的数据流,Python 应用程序将建立与 memcached 的服务 IP 的连接。它被重定向到客户端侧的边车。边车将连接路由到 memcached 服务器之一,并在两个套接字之间复制数据 - 一个连接到应用程序,另一个连接到 memcached。服务器端在服务器端边车和 memcached 之间也会发生同样的情况。此时,代理的作用只是在两个套接字之间乏味地推送位。但是,它最终会给端到端连接带来相当大的延迟。

现在,让我们假设应用程序以某种方式直接连接到 memcached,那么可以跳过两个中间代理。数据将直接在应用程序和 memcached 之间流动,没有任何中间跳跃。AppSwitch 可以通过在 Python 应用程序发出connect(2)系统调用时透明地调整目标地址来实现这一点。

无代理协议解码

事情将变得有点奇怪。我们已经看到代理可以绕过不涉及查看应用程序流量的情况。但是对于其他情况,我们还能做些什么呢?事实证明,可以。

在微服务之间典型的通信中,大部分有趣的信息是在初始标头中交换的。标头后面是正文或有效负载,它通常代表通信的大部分。再次,代理在这种通信部分退化为数据推送器。AppSwitch 提供了一种巧妙的机制来跳过这些情况下的代理。

尽管 AppSwitch 不是代理,但它确实仲裁应用程序端点之间的连接,并且它确实可以访问相应的套接字 FD。通常,AppSwitch 只是将这些 FD 传递给应用程序。但它也可以使用recvfrom(2) 系统调用套接字上的MSG_PEEK 选项窥视连接上接收到的初始消息。它允许 AppSwitch 检查应用程序流量,而无需实际将其从套接字缓冲区中删除。当 AppSwitch 将 FD 返回给应用程序并退出数据路径时,应用程序将对连接进行实际读取。AppSwitch 使用此技术来执行对应用程序级流量的更深入分析,并实现下一节中讨论的复杂网络功能,这一切都在不进入数据路径的情况下进行。

零成本负载均衡器、防火墙和网络分析器

负载均衡器和防火墙等网络功能的典型实现需要一个中间层,该中间层需要访问数据/数据包流。例如,Kubernetes 对负载均衡器 (kube-proxy) 的实现通过 iptables 引入了一个对数据包流的探测,而 Istio 在代理层实现了相同的功能。但是,如果所有需要的只是根据策略重定向或丢弃连接,则在连接的整个过程中没有必要停留在数据路径中。AppSwitch 可以通过简单地在 API 级别操作控制路径来更有效地处理这个问题。鉴于其与应用程序的紧密联系,AppSwitch 还可以轻松访问各种应用程序级指标,例如堆栈和堆使用情况的动态、服务何时启动、活动连接的属性等,所有这些都可能构成丰富的信号,用于监控和分析。

更进一步,AppSwitch 还可以根据从套接字缓冲区获得的协议数据执行 L7 负载均衡和防火墙功能。它可以将协议数据和各种其他信号与从 Pilot 获取的策略信息合成,以实现高效的路由和访问控制执行形式。它本质上可以“影响”应用程序连接到正确的后端服务器,而无需对应用程序或其配置进行任何更改。就好像应用程序本身注入了策略和流量管理智能一样。除了在这种情况下,应用程序无法逃脱影响。

还有一些更神奇的东西,实际上可以修改应用程序数据流而无需进入数据路径,但我将在以后的帖子中保留它。AppSwitch 的当前实现,如果用例需要修改应用程序协议流量,则使用代理。对于这些情况,AppSwitch 提供了一种高度优化的机制来吸引流量到代理,如下一节所述。

流量重定向

在边车代理可以查看应用程序协议流量之前,它首先需要接收连接。目前,通过一层数据包过滤来重定向进入和离开应用程序的连接,该过滤层重写数据包,使其进入各自的边车。创建可能需要大量规则来表示重定向策略非常繁琐。而且,随着边车捕获的目标子网发生变化,应用这些规则和更新这些规则的过程非常昂贵。

虽然 Linux 社区正在解决一些性能问题,但还有另一个与权限有关的问题:每当策略更改时,都需要更新 iptables 规则。根据当前的体系结构,所有特权操作都在一个 init 容器中执行,该容器在特权被实际应用程序删除之前,只在最开始运行一次。由于更新 iptables 规则需要 root 权限,因此没有办法在不重启应用程序的情况下执行此操作。

AppSwitch 提供了一种无需 root 权限即可重定向应用程序连接的方法。因此,无特权应用程序已经能够连接到任何主机(modulo 防火墙规则等),并且应该允许应用程序的所有者更改其应用程序通过connect(2) 传递的主机地址,而无需额外的权限。

套接字委托

让我们看看 AppSwitch 如何在不使用 iptables 的情况下帮助重定向连接。想象一下,应用程序以某种方式自愿地将用于其通信的套接字 FD 传递给边车,那么就不需要 iptables 了。AppSwitch 提供了一个名为套接字委托的功能,它正是这样做的。它允许边车透明地获得应用程序用于通信的套接字 FD 的副本,而无需对应用程序本身进行任何更改。

以下是将在 Python 应用程序示例上下文中实现此目标的步骤序列。

  1. 应用程序启动到 memcached 服务的服务 IP 的连接请求。
  2. 来自客户端的连接请求被转发到守护程序。
  3. 守护程序创建一对预连接的 Unix 套接字(使用socketpair(2) 系统调用)。
  4. 它将套接字对的一端传递到应用程序中,这样应用程序将使用该套接字 FD 进行读写。它还确保应用程序始终将其视为合法 TCP 套接字,因为它通过插入查询连接属性的所有调用来预期。
  5. 另一端通过不同的 Unix 套接字传递到边车,守护程序在该套接字上公开其 API。有关应用程序正在连接到的原始目标的信息也通过相同的接口传送。
Socket delegation protocol
基于套接字委托的连接重定向

一旦应用程序和边车连接,其余部分就会像往常一样进行。边车将启动到上游服务器的连接,并在从守护程序接收到的套接字和连接到上游服务器的套接字之间代理数据。这里的主要区别在于,边车将获得连接,不是通过accept(2) 系统调用(如正常情况下那样),而是通过 Unix 套接字从守护程序获得。除了通过正常的accept(2) 通道监听来自应用程序的连接外,边车代理还将连接到 AppSwitch 守护程序的 REST 端点并以这种方式接收套接字。

为了完整起见,以下是服务器端将发生的步骤序列

  1. 应用程序接收连接
  2. AppSwitch 守护程序代表应用程序接受连接
  3. 它使用socketpair(2) 系统调用创建一个预连接的 Unix 套接字对
  4. 套接字对的一端通过accept(2) 系统调用返回给应用程序
  5. 套接字对的另一端以及守护程序最初代表应用程序接受的套接字被发送到边车
  6. 边车将提取两个套接字 FD——一个与应用程序连接的 Unix 套接字 FD 和一个与远程客户端连接的 TCP 套接字 FD
  7. 边车将读取守护程序提供的有关远程客户端的元数据并执行其通常的操作

“边车感知”应用程序

套接字委托功能对于明确知道边车并希望利用其功能的应用程序非常有用。他们可以通过使用相同的功能将套接字传递给边车来自愿委托他们的网络交互。在某种程度上,AppSwitch 透明地将每个应用程序变成一个边车感知应用程序。

这一切是如何结合在一起的?

退一步来说,Istio 将应用程序的常见连接问题卸载到一个边车代理上,该代理代表应用程序执行这些功能。AppSwitch 通过避开中间层并将代理仅用于真正必要的情况来简化和优化服务网格。

在本节的其余部分,我将概述如何根据非常粗略的初始实现将 AppSwitch 集成到 Istio 中。这不是设计文档——没有探索所有可能的集成方式,也没有解决每个细节。目的是讨论实现的高级方面,以展示这两个系统如何结合在一起的粗略想法。关键是 AppSwitch 将充当 Istio 和真实代理之间的缓冲。它将充当不需要调用边车代理的情况下的“快速路径”。对于使用代理的情况,它将通过消除不必要的层来缩短数据路径。查看此博客,以更详细地了解集成的过程。

AppSwitch 客户端注入

类似于 Istio 边车注入器,一个名为ax-injector 的简单工具将 AppSwitch 客户端注入到标准 Kubernetes 清单中。注入的客户端透明地监控应用程序并通知 AppSwitch 守护程序应用程序产生的控制路径网络 API 事件。

如果使用 AppSwitch CNI 插件,则可以不进行注入并与标准 Kubernetes 清单一起工作。在这种情况下,CNI 插件将在收到初始化回调时执行必要的注入。但是,使用注入器确实有一些优势:(1)它适用于像 GKE 这样的严格控制的环境(2)它可以很容易地扩展到支持其他框架,例如 Mesos(3)同一个集群能够运行标准应用程序和“AppSwitch 启用”的应用程序。

AppSwitch DaemonSet

可以将 AppSwitch 守护程序配置为作为DaemonSet 运行,也可以配置为直接注入到应用程序清单中的应用程序扩展。无论哪种情况,它都会处理来自其支持的应用程序的传入网络事件。

策略获取代理

这是将 Istio 指示的策略和配置传达给 AppSwitch 的组件。它实现 xDS API 以从 Pilot 监听并调用相应的 AppSwitch API 来对守护进程进行编程。例如,它允许将由 istioctl 指定的负载均衡策略转换为等效的 AppSwitch 功能。

AppSwitch “自动整理” 服务注册表的平台适配器

鉴于 AppSwitch 位于应用程序网络 API 的控制路径中,它可以直接访问集群中服务的拓扑结构。AppSwitch 以服务注册表的形式公开这些信息,该注册表会随着应用程序及其服务的添加和删除而自动且(几乎)同步地更新。与 Kubernetes、Eureka 等一起用于 AppSwitch 的新平台适配器将向 Istio 提供上游服务的详细信息。这并非严格必要,但它确实简化了 AppSwitch 代理在上面从 Pilot 收到的服务端点之间的关联。

代理集成和链接

需要对应用程序流量进行深度扫描和修改的连接通过前面讨论过的套接字委派机制传递给外部代理。它使用 代理协议 的扩展版本。除了代理协议支持的简单参数之外,还会将各种其他元数据(包括从套接字缓冲区获取的初始协议头)和实时套接字 FD(代表应用程序连接)转发给代理。

代理可以查看元数据并决定如何处理。它可以通过接受连接来执行代理操作,或者通过指示 AppSwitch 允许连接并使用快速路径或直接丢弃连接来进行响应。

该机制的一个有趣的方面是,当代理从 AppSwitch 接受套接字时,它可以依次将套接字委派给另一个代理。实际上,这就是 AppSwitch 目前的运行方式。它使用一个简单的内置代理来检查元数据,并决定是在内部处理连接还是将其传递给外部代理(Envoy)。相同的机制可以潜在地扩展到允许使用一连串插件,每个插件都寻找特定的签名,链中的最后一个插件执行实际的代理工作。

不仅仅是性能

从数据路径中删除中间层不仅仅是为了提高性能。性能是一个很棒的副作用,但它 *是* 一个副作用。API 级方法有很多重要的优势。

自动应用程序加入和策略创作

在微服务和服务网格出现之前,流量管理由负载均衡器完成,访问控制由防火墙实施。应用程序通过相对静态的 IP 地址和 DNS 名称来识别。事实上,这仍然是大多数环境中的现状。此类环境将从服务网格中受益匪浅。然而,需要提供一个通往新世界的实用且可扩展的桥梁。转型中的困难并非源于缺乏功能,而是需要重新思考和重新实现整个应用程序基础设施的投资。目前,大多数策略和配置以负载均衡器和防火墙规则的形式存在。不知何故,需要利用现有上下文来提供采用服务网格模型的可扩展路径。

AppSwitch 可以大大简化加入流程。它可以将与目标相同的网络环境投射到应用程序上,如同其当前的源环境。如果没有这方面的帮助,对于传统应用程序而言,这通常是无法接受的,因为这些应用程序具有复杂配置的静态 IP 地址或硬编码的特定 DNS 名称。AppSwitch 可以帮助捕获这些应用程序及其现有配置,并将它们连接到服务网格,而无需进行任何更改。

更广泛的应用程序和协议支持

HTTP 显然在现代应用程序环境中占据主导地位,但一旦我们谈论传统应用程序和环境,就会遇到各种协议和传输。特别是,对 UDP 的支持不可避免。传统的应用程序服务器,例如 IBM WebSphere,广泛依赖于 UDP。大多数多媒体应用程序使用 UDP 媒体流。当然,DNS 可能是使用最广泛的 UDP “应用程序”。AppSwitch 以与 TCP 相同的方式在 API 级别支持 UDP,并且当它检测到 UDP 连接时,它可以透明地将其在其“快速路径”中处理,而不是将其委派给代理。

客户端 IP 保留和端到端原则

保留源网络环境的相同机制也可以保留服务器看到的客户端 IP 地址。如果存在侧车代理,连接请求将来自代理而不是客户端。结果,服务器看到的连接的对等地址(IP:port)将是代理的地址而不是客户端的地址。AppSwitch 确保服务器看到客户端的正确地址,正确记录它,并且根据客户端地址做出的任何决定都仍然有效。更一般地说,AppSwitch 保留了 端到端原则,而该原则在其他情况下会被混淆真实底层上下文的中间层破坏。

通过访问加密标头来增强应用程序信号

加密流量完全破坏了服务网格分析应用程序流量的能力。API 级插入可能会提供一种解决方法。AppSwitch 的当前实现是在系统调用级别访问应用程序的网络 API。但是,原则上可以在堆栈中更高位置影响应用程序的 API 边界,在该边界处应用程序数据尚未加密或已解密。最终,数据始终由应用程序以明文形式生成,然后在发出之前在某个时间点加密。由于 AppSwitch 直接在应用程序的内存上下文中运行,因此可以更高地访问堆栈中的数据,此时它仍然以明文形式保存。要使此方法有效,唯一的要求是用于加密的 API 应该定义明确且适合插入。特别是,它需要访问应用程序二进制文件的符号表。需要明确的是,AppSwitch 目前没有实现此功能。

那么净结果是什么呢?

AppSwitch 从标准服务网格堆栈中删除了许多层和处理。所有这些在性能方面意味着什么?

我们进行了一些初步实验,以根据前面讨论的 AppSwitch 的初步集成来描述优化机会的程度。这些实验在 GKE 上运行,使用 fortio-0.11.0istio-0.8.0appswitch-0.4.0-2。在无代理测试的情况下,AppSwitch 守护进程作为 Kubernetes 集群上的 DaemonSet 运行,并且 Fortio pod 规范被修改为注入 AppSwitch 客户端。这些是对此设置所做的唯一两个更改。该测试被配置为测量跨 100 个并发连接的 GRPC 请求的延迟。

Performance comparison
有和没有 AppSwitch 的延迟

初步结果表明,有和没有 AppSwitch 的 p50 延迟之间存在超过 18 倍的差异(3.99 毫秒与 72.96 毫秒)。禁用混合器和访问日志时,差异约为 8 倍。显然,差异是由于绕过了数据路径中所有这些中间层。在 AppSwitch 的情况下,Unix 套接字优化没有被触发,因为客户端和服务器 pod 被调度到不同的主机上。如果客户端和服务器恰好位于同一位置,则 AppSwitch 案例的端到端延迟会更低。本质上,在 Kubernetes 集群的各自 pod 中运行的客户端和服务器通过经过 GKE 网络的 TCP 套接字直接连接——没有隧道、桥接或代理。

最终结果

我从 David Wheeler 的看似合理的引语开始,该引语说添加另一层不是解决太多层问题的解决方案。并且在博客的大部分内容中,我都认为当前的网络堆栈已经包含太多层,应该删除它们。但是,AppSwitch 本身不是一层吗?

是的,AppSwitch 显然是另一层。然而,它可以删除多个其他层。通过这样做,它将新的服务网格层与传统网络环境的现有层无缝地粘合在一起。它抵消了侧车代理的成本,并且随着 Istio 升级到 1.0,它为现有应用程序及其网络环境提供了一座桥梁,使其能够过渡到服务网格的新世界。

也许 Wheeler 的引语应该改为

致谢

感谢 Mandar Jog(Google)就 AppSwitch 对 Istio 的价值进行了几次讨论,并感谢以下个人(按字母顺序排列)对该博客早期草稿的审阅。

分享此帖子