译者按:这篇博客非常清晰地阐述了目前很热的 eBPF 和 Service Mesh 的关系,并分别介绍 Envoy 在几种不同的数据平面架构模型中的位置,以及这几种架构模型各自的优势和权衡。最近我和同事以及社区的同学就引入 eBPF 之后 Service Mesh 的架构演进做了一些讨论,结论和 Solo 的这篇博客中的某些观点类似。作为 Linux 内核的一种扩展能力,eBPF 并不会替换 Envoy 的七层代理能力,而是作为 Service Mesh 数据面的一个增强技术。
在 Solo.io,我们的目标是围绕应用网络和服务连接为客户带来有价值的解决方案。早在今年 10 月,我们就宣布计划用 eBPF 加强我们的企业级服务网格产品(Gloo Mesh Enterprise),以优化网络、可观察性和安全性方面的功能。eBPF 在服务网格中能发挥多大的作用?服务代理的角色将会如何改变? 在这篇博客中,我们将探讨 eBPF 在服务网格数据平面中的角色,以及各种不同数据平面架构的一些权衡因素。
告别服务代理?
服务网格为服务提供了复杂的应用层网络管理,如服务发现、流量路由、弹性(超时/重试/断路)、认证/授权、可观察性(日志/度量/追踪)等。我们可以用 eBPF 将所有这些功能写入到操作系统内核吗?
简短的回答:这将是相当困难的,而且可能也不是一个正确的做法。eBPF 采用的是一个事件-处理器模型,因此其运行方式有一些限制。你可以把 eBPF 模型看成是内核的 “Function as a service”。例如,eBPF 代码的执行路径必须是完全确定的,会在执行前进行验证,以确保其可以在内核中安全运行。eBPF 程序不能有任意的循环,否则验证程序将不知道程序何时停止执行。简而言之,eBPF 是图灵不完整的。
第7层的处理(如各种协议编解码、重试、头的操作等)在eBPF中单独实现会非常复杂,而且没有内核的更好的本地支持。也许这种支持会出现,但这很可能是多年以后的事情,而且在旧版本上也不会有。在许多方面,eBPF是O(1)复杂性的理想选择(例如检查一个数据包,操作一些比特,然后发送它)。实现像HTTP/2和gRPC这样复杂的协议可能是O(n)复杂度,而且非常难以调试。那么,这些L7功能可以驻留在哪里?
Envoy代理已经成为服务网状结构实现的事实上的代理,并且对我们大多数客户需要的第7层功能有非常好的支持。虽然eBPF和Kernel可以用来改善网络的执行(短路最佳路径、卸载TLS/mTLS、可观察性收集等),但复杂的协议协商、解析和用户扩展可以保留在用户空间。对于第7层的复杂情况,Envoy仍然是服务网状结构的数据平面。
共享代理还是边车代理?
在尝试优化服务网格的数据路径时,另一个考虑因素是,是为每个工作负载运行一个边车代理,还是一个节点中的所有工作负载使用一个共享代理。当运行具有数千个 pod 和数百个节点的大规模集群时,共享代理模型可以提供内存和配置开销的优化。但共享代理模型并不适用于所有人。许多企业用户认为,因为采用边车代理可以获得更好的租户和工作负载隔离,一些额外的内存开销是值得的。
在内存和网络开销、租户隔离、运维和简单性等方面,这两种模式分别具有各自的优势和权衡。不管采用哪种模式,都可以受益于基于 eBPF 的优化。服务网格数据面并不只有这两种架构,下面我们根据以下的维度来看一下我们拥有的选项。
- 内存/CPU开销–为七层代理配置路由和集群细节包括代理特定的配置,这些配置可能很冗长;一个工作负载需要与之通信的服务越多,它需要的配置就越多。
- 功能隔离–应用程序各有不同,往往需要针对每个工作负载对连接池、套接字缓冲区、重试语义/预算、外部授权和限流等进行不同的优化。我们看到很多定制数据路径的需求,这就也是我们引入 Wasm 扩展的原因。调试这些功能和行为也变得很困难。我们需要找出一种方法,以在工作负载之间隔离这些功能。
- 安全粒度 - 零信任理念的一个重要部分是在运行时基于当前的上下文建立对对等体的信任;我们往往希望尽可能缩小这些信任边界的范围。
- 升级影响–服务网格是非常重要的基础设施。服务网格作用在服务的请求路径上,我们需要对数据面组件的升级进行非常严格的控制,以尽量减少业务中断。
让我们看看四种可能的服务网格数据面架构。在这四种架构中,都是用了 eBPF 来优化和缩短网络路径,并采用 Envoy 代理来实现第七层的流量管理功能。我们将查看每种架构分别在哪里运行7层代理,并从开销、隔离、安全和升级这几方面评估每种架构的优势和权衡。
边车代理
在这种模型中,我们为每个应用实例部署一个边车代理。边车代理拥有为该工作负载进行路由所需的全部配置,并且可以根据工作负载进行定制。 这些配置被重复下发到多个工作负载的边车代理中,对于资源开销而言,该模型是 “次优 “的。
这种模式确实提供了最好的隔离,可以减少出现问题时的故障域。配置错误或特定应用的缓冲区/连接池/超时被隔离到特定的工作负载。使用 Lua 或 Wasm 的扩展(这些扩展有可能使代理瘫痪)也被限制在特定的工作负载中。
从安全的角度来看,我们直接发起和终止到应用程序的连接。我们可以使用服务网格的 mTLS 功能来证明连接两端的服务的身份,身份验证的范围缩小到应用进程的水平。然后,我们可以使用这个身份来编写细粒度的授权策略。这种模式的另一个好处是,如果一个代理成为了攻击者的受害者,被攻击的代理会只会影响到一个工作负载;问题域是有限的。然而该模式在安全方面也存在缺点,由于边车必须与工作负载一起部署,则有可能工作负载选择不注入边车,或者更糟的是,找到一种方法来绕过边车。
最后,在这个模型中,可以按单个工作负载的粒度对数据面的代理进行灰度升级。例如我们可以将 Pod A 的数据平面升级到一个新的版本,而不影响节点上的任何其他工作负载。该模型的缺点是注入边车仍然很棘手,而且如果边车代理的不同版本之间有变化,可能会影响应用实例。
每个节点共享一个代理
每个节点共享一个代理的模型引入了对大型集群有意义的优化,在某些大型集群中,内存开销是需要首要考虑的问题,因此希望采用节点共享代理的方式对内存成本进行摊销。在这种模式在节点上安装一个代理来为节点上的所有工作负载共享路由和服务集群等配置信息,而不是在每个边车代理上重复进行配置。 从功能隔离的角度来看,这种模式试图在一个 Envoy 代理进程中解决该节点上所有工作负载实例的问题,这可能会存在弊端。例如,多个应用程序的配置可能会相互冲突或影响?你能安全地加载因监管原因而必须分开管理的加密内容或私钥吗?你可以在不影响其他应用程序的情况下部署 Wasm 扩展吗?为一批应用程序共享一个代理存在隔离的问题,采用独立的进程/代理则可以很好地解决这些问题。
在节点共享代理模型中,安全边界也成为了共享模式。工作负载的身份现在是在节点层面,而不是实际的工作负载中处理的。那么在代理和工作负载之间的最后这段路程会发生什么?更糟糕的是,如果代表多个(可能多达数百个)工作负载身份的共享代理被破坏,会发生什么?
最后,如果共享代理升级时出现版本冲突、配置冲突或扩展不兼容等问题,则可能会影响该节点上的所有工作负载。任何时候,都必须小心处理应用程序请求的共享基础设施的升级。从好的方面看,升级共享节点代理不必考虑注入边车的那些复杂问题。
每个节点上的每个服务账户共享一个代理
我们可以按照服务账号来隔离节点上的共享代理,而不是为整个节点上的所有工作负载共享一个代理。在这种模式下,我们为每个服务账户/身份部署一个 “共享代理”,该服务账户/身份下的任何工作负载都使用该代理。我们可以通过这种模式来规避边车注入的一些复杂问题。 这个模型试图在一个节点上存在多个相同身份的实例的情况下节省内存,并保持一定程度的功能和故障隔离。对于工作负载身份而言,这种模式具有和边车模式相同的优点,但是它也有共享代理的缺点:如何保证最后这段工作负载和代理之间的连接的安全?如何将和工作负载实例建立认证?我们可以采用这种方式改进这种模式:在工作负载中部署一个较小的 “微型代理”,用于实例之间的端到端 mTLS。我们在下一个模式中将会对此进行介绍。
带有微型代理的共享远程代理
在这个模式中加入了一个较小的、轻量级的 “微型代理”,该微型代理只处理 mTLS,不负责 L7 相关的策略,攻击面较小。这个微型代理被部署为工作负载实例的边车。当需要应用7层策略时,工作负载实例的流量会被重定向到7层代理(Envoy)上。该7层代理可以采用共享节点代理、每个服务账户代理,或者远程代理的方式运行。这种模式也允许在不需要这些策略的时候完全绕过7层代理(但依然会采用微型代理来处理应用实例之间的 mTLS 发起/协商/终止)。 这种模式减少了边车模式带来的7层策略的配置开销,但可能引入更多的网络跳数。这些跳转可能(也可能不会)导致更大的请求时延。在某些情况下,由于 L7 代理甚至不在请求的数据路径中,这将改善请求时延。
这种模式结合了边车代理的功能隔离和安全优势,因为微型代理仍然会与工作负载实例一起部署。
从升级的角度来看,虽然我们引入了更多组件,但我们可以采用对应用透明的方式来更新 L7 代理。当然我们还需要处理微型代理的升级,这与我们讨论的第一种模式中的边车架构有一些相同的缺点。
最后的考虑
如同我们在 Service Mesh Con 2019上 的 “The truth about the service mesh data plane“中讨论的一样,服务网格数据平面可以有不同的架构,每种架构有其不同的优势和权衡。在 Solo.io,我们认为 eBPF 是优化服务网格的一种强大方式,同时我们认为 Envoy 代理是数据平面的基石。我们在与客户合作过程中(这些客户包含了各种规模,包括一些世界上最大的服务网格部署),帮助客户平衡优化、功能、可扩展性、调试性和用户体验之间的权衡。