本文地址:https://www.ebpf.top/post/cloudflare-replatforming-3

原文地址:https://ebpf.io/blog/cloudflare-replatforming-3/
作者:Cloudflare Engineering
发布时间:2026-09-23
翻译:狄卫华
许可:原文采用 CC BY 4.0 许可,本文为其中文翻译

本系列记录 Cloudflare 与 eBPF 的八年历程:从专用的 DDoS 工具,成长为其全球网络的可编程骨干。内容涵盖向 Linux 内核贡献代码、技术挑战、业务投资回报(ROI),以及给其他希望采用 eBPF 的公司的建议与运维策略。

在第 1 部分中,我们说明了 Cloudflare 为何把 eBPF 当作一个平台来押注,而不是一堆单点方案。在第 2 部分中,我们考察了标准 Linux 网络 API 无法支撑 Cloudflare 需求的那些地方,以及团队为填补这些缺口所构建的内容。本文将深入探讨团队在落地 eBPF 过程中面临的技术挑战。

基于标准 Linux 网络 API 构建只能走到一定程度;要突破这些限制,需要团队愿意深挖、坚持不懈。从为了让代码合入 Linux 主线内核而经历的长达 13 个月、漫长而艰苦的组织层面征程,到在一线调试「不可能发生」的报文损坏,本文将详述那些陡峭的挫折与死胡同,以及最终换来的五项开创性技术成果。

1. 最艰难的架构挑战:长达 13 个月的上游合入组织征程

耗时最长、组织难度最大的障碍,是通过把 sk_lookup BPF 程序类型合入 Linux 主线内核,来弥补 BSD 套接字 API 的缺陷。最初只是一个「让套接字查找可编程」的简单想法,最终却演变成一段 13 个月的旅程。

Cloudflare 需要在数千台服务器、数百万个 IP 上运行服务。标准 Linux 套接字迫使我们二选一:要么逐个绑定每个 IP(创建数百万个套接字,这会拖垮性能),要么绑定到 0.0.0.0(这又使得无法在同一端口、不同 IP 上运行多个服务)。我们最初采用 TPROXY 之类的变通方案来引导流量,但这需要提升权限,并且与网络协议栈之间存在微妙的相互作用。我们还多年维护着未被上游接受的自定义内核补丁(即自 2017 年起一直携带的 SO_BINDTOPREFIX)。在推动补丁合入上游的过程中,我们多次遭到社区的反对。

阻力一——方向错了: 第一版 RFC(RFCv1)的设计是让 BPF 程序修改报文的四元组(4-tuple),以此影响套接字查找。内核评审者在 Netconf 2019 上否决了这一方案,并引导团队转向「基于 map 的重定向」,即改为从 BPF map 中选择套接字。整个设计不得不为 RFCv2 推倒重来。

阻力二——SOCKMAP 尚未就绪: 团队原计划使用 REUSEPORT_SOCKARRAY map,但评审者要求改用 SOCKMAP,而当时 SOCKMAP 根本不支持 UDP 套接字或处于监听状态的 TCP 套接字。团队不得不先为 SOCKMAP 实现 UDP 支持(历经 4 个补丁版本,合入 Linux 5.7),真正的 sk_lookup 工作才得以推进。这是一个原计划中并不存在的完整中间项目。

阻力三——需要新的挂载模型: 就在团队忙于 SOCKMAP 时,Linux 5.7 引入了基于 bpf_link 的挂载方式,BPF 维护者随之采纳了一项新政策:所有新的程序类型都必须使用 bpf_link。而当时这套基础设施只支持 cgroup 与跟踪类程序,不支持网络命名空间。团队不得不为网络命名空间挂载实现 bpf_link,并加入多程序支持以便面向未来。

阻力四——API 语义不断演变: 套接字选择机制从基于 map 的选择,变成了使用 bpf_sk_assign 的两步式方案,同时还要与 reuseport 负载均衡集成,并新增 SK_DROP 裁决码。

整个工作包含 10 余个独立的上游补丁系列。内核工作需要坚持。有了想法并写出代码,可能只是开始;真正的工作从维护者和评审者的反馈到来时才开始。这可能带来意料之外的工作,有时其规模大到足以构成一个中间里程碑。这会让人沮丧:明明你在朝终点线推进,球门却不知怎的总在移动。

组织层面的挑战在于,要在一年多的时间里维持这项多人协作的工作,而那些依赖 sk_lookup 的生产系统(Tubular、Spectrum 集成、bind-to-prefix 的替代方案)则一直在等待。sk_lookup 钩子(hook)最终合入 Linux 5.9,如今每一位 Linux 用户都可以使用。Cloudflare 在其之上构建了 Tubular:一个控制平面,可将服务绑定到任意前缀,并在不重启应用的情况下即时变更 IP 分配。

2. 最艰难的调试挑战:「不可能发生」的报文损坏

技术难度最高的一次调试,涉及 TCP 防护系统 flowtrackd。该系统开始因报文损坏而崩溃,而这种损坏在理论上似乎不可能发生:合法的 TCP 报文在被接收之后、被应用处理之前,遭到了截断或损坏。

应用第一次解析某个报文能够成功,但片刻之后对同一内存缓冲区的第二次解析却会 panic,因为数据已经变了。这意味着可能是内存损坏、编译器 bug,或者内核 bug。

这个问题极为罕见:在一个持续运行压力测试的 10 台虚拟机集群上,每天只能复现一次。由于损坏发生在驱动层,标准调试工具都无能为力。

为了定位问题,团队使用 ftrace 和 kprobe 在内核中构建了一个定制的「飞行记录仪」,用来跟踪单个报文描述符在内核环形缓冲区(ring buffer)中流转的完整生命周期。跟踪数据揭示了 Linux veth(虚拟以太网)驱动中的一个竞态条件:该驱动违反了 NAPI 的保证,使得内核在应用仍在读取某个内存缓冲区时就将其「刷新」(复用)了。

为解决该问题,Cloudflare 工程师为 Linux 内核打了补丁,确保在缓冲区被释放之前报文处理已经完成。该补丁已合入上游。

3. 五项开创性的技术成果

并不存在某一项「最令人印象深刻」的成果。Cloudflare 用 eBPF 解决了类型截然不同的问题,从原始报文处理速度,到架构可扩展性,再到内核 API 设计。有五项具体成果因各自不同的原因,在技术上堪称开创性。

3.1 向 Linux 主线内核贡献 sk_lookup 并构建 Tubular

架构意义最重大的成果,是创建 sk_lookup BPF 程序类型并使其被上游接受(合入 Linux 5.9)。它在不直接修改内核套接字查找代码的前提下,从根本上扩展了 BSD 套接字 API。Cloudflare 的 Anycast 架构要求每台服务器监听数百万个 IP,而 30 年前设计的标准 bind() 与 listen() API 无法扩展到这种规模。

sk_lookup 钩子允许用户态对内核的套接字查找逻辑进行编程。Cloudflare 在其之上构建了 Tubular,这是一个控制平面,可在不重启应用的情况下,把服务(如 HTTP 或 DNS)即时绑定到数百万个 IP 或互不相连的 IP 段上。如今,Tubular 在 Cloudflare 的每一台边缘服务器上分发流量。

3.2 「软单播」(Soft-Unicast)——解决 IPv4 地址枯竭

或许最具创新性的架构壮举,是将 IP 地址与物理服务器解耦。Cloudflare 用 eBPF 实现了「软单播」(Soft-unicast),可以把单个 IPv4 地址分配给一个数据中心,再按端口范围把它切分给多台物理服务器。

标准网络会把一个 IP 绑定到单个接口上。Cloudflare 的 Unimog 负载均衡器(XDP)会检查入站报文的端口,并将其分发给拥有该端口切片的具体机器。这使得单个 /32 IP 地址可以由数十台物理服务器共享,从而应对日益严峻的 IPv4 地址枯竭问题,并让服务器集群在扩容时无需按比例增加 IP 地址。

3.3 在通用硬件上每秒丢弃 800 万个报文

就原始工程性能而言,l4drop 系统(XDP)是一项突出的成果。它用运行在标准 Linux 内核上的软件,取代了依赖硬件的专有方案。在生产环境中,一台使用 l4drop 的服务器在一次容量型攻击期间成功丢弃了超过每秒 800 万个报文。即便入站报文量激增 40 倍,整体 CPU 使用率也只上升了约 10%,这证明「通用硬件 + eBPF」可以胜过专用的专有方案。

3.4 udpgrm——解决「无解」难题:零停机 UDP 重启

udpgrm(UDP Graceful Restart Marshal)解决了一个多年来被反复讨论、多次尝试均告失败的问题。核心难点在于 UDP 是无状态的,因此在新旧进程之间并不存在类似 TCP listen/accept 那样的交接机制。

udpgrm 同时钩入六个不同的 BPF 挂载点:

  • BPF_CGROUP_INET4_BIND / BPF_CGROUP_INET6_BIND——在 bind 时创建每套接字状态
  • BPF_CGROUP_GETSOCKOPT / BPF_CGROUP_SETSOCKOPT——完全在 eBPF 中实现一套自定义套接字 API(UDP_GRM_WORK_GEN、UDP_GRM_SOCKET_GEN),无需修改内核
  • BPF_CGROUP_UDP4_SENDMSG / BPF_CGROUP_UDP6_SENDMSG——在发送时「确认」(affirm)流,即将其加入流跟踪表
  • 外加一个 SO_ATTACH_REUSEPORT_EBPF 程序,负责实际的套接字选择

该系统管理多个(而不只是两个)套接字「世代」,维护一张流表以保证连接粘滞性,并支持两种解析模式:通用的流哈希(flow-hash),以及感知 QUIC 的基于 cookie 的路由(在 QUIC DCID 中嵌入一个 3 字节的 cookie,无需查流表即可直接选中应用套接字)。

3.5 借助 ebpf_exporter 实现从内核到应用的分布式追踪

ebpf_exporter 中的分布式追踪集成以一种新颖的方式,打通了两个传统上毫无关联的世界:内核层的报文处理与应用层的请求追踪。

像 nginx 这样的用户态应用内置了 USDT 探针,把 OpenTelemetry 的 trace ID 与 span ID 从应用传入内核。eBPF 程序挂载到这些 uprobe 上【译者注:USDT 探针在运行时通过 uprobe 机制挂载】,并把追踪上下文存入以套接字指针为键的 BPF_MAP_TYPE_HASH 中。当被追踪的套接字上发生内核事件(TCP 重传、CFS 限流、调度器事件)时,BPF 程序会用已存储的追踪上下文创建 span,并通过环形缓冲区推送到用户态。ebpf_exporter 解码这些事件,并将其作为 OpenTelemetry span 发送给本地的 tracefwdr。最终在 Jaeger/Tempo 中得到一条完整的 trace,把 nginx 请求处理与内核 TCP 重传展示为相互关联的 span。

4. 结语

突破 Linux 内核的极限绝非易事。正如这些挑战所展示的,用 eBPF 对一张全球网络进行平台化改造,需要的远不止写出巧妙的代码。它需要深厚的技术韧性、面对不断移动的球门时的坚持,以及与更广泛的开源社区协作的决心。

无论是走完 sk_lookup 长达 13 个月的上游合入流程,将物理服务器与 IP 解耦以应对 IPv4 地址枯竭,还是实现「无解」的零停机 UDP 重启,这些来之不易的工程战役都从根本上改变了我们的架构。但技术成果只是故事的一半。在第 4 部分中,我们将探讨这些投入如何在实践中得到回报:更快的事故响应、更深入的可观测性,以及一类全新的运维可见性。

延伸阅读

若还想顺着读,可看这几篇:

  1. Cloudflare 平台重塑:从硬件厂商锁定到可编程网络 eBPF 平台(上)
  2. Cloudflare 平台重塑:用 eBPF 填补 Linux 生态缺口(下)
  3. Cloudflare 基于 BPF LSM 热修高危提权 Copy Fail