3

请暂时抛弃使用 eBPF 取代服务网格和 sidecar 模式的幻想

 1 year ago
source link: https://jimmysong.io/blog/epbf-sidecar-and-service-mesh/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近 eBPF 技术在云原生社区中持续火热,在我翻译了《什么是 eBPF 》之后,当阅读“云原生环境中的 eBPF”之后就一直在思考 eBPF 在云原生环境中究竟处于什么地位,发挥什么样的作用。当时我评论说“eBPF 开启了上帝视角,可以看到主机上所有的活动,而 sidecar 只能观测到 pod 内的活动,只要搞好进程隔离,基于 eBPF 的 proxy per-node 才是最佳选择”,再看到 William Morgan 的这篇文章 1之后,让我恍然大悟。下面节选翻译了文章我比统同意的观点,即 eBPF 无法替代服务网格和 sidecar,感兴趣的读者可以阅读 William 的原文。

什么是 eBPF

在过去,如果你想让应用程序处理网络数据包,那是不可能的。因为应用程序运行在 Linux 用户空间,它是不能直接访问主机的网络缓冲区。缓冲区是由内核管理的,受到内核保护,内核需要确保进程隔离,进程之间不能直接读取对方的网络数据包。正确的做法是,应用程序通过系统调用(syscall)来请求网络数据包信息,这本质上是内核 API 调用——应用程序调用 syscall,内核检查应用程序是否有权限获得其请求的数据包;如果有,就把返回数据包。

有了 eBPF 之后,应用程序不再需要 syscall,数据包不需要在内核空间和用户空间之间来回交互传递。而是我们将代码直接交给内核,让内核自己执行,这样就可以让代码全速运行,效率更高。eBPF 允许应用程序和内核以安全的方式共享内存,eBPF 允许应用程序直接向内核提交代码,目标都是通过超越系统调用的方式来实现性能提升。

eBPF 不是银弹,你不能用 eBPF 运行任意程序,实际上 eBPF 可以做的事情是非常有限的。

eBPF 的局限性

eBPF 的局限性也是因为内核造成的。内核中运行的应用程序应当有自己的租户,这些租户之间会争抢系统的内存、磁盘和网络,内核的职责就是隔离和调度这些应用程序的资源,同时内核还要保护确认应用程序的权限,保护其不被其他程序破坏。

因为我们直接将 eBPF 代码交给内核执行,这绕过了内核安全保护(如 syscall),内核将面临直接的安全风险。为了保护内核,所有 eBPF 程序要想运行都必须先通过一个验证器。但是要想自动验证程序是很困难的,验证器可能会过度限制程序的功能。比如 eBPF 程序不能是阻塞的,不能有无限循环,不能超过预定的大小;其复杂性也受到限制,验证器会评估所有可能的执行路径,如果 eBPF 程序不能在某些范围内完成,或者不能证明每个循环都有一个退出条件,那么验证器就不会允许该程序运行。有很多应用程序都违反了这些限制,要想将它们作为 eBPF 程序来运行的话,要么重写以满足验证器的需求,要么给内核打补丁,来绕过一些验证(这可能比较困难)。不过随着内核版本的升级,这些验证器也变得更加智能,限制也逐渐变得宽松,也有一些创造性的方法来绕过这些限制。

但总的来说,eBPF 程序能做的事情非常有限。对于一些重量级事件的处理,例如处理全局范围内的 HTTP/2 流量,或者 TLS 握手协商不能在纯 eBPF 环境中完成。充其量,eBPF 可以做其中的一小部分工作,然后调用用户空间应用程序来处理对于 eBPF 来说过于复杂而无法处理的部分。

eBPF 与服务网格的关系

因为上文所述的 eBPF 的各项限制,七层流量仍然需要用户空间的网络代理来完成,eBPF 并不能替代服务网格。eBPF 可以与 CNI(容器网络接口)一起运行,处理三层/四层流量,而服务网格处理七层流量。

每个主机一个代理的模式比 sidecar 更糟

对于每个主机一个代理(per-host)的模式,服务网格的早期实践者 Linkerd 1.x 就是这么用的,笔者也是从那个时候开始关注服务网格,Linkerd 1.x 还使用了 JVM 虚拟机!但是经过 Linkerd 1.x 的用户实践证明,这种模式相对于 sidecar 模式,对于运维和安全来说会更糟糕。

为什么说 sidecar 模式比 per-host 模式更好呢?因为 sidecar 模式有以下几个优势,这是 per-host 模式所不具备的:

  1. 代理的资源消耗随着应用程序的负载而变化。随着实例流量的增加,sidecar 会消耗更多的资源,就像应用程序一样。如果应用程序的流量非常小,那么 sidecar 就不需要消耗很多资源。Kubernetes 现有的管理资源消耗的机制,如资源请求和限制以及 OOM kill,都会继续工作。
  2. 代理失败的爆炸半径只限于一个 pod。代理失败与应用失败相同,由 Kubernetes 负责处理失败的 pod。
  3. 代理维护。例如代理版本的升级,是通过如滚动更新,灰度发布等应用程序本身相同的机制完成的。
  4. 安全边界很清楚(而且很小):在 pod 级别。Sidecar 在应用程序实例的同一安全上下文中运行。它是 pod 的一部分,与应用程序具有一样的 IP 地址。Sidecar 执行策略,并将 mTLS 应用于进出该 pod 的流量,而且它只需要该 pod 的密钥。

而对于 per-host 模式,就没有上述好处了。代理与应用程序 pod 完全解耦,处理主机上所有 pod 的流量,这样会代理各种问题:

  1. 代理消耗的资源是高度可变的,这取决于在某个时间点 Kubernetes 调度了多少个 pod 在该主机上。你无法有效的预测特定代理的资源消耗情况,这样代理就有崩溃的风险(原文是这么说的,这点笔者还是存疑的,希望有点读者能解帮忙解释下)。
  2. 主机上 pod 之间的流量争抢问题。因为主机上的所有流量都经过同一个代理,如果有一个应用程序 pod 的流量极高,消耗了代理的所有资源,主机上的其他应用程序就有被饿死的危险。
  3. 代理的爆炸半径很大,而且是不断变化的。代理的故障和升级现在影响到随机的应用程序集合中的一个随机的 pod 子集,意味着任何故障或维护任务都有难以预测的风险。
  4. 使得安全问题更加复杂。以 TLS 为例,主机上的代理必须包含该主机上所有应用程序的密钥,这使得它成为一个新的攻击媒介,容易受到混淆代理 问题的影响——代理中的任何 CVE 或漏洞都是潜在的密钥泄露风险。

简而言之,sidecar 模式继续贯彻了容器级别的隔离保护——内核可以在容器级别执行所有安全保护和公平的多租户调度。容器的隔离仍然可以完美的运行,而 per-host 模式却破坏了这一切,重新引入了争抢式的多租户隔离问题。

当然 per-host 也不是一无是处,该模式最大的好处是可以成数量级的减少代理的数量,减少网络跳数,这也就减少了资源消耗和网络延迟。但是与该模式带来的运维和安全性问题相比,这些优势都是次要的。我们也可以通过持续优化 sidecar 来弥补 sidecar 模式在这方面的不足,而 per-host 模式的缺陷确是致命性的。

其实归根结底还是回到了争抢式多租户问题上,那么能否利用现有的内核解决方案,改进一下 per-host 模式中的代理,让其支持多租户呢?比如改造 Envoy 代理,使其支持多租户模式。虽然从理论来说这是可行的,但是工作量巨大,Matt Klein 也觉得不值得这样做 2,还不如使用容器来实现租户隔离。而且即使让 per-host 模式中的代理支持了多租户,仍然还有爆炸半径和安全问题需要解决。

不管有没有 eBPF,在可预见的未来,服务网格都会基于运行在用户空间的 sidecar 代理(proxyless 模式除外)。Sidecar 模式虽然也有弊端,但它依然是既能保持容器隔离和操作的优势,又能处理云原生网络复杂性的最优方案。eBPF 的能力将来是否会发展到可以处理七层网络流量,从而替代服务网格和 sidecar,也许吧,但那一天可能很遥远。


  1. William Morgan 的 eBPF, sidecars, and the future of the service mesh 这篇文章正好回答了我的关于 eBPF、sidecar 的疑问。 ↩︎

  2. 关于 per-host 模式中的代理改造问题,Twitter 上有一个精彩的讨论 。 ↩︎


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK