# eBPF、Sidecar 和服务网格的未来

原文：[eBPF, sidecars, and the future of the service mesh](https://buoyant.io/blog/ebpf-sidecars-and-the-future-of-the-service-mesh)

作者：[William Morgan](https://twitter.com/wm)

eBPF 给云原生世界带来了很多变化。感谢 Cilium 之类的新技术，eBPF 已经成为了 Kubernetes CNI 的一个流行选择。[Linkerd](https://linkerd.io/?_gl=1*1sj56b2*_ga*MTQ4MzE1OTk3LjE2NjQ5OTE1NDg.*_ga_TV358ZPK6D*MTY2NTAxODcwNS4yLjEuMTY2NTAxODcwOC4wLjAuMA..#_ga=2.61978796.1857639420.1664991548-148315997.1664991548) 这样的服务网格产品也经常会[和 Cilium 或类似的 CNI 产品协同工作](https://buoyant.io/2020/12/23/kubernetes-network-policies-with-cilium-and-linkerd/)，从而同时在 7 层和 3/4 层分别得到 Linkderd 和 Cilium 的强大处理能力。但是 eBPF 的网络技术到底多强大？会强大到——例如替换 Linkerd 的 Sidecar Proxy，从而能在内核里完成所有操作吗？

本文中我会尽量进行评估，尤其会重点关注会对用户产生影响的部分。我会讲述 eBPF 是什么，能做什么不能做什么。我还会针对 Sidecar 和其它模型在运维和安全方面的能力进行深入对比。最后我会摆出我的结论——关于我们 Linkerd 团队，对 eBPF 参与下的服务网格的未来。

## 我是谁

大家好我是 William Morgan，Linkerd 的创建者之一。Linkerd 是第一个服务网格产品，也是服务网格这个词的定义者。我还是 Buoyant 的 CEO，该公司的使命就是在世界范围内推动 Linkerd 的采用。你可能会阅读过我的一些文字，例如 [The Service Mesh: What every software engineer needs to know about the world’s most over-hyped technology](https://buoyant.io/service-mesh-manifesto/) 或者 [A Kubernetes engineer’s guide to mTLS: Mutual authentication for fun and profit](https://buoyant.io/mtls-guide/)。

在 Linkerd 身上我投入了很多精力，这是我的偏爱。我同时也乐于实际参与该产品的实现过程。Linkerd 的最终目标是为用户简化服务网格技术，Linkerd 的实现细节就是为此服务的。例如 Linerd 1.x 时代使用的是主机级别的代理，而[出于安全和运维方面的考虑，我们换用了 Sidecar 模型](https://www.infoq.com/articles/linkerd-v2-production-adoption/)。我注意到，eBPF 可能让我们进一步的简化 Linkerd（尤其是在运维领域）。

## 什么是 eBPF

在扎到服务网格的细节之前，可以先从 eBPF 开始。这个铺天盖地的新技术到底是什么？

eBPF 是 Linux 内核的一个功能，应用借助 eBPF 可以自助地在内核中执行一些任务。eBPF 大放异彩的重要原因就是，它发迹于网络，但是其能力并不限于网络，eBPF 解放了一大类的网络可观测能力，因为性能影响，在过去这根本不可能实现。

假设要做一个能够处理网络数据包的应用。主机的网络 Buffer 是由内核管理和保护的，例如内核要保障一个进程无法读取 Buffer 中另一个进程的数据包。应用无法直接访问 Buffer，但是有一种被称之为 `syscall` 的机制，给应用一种调用内核功能的能力：应用调用 syscall，内核检查一下应用是否对目标数据包进行操作的权限，如果有，则完成调用。

Syscall 是可移植的（你的代码甚至可以在非 Linux 的机器上运行），不过比较慢。在现代网络环境之中，一个主机每秒钟都可能要处理海量的数据包，用基于 syscall 的代码来处理每个数据包是不现实的。

Syscall 代码需要在内核空间和用户空间之间进行数据传递，而 eBPF 会把代码直接交给内核执行。没有 syscall，应用能够全速运行——然而下面会提到，没这么简单：）

eBPF 是最近的内核功能之一，和 Linkerd 大量使用的 `io_uring` 一样，改变了应用程序和内核的互动方式。ScyllaDB 的 Glauber Costa 就此写了一篇 [`io_uring` 和 eBPF 如何改变 Linux 编程](https://www.scylladb.com/2020/05/05/how-io_uring-and-ebpf-will-revolutionize-programming-in-linux/)，推荐阅读。这些功能的工作方式差别很大：`io_uring` 使用一种特殊的数据结构，让应用和内核能够安全地共享内存；eBPF 则是让应用能够提交代码到内核。两种方式的目标都是获得超越 syscall 方式的性能。

eBPF 进步巨大，但也并非魔法。并不是任意代码都可以用 eBPF 的方式运行的。实际上出于种种考虑，eBPF 的能力是受到严格限制的。

## 多租户的难处

eBPF 的局限源自于内核，为什么存在 syscall 这样的东西呢？为什么程序不可以直接访问网络、内存或者磁盘呢？

内核所面对的是一个充满竞争的多租户世界。多租户中的租户可能是人、账号或者其他什么 Actor，多个租户分享同一个主机，各自运行各自的程序。不同租户不应该访问它人的数据，或者说互不干涉。内核既要保障程序的运行，又要维持秩序，换句话说，内核要对租户进行隔离。

这意味着内核不会完全信任任何程序。某个租户的程序，在任何时间点都可能尝试去破坏其它租户的程序或者数据。内核要确保未授权程序，不能停止或者打断其他程序、或者拒绝其资源使用、又或直接访问其它程序的网络、磁盘或者内存等。

这是一个致命需求。几乎所有软件相关的安全保证，最终都依赖于内核的这种能力。不经授权读取其他程序的内存或者网络流量，意味着被渗透或者更糟糕的情况；无授权的情况下写入其它程序的内存或者网络流量则意味着仿冒或者更大的麻烦。允许程序破坏规则的内核漏洞是一个非常严重的问题。要打破这种规则的最好方法就是获取对内核状态的访问——如果能够读写内核的内存，就能绕过这种规则。

隔离失败的后果难以承受，这就是应用和内核之间的交互被严格控制的原因。内核开发者们倾注了巨量精力。

这也是容器的力量之源——它们继承了同样的隔离保障，并将其应用到任意的应用程序和依赖包，得益于现代内核技术，我们能够用相互隔离的方式来运行程序，用完整的内核能力来处理多租户竞争场景。用虚拟机也能达成这种隔离，但是更慢、更昂贵。容器技术给了我们（几乎）一致的保证，并且成本大幅降低。

云原生中各个方面几乎都依赖于这种隔离能力。

## eBPF 的局限

回到 eBPF。就像前面讨论的，eBPF 让我们能够提交代码到内核并运行。从内核安全的角度来说，这是个非常恐怖的事情——这会穿越应用和内核之间的界限，直接面对安全威胁。

要提高安全性内核对这种代码提出了相当苛刻的限制。所有 eBPF 代码运行之前，都必须通过 `verifier` 这一关，它会对代码进行检查，识别其中的不当行为。内核只会运行通过检查的代码。

自动校验程序是个高难度的事情，所以 Verifier 会有宁杀错莫放过的倾向。因此 eBPF 代码受限颇多，例如不能阻塞、不能无限循环、不能超过预定义的尺寸；其复杂度也是受限的——Verifier 会对所有可能的执行路径进行评估，如果不能在某些限制下完成评估、或者不能证明每个循环都有退出条件，程序就无法通过校验。

有很多完美的安全程序无法满足这种限制。如果想要用 eBPF 的方式运行这些程序，就需要用 Verifier 的方式重写程序（或者提交 Patch 给 Verifier...）。如果你是 eBPF 粉丝，还是有好消息的，每次内核发布，Verifier 都会变得更具智能，这些限制也随之逐步放松。另外也有些创新的方法来应对这些限制。

即使如此，eBPF 的受限情况，也导致 eBPF 程序的应用场景非常有限。在 eBPF 程序中，就算是跨数据包缓冲数据也并非易事。更严重的情况，例如处理 HTTP/2 流量所需的全部代码就远远超出了纯 eBPF 的范围，终止 TLS 也是绝不可能。

最好的情况下，eBPF 能够分担其中一小部分工作，过于复杂的逻辑还是需要用户空间的程序来处理的。

## eBPF vs 服务网格

eBPF 说完了，再来说说服务网格。

服务网格负责处理现代化的云原生网络的复杂性。例如 Linkerd 的几个重要功能：[初始化并终结 TLS](https://buoyant.io/mtls-guide/)；跨越连接重试请求；透明地将连接从 HTTP/1.x 升级成 HTTP/2 从而提高性能；根据工作负载的身份进行访问控制；跨越 Kubernetes 集群发送流量，以及很多其他功能。

Linkerd 和多数服务网格一样，会在每个应用 Pod 间插入代理，这些代理会处理进出 Pod 的流量，从而完成网格能力。这些代理在自己的容器里运行，代理容器和应用容器伴行——这种模型被称为 Sidecar。Linkerd 的代理是[基于 Rust 实现的，轻量、快速](https://linkerd.io/2020/07/23/under-the-hood-of-linkerds-state-of-the-art-rust-proxy-linkerd2-proxy/?_gl=1*fjht61*_ga*MTQ4MzE1OTk3LjE2NjQ5OTE1NDg.*_ga_TV358ZPK6D*MTY2NTA2MjQyNy4zLjAuMTY2NTA2MjQyNy4wLjAuMA..#_ga=2.74669970.1857639420.1664991548-148315997.1664991548)，但是 Sidecar 并非全部。

十年前，要在集群里部署成百上千个代理服务器，并和每个应用的每个实例配对，绝对是个运维噩梦。但是感谢 Kubernetes，让这种想法得以实现。Linkerd 还让这些代理成为可管理的：Linkerd 的微代理不需要调谐，并且也实现了资源消耗的最小化。

在这个上下文中，eBPF 和服务网格融洽地相处了几年。Kubernetes 的贡献在于提供了一个可编排的、层次清晰的平台；eBPF 和服务网格非常适用于这种模型：CNI 负责 3、4 层流量，服务网格负责 7 层。

服务网格对平台所有者来说非常有帮助。它提供了 mTLS、请求重试、金指标等，这样开发者就无需自行实现这些能力了。但这也要付出大量部署运行代理的成本。

所以回到老问题：我们能做的更好吗？我们能够使用 `eBPF 服务网格`来代替代理服务器么？

## eBPF 服务网格还是需要代理服务器

根据前面对 eBPF 的铺垫，我们可以进行一些更深入的探讨。

不幸的是，触底非常快速：eBPF 的限制意味着，服务网格的完整能力（例如根据 Header 进行 HTTP/2 流量管理，初始化或者终结 mTLS 等）远远超出了纯 eBPF 方法的实现能力。

就算是在限制之内，用 eBPF 来实现也不见得就好。eBPF 编写困难、调试更难；这些网格功能的实现已经很难了，在一个有限的编程模型中实现就是难上加难。

所以不管在技术限制和软件工程实践来说，纯 eBPF 服务网格不可能实现。

但是 eBPF 结合用户空间代码的方式就能更好的应对复杂问题—— eBPF 负责一些专门问题，用户控件的代理服务器负责其他任务。

## 节点级代理对比 Sidecar

所以 eBPF 服务网格还是需要代理。但是是不是一定要 Sidecar 模型呢？如果我们用节点级代理——是不是能够有一个无 Sidecar、有 eBPF 的服务网格呢？

是的——不过这不是个好主意。我们在 Linkerd 1.x 中已经吃过这个苦头（对不起了，我们的早期用户）。相对于 Sidecar 来说，节点级代理对运维、管理和安全都是很不友好的。

Sidecar 模型中，所有进入应用实例的流量都会通过 Sidecar 代理。这种情况下，Sidecar 成为了应用的一部分：

- 代理的资源消耗是随应用负载变化的。进入实例的流量增长时，Sidecar 跟应用一样消耗更多的资源。如果应用流量很小，Sidecar 也不需要消耗太多资源（Linkerd 的代理的资源消耗最小仅有 2-3MB）。Kubernetes 的资源管理手段在这种场景下都有作用。
- 代理的爆炸半径被限制在 Pod 范围内。代理故障和应用故障一样，也可以被 Kubernetes 的 Pod 管理手段进行处理。
- 代理服务器的维护，例如版本升级同样可以用 Kubernetes 的滚动升级等机制来完成。
- 安全边界清晰，同样限制在 Pod 范围。Sidecar 跟应用实例共享同样的安全上下文。他们是同一 Pod 的组成部分，IP 共享。它对进出 Pod 的流量进行策略控制和 mTLS 发起和终结，并且只需要用到 Pod 的密钥物料。

在节点模型里，这些优势就不存在了。被 Kubernetes 调度到同一主机上的所有应用实例，流量都由这个节点级代理来处理。代理和应用完全解耦，产生了一些或大或小的问题：

- 代理服务器的资源消耗弹性巨大：资源水平取决于 Kubernetes 调度到本节点上的 Pod 数量。对特定代理来说，资源的消耗难于预测，也难于分析；这样一旦出了问题，服务网格团队将会因此受到指责。
- 应用易于受到“嘈杂邻居”的流量影响。节点上所有的流量都从同一个代理服务器经过，一个高流量 Pod 会耗尽代理服务器的所有资源，代理服务器必须确保资源的合理分配，否则应用将面临风险。
- 代理的爆炸半径变大，并且易变。代理服务器的故障和升级会影响到不同应用的不同实例，这意味着维护任务可能面临着难于预料的后果。
- 安全问题更加复杂。用 TLS 为例，节点级代理必须包含节点上所有应用的密钥物料，这就成了一种新的攻击目标，代理的 CVE 和漏洞都会造成大量的密钥泄漏。

简而言之，Sidecar 模式保留了容器化所承诺的隔离能力——内核能在容器一级完成安全和多租户隔离的工作。节点级模型破坏了这个边界，重新把多租户问题摆上了台面。

当然，节点级代理是有些优势的。例如 Sidecar 模型里，一次访问要穿过两个代理服务器，而节点级模型则只需要一次，很明显会降低延迟。可以用少量、高配置的代理服务器来应对高负载场景（Linkerd 1.x 就是一个好例子——大规模场景下表现突出，小规模下则不尽如人意）。你的网络架构图上可以少画不少盒子，显得简单很多。

但是这种优势比起安全和运维要面临的问题来说，就有些得不偿失了。为了弥补代理造成的性能损失，必须要把 Sidecar 做的又小又快。

## 只改进代理就可以了吗

节点级代理遇到的一些问题是伴随着多租户场景发生的。Sidecar 模式下，我们借用内核和容器的呢能力来应对这些问题。节点级代理模式下，没有这种能力可以依赖，那么——我们能不能对代理服务器进行改进，使之能够处理多租户竞争的场景呢。

答案是否定的。其实也不是不可以——理论上是可行的，实际上要投入大量的工作，并且也不是一个通用的能力。推上有个话题 [Some of what would have to be done](https://twitter.com/mattklein123/status/1522925333053272065)，进行了大量的这方面的讨论。需要完成大量的棘手任务，并且要不断地受到 Sidecar 模型的诱惑，持续地进行评估。

就算是完成了——还是要面对爆炸半径和安全性方面的问题。

## 服务网格的未来

综上所述——不论有没有 eBPF，在可预见的未来里，服务网格会是构建在用户空间中运行的 Sidecar 代理之上的。

Sidecar 当然不是完美的，但是目前条件下，要同时应对云原生网络的复杂性，以及容器的隔离性，最好的选择就是 Sidecar。eBPF 能卸载网格的一部分工作，它最合适的合作方是 Sidecar 而非节点级代理——让 Sidecar 变快的同时还能保持容器化的可管理性和安全性。eBPF 的目的不会是“用干掉 Sidecar 的方式来降低服务网格的复杂性并提高服务网格的性能”。

eBPF 的能力最终会进化到去掉代理直接处理服务网格的 7 层流量吗？有可能，但是基于上述原因，即使如此，也不可能放弃用户空间的代理服务器。内核会用一些机制来吸收服务网格的能力？可能——但是似乎没人想要一个网格内核，也看不到这种方向的前景。

所以在可预见的未来，Linkerd 会持续把精力投放到 Sidecar Proxy 的可维护、轻量化以及高性能方面，其中一个努力的方向就是在可能的情况下，向 eBPF 卸载部分工作。我们的本分就是为用户的 Linkerd 操作体验负责，从这个起点出发，来对每个设计和工程思维进行权衡。
