eBPF、sidecars以及服务网格展望


eBPF(Extended Berkeley Packet Filter)是一种相当酷的技术,它为云原生世界提供了很多帮助。它已经成为Kubernetes集群里CNI层的主流选择,譬如说Cilium这样的项目。Linkerd这样的服务网格部署了和Cilium一样的CNI层,结合了Linkerd强大的L7处理和Cilium超快的L3/4处理。

但eBPF的网络技术到底有多强大呢?比如,它是否允许我们完全替换Linkerd的sidecar代理,并且将所有的事务都在内核中运行?

本文,我们会尽量来评估这种可能性,特别是它对用户的影响。我将描述eBPF是什么以及它能做什么和不能做什么。我将阐述sidecar与其他模型的深入研究,并从运维和安全角度对它们进行对比。最后,我将阐述我的结论——我们Linkerd团队认为服务网格的未来与eBPF相关。

自我介绍

大家好,我是威廉·摩根(William Morgan)。我是Linkerd的创始人之一,Linkerd是第一个服务网格,也是它定义了服务网格的各个术语。我同时还是Buoyant公司的首席执行官,该公司帮助世界各地的组织采用Linkerd。没准大家可能还记得我曾经发表的又长又枯燥的技术文章:《Service Mesh:每个软件工程师都需要知道的这个被全世界高估的技术》和《Kubernetes工程师关于mTLS的指南:相互认证所带给你的愉悦和效益》。

我一直在强调Linkerd,也许是我的偏见。但我也很高兴以务实的态度从实施的层面看待它。Linkerd的最终目标是为我们的用户提供最简单的服务网格,Linkerd如何实现这种简化,其实是一个实施细节。例如,今天Linkerd使用sidecar,但更早的Linkerd的1.x版本是作为基于每个主机的代理部署的,这是出于操作性和安全的考虑才做出这样的变化的。eBPF被我们关注的是,它有可能让我们进一步简化Linkerd,尤其是在操作性层面。

eBPF是什么?

在我们进入服务网格细节之前,让我们先从eBPF开始。这个席卷推特圈的热门新技术到底是什么?

eBPF是Linux内核的一个特性,它允许应用程序在内核本身中执行某些类型的工作。eBPF起源于网络世界,但它不限于网络,这正是它的亮点:在其他事务中,eBPF解锁了整个网络的可观察性,这在过去是不可能的,因为它们会对性能产生影响。

假设你希望应用程序处理网络数据包。你不能直接访问宿主机的网络缓冲区。这个缓冲区由内核管理,因为内核必须保护它。例如,它必须确保一个进程不能读取另一个进程的网络数据包。相反,应用程序可以通过一个系统调用(syscall)来请求网络数据包信息,这本质上是一个内核的API调用:你的应用程序调用syscall,内核会检查你是否有权限获得你请求的数据包;如果有,就返还给你。

Syscall是可移植的,你的代码可以在非Linux机器上运行,但这会很慢。在现代网络环境中,你的机器可能每秒处理数千万个数据包,编写基于syscall的代码来处理每个数据包是不可能的。

来说说eBPF。代码不是在一个紧密的循环中调用syscall,并在“内核空间”和“用户空间”之间来回传递,而是直接将我们的代码交给内核,让它自己执行!瞧:现在没有更多的syscall了,我们的应用程序应该可以全速运行了。(当然,正如下面将看到的,事情并没有这么简单。)

eBPF是最近一系列内核特性中的一个,比如io_uring(Linkerd在重度使用中),它改变了应用程序和内核交互的方式。(ScyllaDB的Glauber Costa对此有一篇很棒的文章:《io_uring和eBPF将如何彻底改变Linux编程》。)这些特性以截然不同的方式工作着:io_uring使用一个特定的数据结构,允许应用程序和内核以一种安全的方式共享内存;eBPF的工作原理是允许应用程序直接向内核提交代码。但在这两种情况下,目标都是通过越过syscall的方法来提高性能。

eBPF虽然是一个巨大的进步,但并不是能解决一切问题的灵丹妙药。不可能将任意一个应用程序都作为eBPF运行。事实上,能使用eBPF做的事情也是非常有限的,请看下文。

多租户争用是很困难的

在我们理解eBPF为什么如此有限之前,我们需要讨论一下为什么内核本身如此有限。为什么像syscall这样的东西会存在?为什么程序不能直接访问网络(或内存或磁盘)?

内核在一个多租户争用的世界中运行。多租户意味着多个“租户”(例如人、帐户、其他形式的参与者)来共享机器,每个人都运行自己的程序。争用意味着这些租客不是“朋友”。他们不应该访问彼此的数据,或者相互干扰。内核需要在它们执行任意程序的时候提供强制约束的行为。换句话说,内核需要隔离租户。

这意味着内核不能真正信任任何程序。在任何时候,一个租户的程序都可能试图对另一个租户的数据或程序做一些不好的事情。内核必须确保任何程序都不能停止或破坏另一个程序,或拒绝它的资源,亦或是干扰它的运行能力,或从内存、网络或磁盘读取数据,除非得到明确的许可需要这样做。

这是一个非常关键的需求!世界上几乎所有与软件相关的安全保证最终都归结于——内核在执行着此类保护措施。一个程序可以在未经允许的情况下读取另一个程序的内存或网络流量,这是一个数据泄露的行径,也有可能更糟。一个可以写入另一个程序的内存或网络流量的程序是欺诈的行径,甚至更糟。允许程序打破规则的内核调用是一个非常大的问题。而其中一种打破这些规则的方法就是访问内核的内部状态——如果你可以读写内核内存,那么你就可以绕过这些规则。

这就是为什么应用程序和内核之间的每一次交互都要受到高度审查的原因。失败的后果是极其严重的。内核开发人员们已经为这个问题付出了日以继夜的努力。

这也是容器如此强大的原因——它们采用相同的隔离策略,并将其应用于任意一个应用程序和依赖包。多亏了这个先进的内核策略,我们可以彼此隔离地运行容器,并充分利用内核处理多租户争用的能力。以前使用虚拟机实现这种隔离的方法很慢,而且成本很高。容器的神奇之处在于,它们以一种非常便宜的方式为我们提供了(大部分)相同的保证。

我们所认为的“原生云”几乎每个方面都依赖于这个隔离保证。

eBPF的局限性

回到eBPF。正如我们所讨论的,eBPF允许我们交出内核代码,并说“给,请在内核中运行它”。从内核安全的角度来看,我们知道这是一件令人难以置信的可怕的事情——它将绕过应用程序和内核之间的所有障碍(如syscall),并将我们直接置于安全漏洞的区域。

因此,为了确保安全,内核对所执行的代码施加了一些非常重要的约束。在运行它们之前,所有eBPF程序必须通过一个验证器,它检查它们是否有不正常的行为。如果验证程序拒绝这个程序,内核就不会运行它。

程序的自动验证是比较难实现的,而且验证器也有可能因为过于严格,导致出现报错。因此,eBPF的程序非常有限。例如,它们不能有阻拦其他程序的设定;它们不能有无界循环;它们不能超过预设的大小。同时也因为复杂性也会受到限制——验证器需要在所有可能的执行路径上运行,如果它不能在某些限制内完成,或者不能证明每个循环都有退出机制,程序则不能通过。

有许多安全的程序完美的违反了这些限制。如果你想将其中一个程序作为eBPF运行,那太糟糕了!你需要重写程序以满足验证器。如果你是eBPF的粉丝,有个好消息是,随着每个内核版本中的验证器变得更智能,这些限制将逐渐放宽,同时也开始有一些创造性的方法可以绕过这些限制。

但总的来说,eBPF项目所能做的事情非常有限。一些非常重要的事务,例如处理全范围的HTTP/2流量,或协商TLS握手,这些都不能在eBPF中完成。比较好的情况下,eBPF也只能完成这项工作的一小部分,剩下大部分还是需要通过调用用户空间的应用程序来处理,因为eBPF无法处理过于复杂的部分。

eBPF与服务网格对比

了解了eBPF的基础知识之后,让我们回到服务网格。

服务网格用来处理现代云原生网络的复杂性。以Linkerd为例,启动和终止双方的TLS;跨连接重试请求;为提高性能,透明地在代理之间从HTTP/1.x升级到HTTP/2;强制基于工作负载标识的访问策略;跨Kubernetes集群边界发送流量;还有很多很多。

与大多数服务网格一样,Linkerd通过在每个应用程序的Pod中插入一个代理来实现这一点,该代理拦截并增加了与Pod之间的TCP通信。这些代理与应用程序容器一起在它们自己的容器中运行——“sidecar”模型。Linkerd的代理是超轻、超快、基于Rust的微代理,但也有其他办法可行。

十年前,在集群上部署数百或数千个代理并将它们与每个应用程序的每个实例连起来的想法在操作层面上看简直是一场噩梦。但多亏了Kubernetes,突然变得非常简单。多亏了Linkerd巧妙的工程技术(如果我可以自夸的话),它也是可管理的:Linkerd的微代理不需要调优,因为它仅仅消耗极少的系统资源。

在这种情况下,eBPF已经和服务网格配合很多年了。Kubernetes给世界的礼物是一个具有清晰层间边界的可组合平台,eBPF和服务网格之间的关系正好符合该模型:CNI负责L3/L4流量,而服务网格负责L7。

服务网格对于平台的所有者来说是非常棒的。它在平台层面提供了mTLS、请求重试、“黄金指标”等功能,这意味着他们不再需要依赖应用开发人员来构建这些功能。但代价当然是在各处添加大量代理。

所以回到我们最初的问题:我们能做得更好吗?我们是否可以通过“eBPF服务网格”获得服务网格的功能,而不需要sidecar?

eBPF服务网格仍然需要代理

现在有了我们对eBPF的理解,我们可以跳进这些浑浊的水域,探索可能潜伏在里面的东西。

不幸的是,我们很快就触底了:eBPF的限制意味着L7流量代理仍然需要用户空间网络代理来完成繁重的工作。换句话说,任何eBPF服务网格仍然需要代理。

基于每个主机的代理明显比sidecar差

因此我们的eBPF服务网格需要代理。但它是否特别需要sidecar代理?如果我们使用基于每个主机的代理会怎么样?这会给我们一个没有sidecar的、eBPF支持的服务网格吗?

不幸的是,我们在Linkerd 1.x中了解了太多关于为什么这不是一个好主意。同sidecar相比,基于主机的代理在操作、维护和安全性方面都更差。

在sidecar模型中,应用程序的单个实例的所有流量都通过它的sidecar代理处理。这允许代理作为应用程序的一部分,这是理想的:
  • 代理对资源的消耗是随着应用程序负载的变化而变化。随着对实例的通信增加,sidecar会消耗更多的资源,就像应用程序一样。如果应用程序占用的流量很小,则sidecar不需要消耗很多资源。(Linkerd的代理在低流量水平下有2-3MB的内存占用。) 在Kubernetes现有的管理资源消耗的机制下,例如资源请求、限制以及OOM终止,都在继续工作。
  • 代理失效的爆炸半径仅限于一个Pod。代理失败与应用程序失败是一样的,并由现有的Kubernetes机制处理失败的Pod。
  • 代理维护,例如代理版本的升级,是通过与应用程序本身相同的机制完成的:滚动更新Deployments等。
  • 安全的周界是很明确的(也是非常小的):它就在Pod层。Sidecar在与应用程序实例在相同的安全周界中运行。它是同一个Pod的一部分。它得到相同的IP地址。它执行策略并将mTLS应用于往返于该Pod的流量,它只需要该Pod的关键信息。


在基于主机的模型中,这些细节都不复存在。现在,代理不再是单一的应用程序实例,而是为任意一个应用实例的一组有效随机的Pod处理通信,同时这些Pod是由Kubernetes在某个主机上调度的。代理现在与应用程序完全解耦,这就引入了各种微妙和不那么微妙的问题:
  • 代理资源消耗现在是高度可变的:它取决于Kubernetes在任意时间点在哪个主机上安排了什么。这就意味着你不能有效地预测或推断某个特定代理的资源消耗,那么这就意味着它最终可能会崩溃,服务网格团队也将受到指责。
  • 应用程序现在很容易受到“噪音邻居”流量的影响。由于通过主机的所有流量都通过一个代理,因此一个高流量的Pod可能会消耗所有的代理资源,而代理必须确保公平性,否则应用程序将面临资源短缺的风险。
  • 代理的爆炸半径很大,而且是不断变化的。代理的故障和升级现在会影响随机应用程序集上的随机Pod集群,这意味着任何故障或维护任务都难以预测其影响。
  • 安全问题现在要复杂得多。例如,要执行TLS,基于主机的代理必须包含每个应用程序的密钥信息,这种成为易受混淆的代理问题,并且成了新的漏洞攻击的向量,也就是说,代理中的任何CVE或漏洞现在都是潜在的密钥泄漏。


简而言之,sidecar保持了从转移到容器中获得的隔离保证:内核可以在容器级别执行多租户的所有安全性和公平性考虑因素,而且一切都可以正常工作。基于主机的模型使我们完全脱离了这个世界,也给我们留下了多租户争用的所有问题。

当然,基于每个主机的代理也有一些优点。你可以将请求必须通过的代理数量从sidecar模型中的每跳两个减少到每跳一个,这样可以节省延迟。你可以使用数量更少、但更大的代理,如果你的代理具有较高的基础架构成本,那么这样可能更有利于资源消耗。(Linkerd 1.x就是一个很好的例子——很擅长扩大流量规模但不擅长缩小规模)。而且你的网络架构图也变得更简单了,因为你有更少的节点。

但是与你所遇到的运维性和安全问题相比,这些优点是次要的。而且,除了在网络图中减少节点之外,我们可以通过良好的工程设计来减少这些差异——确保我们的sidercar尽可能快、尽可能的小。

我们能不能改进一下代理?

关于基于每个主机的代理,我们列出的一些问题涉及到我们的多租户争用。在sidecar领域,我们使用内核的现有解决方案通过容器来解决多租户争用。但在基于主机的代理模型中,我们不能这样做,然而我们可以通过让基于主机的代理本身能够处理多租户的争用来解决这些问题吗?例如,一个流行的代理是Envoy。我们是否可以通过调整Envoy来处理多租户争用来解决每个主机代理的问题?

如果说是肯定的,是因为“它不会违背宇宙的物理定律”。但因为“这将是一项巨大的工作,不会很好地利用任何人的时间”,所以答案应该是否定的。Envoy不是为多租户争用设计的,它将需要巨大的努力来改变这一点。有一个很长的有趣的Twitter帖子,探讨了如果你想深入了解Envoy的细节,必须做很多事情:需要在项目中增加大量非常棘手的工作,以及必须不断权衡“每个租户只运行一个Envoy”的大量更改——也就是sidecar。

即使你完成了这项工作,到最后,你仍然会遇到爆炸半径和安全的问题。

服务网格的未来

综上所述,我们得出了一个结论:不管有没有eBPF,服务网格可预见的未来都是通过在用户空间中运行的sidecar代理构建的。

Sidecar并不是没有问题,但它们是现有的最好的解决方案,可以在保持容器所提供的隔离性的同时,还要全面处理云本地网络的复杂性。说到eBPF,它能从服务网格分担工作,它应该通过使用sidecar代理来做到这一点,而不是基于主机的代理。“让现有的基于sidecar的方法更快,同时保留容器化的操作和安全优势”与“通过摆脱sidecar来解决服务网格的复杂性和性能”两者卖点不太一样,但从用户的角度来看,这是一种胜利。

eBPF的功能最终会发展到不需要代理来处理服务网格提供的L7工作的全部范围吗?也许吧。内核将能够通过除eBPF以外的机制来吸收工作范围吗?也许吧。这两种可能性似乎都不是近在眼前,但如果有一天它们真的出现了,或许我们就可以告别“sidercar代理”了。我们期待这种可能性。

与此同时,从Linkerd的角度来看,我们将继续努力使我们的sidecar微型代理尽可能小、快、操作上可以忽略不计,包括将有意义工作交给eBPF。我们的基本职责是维护我们的用户和他们在Linkerd的运营体验,通过这个镜头,我们必须始终权衡每一个设计和工程。

原文链接:eBPF, sidecars, and the future of the service mesh(翻译:伊海峰)

0 个评论

要回复文章请先登录注册