首页 / 文章 / SOCKMAP——未来的TCP拼接技术
← 返回
AI技术

SOCKMAP——未来的TCP拼接技术

✍️ zhirenhun 📅 2026/6/26 👁 77 阅读 ⏱ 21 分钟
SOCKMAP——未来的TCP拼接技术

SOCKMAP——未来的TCP拼接技术

最近,我们偶然发现了反向代理的“圣杯”——一个TCP套接字拼接API。这引起了我们的注意,因为正如你可能知道的,我们运营着一个全球性的反向代理服务网络。恰当的TCP套接字拼接可以减少用户态进程的负载,并实现更高效的数据转发。我们意识到Linux内核的SOCKMAP基础设施可以用于此目的。SOCKMAP是一个非常有前途的API,它很可能在像软件代理这类数据密集型应用的架构中引发一场颠覆性的变革。

SOCKMAP 架构示意

但让我们先回顾一下。

L7代理的诞生之痛

从用户态传输大量数据是低效的。Linux提供了一些专门设计的系统调用来解决这个问题。例如,sendfile(2)系统调用(Linus并不喜欢它)可用于加速将大文件从磁盘传输到套接字。还有传统的代理用来在两个TCP套接字之间转发数据的splice(2)。最后,vmsplice可以将内存缓冲区“粘”到管道中而无需复制,但很难正确使用。

遗憾的是,sendfilesplicevmsplice非常专门化、同步,并且只解决了问题的一部分——它们避免了将数据复制到用户态。它们未能解决其他效率问题。

函数        | 数据传输方向          | 避免用户态内存 | 零拷贝
sendfile    | 磁盘文件 --> 套接字    | 是             | 否
splice      | 管道 <--> 套接字      | 是             | 是?
vmsplice    | 内存区域 --> 管道     | 否             | 是

转发大量数据的进程面临三个问题:

  • 系统调用开销: 为每个转发的数据包执行多次系统调用代价高昂。
  • 唤醒延迟: 用户态进程必须频繁被唤醒以转发数据。根据调度器的不同,这可能导致较差的尾部延迟。
  • 复制开销: 将数据从内核复制到用户态,然后立即复制回内核并非没有成本,并且会累积成可观的代价。

在TCP套接字之间转发数据是一种常见做法。它用于:

  • 透明转发HTTP代理,如Squid。
  • 反向缓存HTTP代理,如Varnish或NGINX。
  • 负载均衡器,如HAProxy、Pen或Relayd。

多年来,已经有过许多尝试来降低Linux上TCP套接字之间盲目数据转发的成本。这个问题通常被称为“TCP拼接”、“L7拼接”或“套接字拼接”。

让我们比较一下通常的TCP拼接方法。为了简化问题,我们不编写一个功能丰富的七层TCP代理,而是编写一个简单的TCP回显服务器。

这不是开玩笑。一个回显服务器可以很好地说明TCP套接字拼接。你知道——“回显”基本上就是……将套接字与自身拼接!

朴素的TCP回显服务器看起来像这样:

while data:
    data = read(sd, 4096)
    writeall(sd, data)

没有比这更简单的了。在阻塞套接字上,这是一个完全有效的程序,并且可以正常工作。为完整起见,我在这里准备了完整的代码

Splice:专门的系统调用

Linux有一个令人惊叹的splice(2)系统调用。它可以告诉内核在套接字的TCP缓冲区和管道的缓冲区之间移动数据。数据保留在内核侧的缓冲区中。这解决了不必要地在用户态和内核态之间复制数据的问题。使用SPLICE_F_MOVE标志,内核甚至可能完全避免复制数据!

我们使用splice()的程序看起来像这样:

pipe_rd, pipe_wr = pipe()
fcntl(pipe_rd, F_SETPIPE_SZ, 4096);
while n:
    n = splice(sd, pipe_wr, 4096)
    splice(pipe_rd, sd, n)

我们仍然需要唤醒用户态程序并执行两次系统调用来转发任何数据片段,但至少我们避免了所有复制。完整源代码

io_submit:使用Linux AIO API

之前关于io_submit()的博客文章中,我们提议将AIO接口用于网络套接字。详情请阅读那篇博文,但这里是准备好的程序,它仅使用单个系统调用实现了回显服务器循环。

[图片] 由 jrsnchzhrs 提供 (By-Nd 2.0)

SOCKMAP:终极武器

近年来,Linux内核引入了eBPF虚拟机。借助它,用户态程序可以在内核上下文中运行专门的、非图灵完备的字节码。如今,可以为从数据包过滤到策略执行等数十种用例选择eBPF程序

从内核4.14开始,Linux获得了可用于套接字拼接的新eBPF机制——SOCKMAP。它由Cilium.io的John Fastabend创建,将Strparser接口暴露给eBPF程序。Cilium使用SOCKMAP进行七层策略执行,其使用的所有逻辑都嵌入在一个eBPF程序中。该API文档不完善,需要root权限,并且根据我们的经验,有些小问题。但它非常有前途。继续阅读:

以下是使用SOCKMAP的方法:SOCKMAP,或者具体说是“BPF_MAP_TYPE_SOCKMAP”,是一种eBPF映射类型。这个映射是一个“数组”——索引是整数。这一切都很标准。神奇之处在于映射的值——它们必须是TCP套接字描述符。

这个映射非常特殊——它附加了两个eBPF程序。你没看错:eBPF程序是附加到映射上的,而不是像通常那样附加到套接字、cgroup或网络接口上。以下是在用户程序中设置SOCKMAP的方法:

sock_map = bpf_create_map(BPF_MAP_TYPE_SOCKMAP, sizeof(int), sizeof(int), 2, 0)
prog_parser = bpf_load_program(BPF_PROG_TYPE_SK_SKB, ...)
prog_verdict = bpf_load_program(BPF_PROG_TYPE_SK_SKB, ...)
bpf_prog_attach(prog_parser, sock_map, BPF_SK_SKB_STREAM_PARSER)
bpf_prog_attach(prog_verdict, sock_map, BPF_SK_SKB_STREAM_VERDICT)

嗒哒!至此,我们建立了一个名为sock_map的eBPF映射,并附加了两个eBPF程序:parser和verdict。下一步是向这个映射添加一个TCP套接字描述符。没有比这更简单的了

int idx = 0;
int val = sd;
bpf_map_update_elem(sock_map, &idx, &val, BPF_ANY);

此时,奇迹发生了。从现在开始,每次我们的套接字sd接收到一个数据包时,prog_parserprog_verdict都会被调用。它们的语义在strparser.txt介绍性的SOCKMAP提交中有描述。为简单起见,我们简单的回显服务器只需要最小的存根。这是eBPF代码

SEC("prog_parser")
int _prog_parser(struct __sk_buff *skb)
{
    return skb->len;
}

SEC("prog_verdict")
int _prog_verdict(struct __sk_buff *skb)
{
    uint32_t idx = 0;
    return bpf_sk_redirect_map(skb, &sock_map, idx, 0);
}

附注:为了这个测试程序的目的,我编写了一个最小的eBPF加载器。它没有依赖项(既不需要bcc、libelf,也不需要libbpf),并且可以执行基本的重定位(比如解析上面提到的sock_map符号)。查看代码

bpf_sk_redirect_map的调用完成了所有工作。它告诉内核:对于接收到的数据包,请将其从某个套接字的接收队列重定向到位于sock_map中索引0下的套接字的发送队列。在我们的例子中,它们是同一个套接字!在这里,我们完全实现了回显服务器应该做的事情,但纯粹是在eBPF中完成的。

这项技术有多个好处。首先,数据永远不会被复制到用户态。其次,我们永远不需要唤醒用户态程序。所有操作都在内核中完成。很酷,不是吗?

我们还需要一段代码,让用户态程序挂起直到套接字关闭。最好使用经典的poll(2)来完成:

/* 等待套接字关闭。让SOCKMAP施展魔法。 */
struct pollfd fds[1] = {
    {.fd = sd, .events = POLLRDHUP},
};
poll(fds, 1, -1);

完整代码

至此,我们展示了四个简单的TCP回显服务器:

  • 朴素的读写循环
  • splice
  • io_submit
  • SOCKMAP

回顾一下,我们衡量的是三件事的成本:

  • 系统调用开销
  • 唤醒延迟(主要表现为尾部延迟)
  • 复制数据的成本

理论上,SOCKMAP应该击败所有其他方法:

| 方法        | 系统调用开销 | 唤醒用户态 | 复制成本 |
| 读写循环    | 2次系统调用  | 是         | 2次复制  |
| splice      | 2次系统调用  | 是         | 0次复制(?)|
| io_submit   | 1次系统调用  | 是         | 2次复制  |
| SOCKMAP     | 无          | 否         | 0次复制  |

这篇文章的这部分本应展示令人惊叹的数据,清晰地比较不同的方法。遗憾的是,基准测试很难,而且……SOCKMAP结果是最慢的。发布负面结果很重要,所以这里就是。

我们的测试平台如下:

  • 两台裸机Xeon服务器,通过25Gbps网络连接。
  • 两者都禁用了睿频加速,测试程序都绑定了CPU。
  • 为了更好的局部性,我们将RX和TX队列分别定位到一个IRQ/CPU。
  • 测试服务器运行一个脚本,发送10k批固定大小的数据块。该脚本测量回显服务器返回流量所需的时间。
  • 对于每个被测的回显服务器程序,我们进行10次独立的运行。
  • TCP:“cubic”和NONAGLE=1。
  • 两台服务器都运行4.14内核。

我们对实验数据的分析发现了一些异常值。我们认为一些最差的时间,表现为长时间的回显回复,是由不相关的因素引起的,例如网络丢包。在展示的图表中,我们(也许有争议地)跳过了底部1%的异常值,以便专注于我们认为重要的数据。

此外,我们在SOCKMAP中发现了一个错误。一些运行被延迟了高达64毫秒。以下是其中一次测试:

Values min:236.00 avg:669.28 med=390.00 max:78039.00 dev:3267.75 count:2000000
Values:
value |-------------------------------------------------- count
1    | 0
2    | 0
4    | 0
8    | 0
16   | 0
32   | 0
64   | 0
128  | 0
256  | 3531
512  |************************************************** 1756052
1024 | ***** 208226
2048 | 18589
4096 | 2006
8192 | 9
16384| 1
32768| 0
65536| 11585
131072| 1

绝大多数回显运行(本例中为128KiB)在512us频段内完成,而一小部分则停滞了65ms。这非常糟糕,使得SOCKMAP与其他实现的比较毫无意义。这是我们从所有运行中跳过1%最差结果的第二个原因——它使SOCKMAP的数字更有用。抱歉。

我们最快的程序在一个流上达到了约15Gbps,这似乎是硬件限制。这在第一次迭代中非常明显,它显示了我们回显程序的吞吐量。

这个测试显示:通过我们测试的回显服务器传输和接收2MiB数据块的时间。我们重复此操作10k次,并运行测试10次。在剔除最差的1%数字后,我们得到以下延迟分布:

该图表显示,对于2MiB块的TCP回显服务器,朴素的read+write和io_submit程序都能够实现1500us的平均往返时间。

在这里我们清楚地看到,splice和SOCKMAP比其他方法慢。它们受CPU限制,无法达到线速。我们过去曾提出过不寻常的splice性能问题,但也许我们应该再次调试它。

对于每个服务器,我们运行两次测试:不设置和设置SO_BUSYPOLL。此设置应消除“唤醒延迟”并大大减少抖动。结果表明,朴素方法和io_submit测试几乎相同。这太棒了!BUSYPOLL确实减少了偏差和延迟,代价是更多的CPU使用。请注意,splice和SOCKMAP都不受此设置的影响。

16KiB块 - 唤醒时间

我们的第二轮测试使用更小的数据大小,一次发送16KiB的小块。这个测试应该能说明被测程序的“唤醒时间”。

在这个测试中,所有程序在非BUSYPOLL运行下的表现看起来非常相似(最小值和最大值),SOCKMAP是个例外。这很好——我们可以推测唤醒时间是可比的。令人惊讶的是,splice的中位时间略好于其他方法。也许这可以用CPU的伪像来解释,比如由于数据复制更少而具有更好的CPU缓存局部性。SOCKMAP再次成为最慢的,具有最差的最大值和中位时间。嘘。

请记住,我们截断了最差的1%数据——我们人为地缩短了“最大值”。

在这篇博文中,我们讨论了SOCKMAP的理论优势。遗憾的是,我们注意到它还没有准备好投入生产。我们将其与splice进行了比较,发现splice没有从BUSYPOLL中受益,并且性能令人失望。我们注意到朴素的读/写循环和iosubmit方法具有完全相同的性能特征,并且确实受益于BUSYPOLL以减少抖动(唤醒时间)。

如果你正在TCP套接字之间传输数据,你绝对应该看看SOCKMAP。虽然我们的基准测试表明它还没有准备好投入生产,性能不佳、抖动高并且有几个错误,但它非常有前途。我们对此感到非常兴奋。它是Linux上第一个真正允许用户态进程将TCP拼接卸载到内核的技术。它还具有比其他方法更好性能的潜力,满足了异步、仅在内核中运行以及完全避免不必要数据复制等所有条件。

这还不是全部。SOCKMAP能够跨多个套接字传输数据——你可以想象一个完整的连接网格能够发送数据。


原文出处:SOCKMAP - TCP splicing of the future

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

Cloudflare SOCKMAP TCP拼接 eBPF Linux内核
← 上一篇
Un-0:用耦合振荡器生成图像
下一篇 →
Zig 的新 bitCast 语义与 LLVM 后端改进

📌 相关推荐

qMLX:通过极致优化Mac Studio最大化我的AI幻觉
2026/7/12
Mesh LLM:基于iroh的分布式AI计算
2026/7/12
结构化输出 vs 工具使用 vs 预填充:2026年从Claude获取JSON
2026/7/11
← 返回文章列表