最近 eBPF 技术在云原生社区中持续火热,在我翻译了《什么是 eBPF》之后,当阅读“云原生环境中的 eBPF”之后就一直在思考 eBPF 在云原生环境中究竟处于什么地位,发挥什么样的作用。当时我评论说“eBPF 开启了上帝视角,可以看到主机上所有的活动,而 sidecar 只能观测到 pod 内的活动,只要搞好进程隔离,基于 eBPF 的 proxy per-node 才是最佳选择”,再看到 William Morgan 的这篇文章 1之后,让我恍然大悟。下面节选翻译了文章我比统同意的观点,即 eBPF 无法替代服务网格和 sidecar,感兴趣的读者可以阅读 William 的原文。
在过去,如果你想让应用程序处理网络数据包,那是不可能的。因为应用程序运行在 Linux 用户空间,它是不能直接访问主机的网络缓冲区。缓冲区是由内核管理的,受到内核保护,内核需要确保进程隔离,进程之间不能直接读取对方的网络数据包。正确的做法是,应用程序通过系统调用(syscall)来请求网络数据包信息,这本质上是内核 API 调用——应用程序调用 syscall,内核检查应用程序是否有权限获得其请求的数据包;如果有,就把返回数据包。
有了 eBPF 之后,应用程序不再需要 syscall,数据包不需要在内核空间和用户空间之间来回交互传递。而是我们将代码直接交给内核,让内核自己执行,这样就可以让代码全速运行,效率更高。eBPF 允许应用程序和内核以安全的方式共享内存,eBPF 允许应用程序直接向内核提交代码,目标都是通过超越系统调用的方式来实现性能提升。
eBPF 不是银弹,你不能用 eBPF 运行任意程序,实际上 eBPF 可以做的事情是非常有限的。
eBPF 的局限性也是因为内核造成的。内核中运行的应用程序应当有自己的租户,这些租户之间会争抢系统的内存、磁盘和网络,内核的职责就是隔离和调度这些应用程序的资源,同时内核还要保护确认应用程序的权限,保护其不被其他程序破坏。
因为我们直接将 eBPF 代码交给内核执行,这绕过了内核安全保护(如 syscall),内核将面临直接的安全风险。为了保护内核,所有 eBPF 程序要想运行都必须先通过一个验证器。但是要想自动验证程序是很困难的,验证器可能会过度限制程序的功能。比如 eBPF 程序不能是阻塞的,不能有无限循环,不能超过预定的大小;其复杂性也受到限制,验证器会评估所有可能的执行路径,如果 eBPF 程序不能在某些范围内完成,或者不能证明每个循环都有一个退出条件,那么验证器就不会允许该程序运行。有很多应用程序都违反了这些限制,要想将它们作为 eBPF 程序来运行的话,要么重写以满足验证器的需求,要么给内核打补丁,来绕过一些验证(这可能比较困难)。不过随着内核版本的升级,这些验证器也变得更加智能,限制也逐渐变得宽松,也有一些创造性的方法来绕过这些限制。
但总的来说,eBPF 程序能做的事情非常有限。对于一些重量级事件的处理,例如处理全局范围内的 HTTP/2 流量,或者 TLS 握手协商不能在纯 eBPF 环境中完成。充其量,eBPF 可以做其中的一小部分工作,然后调用用户空间应用程序来处理对于 eBPF 来说过于复杂而无法处理的部分。
因为上文所述的 eBPF 的各项限制,七层流量仍然需要用户空间的网络代理来完成,eBPF 并不能替代服务网格。eBPF 可以与 CNI(容器网络接口)一起运行,处理三层/四层流量,而服务网格处理七层流量。
对于每个主机一个代理(per-host)的模式,服务网格的早期实践者 Linkerd 1.x 就是这么用的,笔者也是从那个时候开始关注服务网格,Linkerd 1.x 还使用了 JVM 虚拟机!但是经过 Linkerd 1.x 的用户实践证明,这种模式相对于 sidecar 模式,对于运维和安全来说会更糟糕。
为什么说 sidecar 模式比 per-host 模式更好呢?因为 sidecar 模式有以下几个优势,这是 per-host 模式所不具备的:
而对于 per-host 模式,就没有上述好处了。代理与应用程序 pod 完全解耦,处理主机上所有 pod 的流量,这样会代理各种问题:
简而言之,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 的疑问。 ↩︎
最后更新于 2024/12/11