请暂时抛弃使用 eBPF 取代服务网格和 sidecar 模式的幻想
最近 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 模式所不具备的:
- 代理的资源消耗随着应用程序的负载而变化。随着实例流量的增加,sidecar 会消耗更多的资源,就像应用程序一样。如果应用程序的流量非常小,那么 sidecar 就不需要消耗很多资源。Kubernetes 现有的管理资源消耗的机制,如资源请求和限制以及 OOM kill,都会继续工作。
- 代理失败的爆炸半径只限于一个 pod。代理失败与应用失败相同,由 Kubernetes 负责处理失败的 pod。
- 代理维护。例如代理版本的升级,是通过如滚动更新,灰度发布等应用程序本身相同的机制完成的。
- 安全边界很清楚(而且很小):在 pod 级别。Sidecar 在应用程序实例的同一安全上下文中运行。它是 pod 的一部分,与应用程序具有一样的 IP 地址。Sidecar 执行策略,并将 mTLS 应用于进出该 pod 的流量,而且它只需要该 pod 的密钥。
而对于 per-host 模式,就没有上述好处了。代理与应用程序 pod 完全解耦,处理主机上所有 pod 的流量,这样会代理各种问题:
- 代理消耗的资源是高度可变的,这取决于在某个时间点 Kubernetes 调度了多少个 pod 在该主机上。你无法有效的预测特定代理的资源消耗情况,这样代理就有崩溃的风险(原文是这么说的,这点笔者还是存疑的,希望有点读者能解帮忙解释下)。
- 主机上 pod 之间的流量争抢问题。因为主机上的所有流量都经过同一个代理,如果有一个应用程序 pod 的流量极高,消耗了代理的所有资源,主机上的其他应用程序就有被饿死的危险。
- 代理的爆炸半径很大,而且是不断变化的。代理的故障和升级现在影响到随机的应用程序集合中的一个随机的 pod 子集,意味着任何故障或维护任务都有难以预测的风险。
- 使得安全问题更加复杂。以 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,也许吧,但那一天可能很遥远。
参考
-
William Morgan 的 eBPF, sidecars, and the future of the service mesh 这篇文章正好回答了我的关于 eBPF、sidecar 的疑问。 ↩︎