概述

这篇文章是多年以前的文章了,但是因为最近清理收藏夹才再次翻看,所以内容可能和最新的现状有些出入,请辩证阅读。

Sockmap 是 Linux 下用于 socket 拼接的一个工具,它可以在反向代理时不经过应用层而直接在内核就将流量反向代理到其他的目标端,这对于高负载的网络代理应用程序是一个很有意思并且很有探索价值的一个应用。

为什么叫 sockmap,首先是在 eBPF 中,数据的传递的数据结构就叫 Map,eBPF 提供了很多 Map,而 sockmap 是比较新的一种,由 Cilium 的工程师 John Fastabend 创建。

传统方式的弱点

为什么要使用 sockmap 呢?这就要回到没有 sockmap 之前我们遇到了什么问题。在 Linux 系统中,如果我们要在 socket 和其他数据流结构中扭转数据,常用系统调用的有:

系统调用 通信双方 需要用户空间内存吗 是否零拷贝
sendfile disk file —> socket yes no
splice pipe <—> socket yes yes?
vmsplice memory region —> pipe no yes

他们的问题分别是:

splice() 和 sendfile() 的额外成本

虽然 splice() 和 sendfile() 系统调用在一定程度上优化了 TCP 数据转发的性能,避免了内核与用户空间之间的数据复制,但仍然存在一些性能瓶颈:

  1. 系统调用的开销:splice() 和 sendfile() 都是系统调用,每次调用它们时,都需要在用户空间和内核空间之间进行上下文切换。这些上下文切换会增加额外的开销,从而影响性能。
  2. 多次数据缓冲区操作:虽然 splice() 和 sendfile() 可以在内核空间完成数据传输,但它们仍然需要多次对数据缓冲区进行操作。例如,在 splice() 中,数据需要从一个套接字的接收缓冲区移动到管道缓冲区,然后再移动到另一个套接字的发送缓冲区。这些额外的数据缓冲区操作可能会影响性能。
  3. 额外的内存分配和释放:splice() 和 sendfile() 需要在内核空间为数据传输分配和释放内存。这些内存操作可能导致额外的开销,并且在高负载情况下可能导致内存碎片。
  4. 无法完全避免数据复制:在某些情况下,sendfile() 无法完全避免数据复制。例如,当需要对数据进行处理(如加密)时,sendfile() 无法直接在内核空间完成这些操作,因此可能需要将数据复制回用户空间进行处理。

所以 splice() 和 sendfile() 在数据传输过程中减少了内核与用户空间之间的数据复制,但它们仍然存在一些性能瓶颈,如系统调用的开销、多次数据缓冲区操作、额外的内存分配和释放,以及在某些情况下无法完全避免数据复制。这些性能瓶颈可能限制了这些方法在高性能场景中的表现。

一些性能比较

系统调用次数 用户空间唤醒 拷贝次数
read write loop 2 syscalls yes 2 copies
splice 2 syscalls yes 0 copy (?)
io_submit 1 syscall yes 2 copies
SOCKMAP none no 0 copies

Clouflare 的实践

Cloudflare 在其边缘网络中使用 Sockmap 实现了 TCP Splicing。在这个网络中,每个请求都经过一个负载均衡器,负载均衡器将请求转发给后端服务器。通过使用 Sockmap,Cloudflare 能够在负载均衡器上实现高效的请求转发。

虽然 Sockmap 很有前途,但也存在一些限制和挑战,例如与其他内核特性的兼容性问题,以及对复杂网络环境的支持问题,例如:

测试数据

我不想使用文章中的数据,本来我想自己 bench 一下的,但是一直拖拉没有去实践,所以就先欠着,我会在后面我介绍 eBPF 开发的过程中,顺便把 bench 的程序写了,然后再进行一次 bench,然后再把它填补进来。

Ref