概述

在这篇文章中,我将使用一个简单的 HTTP 基准测试在 Linux 内核的网络堆栈和由 DPDK 提供支持的内核绕过堆栈之间进行头对头的性能比较。我将使用Seastar运行我的测试,这是一个用于构建高性能服务器应用程序的 C++ 框架。Seastar 支持构建使用 Linux 内核或 DPDK 进行网络的应用程序,因此它是进行这种比较的完美框架。

我将基于我之前的性能调整帖子中的许多想法和技术,因此在继续之前至少阅读概述部分可能是值得的。

Linux内核与DPDK之HTTP性能对决插图

保护内核

绕过内核可以打开一个高吞吐量和低延迟的全新世界。根据您询问的对象,您可能听说绕过内核将导致 3-5 倍的性能提升。然而,这些比较中的大多数都是在内核方面没有太多优化的情况下完成的。

Linux 内核被设计为快速,但它也被设计为多用途,因此默认情况下它并未针对高速网络进行完美优化。另一方面,像 DPDK 这样的内核绕过技术对网络性能采取一心一意的方法。整个网络接口专用于单个应用程序,并使用积极的繁忙轮询来实现高吞吐量和低延迟。在这篇文章中,我想看看当一个经过微调的内核/应用程序与内核绕过在无限制的战斗中正面交锋时,性能差距会是什么样子。

DPDK 倡导者建议绕过内核是必要的,因为内核“慢”,但实际上 DPDK 的许多性能优势并非来自绕过内核,而是来自强制执行某些约束。事实证明,这些优点中的许多都可以在仍然使用内核的同时实现。通过关闭某些功能,打开其他功能并相应地调整应用程序,可以实现接近内核绕过速度的性能。

以下是一些也可以使用内核完成的 DPDK 策略:

  • 忙轮询(中断调节 + net.core.busy_poll=1)
  • 完美的局部性(RSS + XPS + SO_REUSEPORT_CBPF)
  • 简化的 TCP/IP 子系统(禁用 iptables/syscall 审计/AF_PACKET 套接字)

内核绕过技术仍然具有的一个优点是,它们避免了在用户空间和内核之间来回转换(和复制数据)所产生的系统调用开销。所以DPDK应该还是有整体优势的,但问题是,优势有多大。

路线图

这篇文章很长,所以这里有一个高级大纲,以防你想跳到一个特定的兴趣领域。

入门

  • 建造海星
  • 基准设置

DPDK 设置和优化

  • AWS 上的 DPDK
  • DPDK 优化

内核堆栈优化

  • 基准内核性能
  • 操作系统级别优化
  • 完美的位置和繁忙的轮询(需要多次尝试才能使其正常工作)
  • 恒定上下文切换
  • 最好是RECV
  • 记得刷新

结果、注意事项和好奇心

  • 最终获胜者是…
  • DPDK 警告
  • 推测性执行缓解措施

结束

  • 结论
  • 附录

建造Seastar

最初建造 Seastar 时我遇到了一些挑战。我想使用 Amazon Linux 2,因为我对它非常熟悉,但很明显,我正在与过时的依赖项打一场失败的战斗。尽管有一些问题,我切换到 vanilla CentOS 8 并设法让它运行,但我仍然觉得我的基础不够稳固。在与 CentOS Stream 9 进行了短暂的交流后,我在公共 Slack 频道寻求帮助,我被指向 Fedora 34 作为构建最新版本代码库的最佳操作系统的方向。

实际上,我使用 Fedora 34(内核 5.15)完成了我的大部分研究和测试,但是虽然 Fedora 可能非常适合其尖端更新,但有时尖端会变成最前沿。当我决定从头开始重现我的结果时,我意识到最新的 Fedora 34 更新正在将内核从 5.11 直接升级到 5.16 版本。不幸的是,内核 5.16 为我的测试触发了性能回归,所以我需要一个替代方案。

事实证明,Amazon Linux 2022基于 Fedora 34,但内核更新政策更为保守,选择坚持使用 5.15 LTS 版本,因此我选择 AL 2022 作为这些测试的新基础操作系统修正主义历史,在这篇文章的其余部分,我将假装我一直在使用它。

HTTP 服务器

我开始使用 Seastar 的内置 HTTP 服务器 ( httpd ) 进行测试,但我决定从 httpd 降低到使用只是伪装成 HTTP 服务器的准系统 TCP服务器。服务器只是发回一个固定的 HTTP 响应而不做任何解析或路由。这简化了我的分析,并更清楚地突出了我所做的每个更改的效果。特别是我想从等式中消除 Seastar 的内置 HTTP 解析器。在我删除它之前,性能会因客户端发送的 HTTP 标头数量而显着不同。因此,我没有去寻找那里发生的事情的兔子洞,而是决定使用我的简单tcp_httpd服务器来作弊。

源代码

根据我为这个项目所做的工作,我在 Seastar 主要 repo 上打开了一些 PR,但由于它们依赖于 epoll,大多数更改都不适合上游,并且当前的开发现在集中在 aio 和 io_uring 上。这篇文章中使用的所有补丁都可以在我在 GitHub 上的 Seastar 存储库中找到。

基准设置

这是 AWS 上基准设置的基本概述。我使用Techempower JSON 序列化测试作为该实验的参考基准。

硬件

软件

  • 操作系统:Amazon Linux 2022(内核 5.15)
  • 服务器:我的简单tcp_httpd服务器:sudo ./tcp_httpd --reactor-backend epoll
  • 客户:我对流行的 HTTP 基准测试工具 wrk 做了一些修改,并给它起了个绰号twrk。twrk 在短时间、低延迟的测试运行中提供更一致的结果。wrk 的标准版本在吞吐量方面应该会产生相似的数字,但 twrk 允许改进p99 延迟,并增加了对显示 p99.99 延迟的支持。

基准配置

我使用以下参数从客户端手动运行 twrk:

  • 没有流水线
  • 256 个连接
  • 16 个线程(每个 vCPU 1 个),每个线程固定到一个 vCPU
  • 统计数据收集开始前 1 秒预热,然后测试运行 5 秒
twrk --latency --pin-cpus -H 'Host: server.tld' "http://172.31.XX.XX:8080/json" -t 16 -c 256 -D 1 -d 5

AWS 上的 DPDK

让 Seastar 和 DPDK 在 AWS 上工作绝非易事。AWS ENA 驱动程序的DPDK 文档最近有了显着改进,但在我开始时有点粗糙,而且很难找到将 Seastar 与 DPDK 一起使用的工作示例。值得庆幸的是,在 Slack 频道的帮助和我顽固的坚持之间,我能够让事情顺利进行。

以下是那些希望做同样事情的人的一些亮点:

  1. DPDK 需要能够接管整个网络接口,因此除了通过 SSH (eth0/ens5) 连接到实例的主接口之外,您还需要附加一个专用于 DPDK 的辅助接口 (eth1/ens6) .
  2. DPDK 依赖于两个可用的内核框架之一,用于将直接设备访问权限暴露给用户空间、VFIO或UIO。VFIO 是推荐的选择,它在最近的内核上默认可用。默认情况下,VFIO 依赖于硬件 IOMMU 支持以确保以安全的方式进行直接内存访问,但是 IOMMU 支持仅适用于 *.metal EC2 实例。对于非金属实例,通过enable_unsafe_noiommu_mode=1加载内核模块时的设置,VFIO 支持不使用 IOMMU 运行。
  3. Seastar 使用 DPDK 19.05,在这一点上有点过时。AWS ENA 驱动程序有一组适用于 DPDK 19.05 的补丁程序,必须应用这些补丁程序才能让 Seastar 在 AWS 上运行。为了方便起见,我将补丁反向移植到我的DPDK 分支中。
  4. 最后但同样重要的是,我在 DPDK/ENA 驱动程序中遇到了导致以下错误消息的错误:runtime error: ena_queue_start(): Failed to populate rx ring. 这个问题去年在 DPDK 代码库中得到修复,所以我将更改反向移植到我的 DPDK 分支。

使用 tcp_httpd 应用程序,我使用 DPDK 作为底层网络堆栈运行我的基准测试:

sudo ./tcp_httpd --network-stack native --dpdk-pmd
Running 5s test @ http://172.31.12.71:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   205.32us   36.57us    1.34ms   62.00us   69.36%
    Req/Sec    74.80k     1.81k    77.85k    69.06k    73.85%
  Latency Distribution
  50.00%  204.00us
  90.00%  252.00us
  99.00%  297.00us
  99.99%  403.00us
  5954189 requests in 5.00s, 0.86GB read
Requests/sec: 1190822.80

DPDK 性能一开始就以令人印象深刻的 119 万请求/秒进入。

初始火焰图

火焰图提供了一种独特的方式来可视化 CPU 使用情况并识别应用程序最常用的代码路径。它们是一个强大的优化工具,因为它们可以让您快速识别和消除瓶颈。单击下面的图像将打开由Flamegraph 工具生成的原始 SVG 文件。这些 SVG 是交互式的。您可以单击细分以深入查看更详细的视图,也可以搜索(Ctrl + F 或单击右上角的链接)查找函数名称。请注意,每个完整的火焰图都捕获了代表 4 个反应器线程(每个 vCPU 一个)的四个几乎相同的堆栈,但在整个帖子中,我们将主要关注分析单个反应器/vCPU 的数据。

Linux内核与DPDK之HTTP性能对决插图1

火焰图分析

快速查看火焰图就足以看出该eth_ena_xmit_pkts函数看起来非常大,占总火焰图的 53.1%。

DPDK 优化

在第 5 代以上实例上,ENA 硬件/驱动程序支持 LLQ(低延迟队列)模式以提高性能。在使用这些实例时,强烈建议您启用相应内核模块(VFIO 或 UIO)的写入组合功能,否则会因 PCI 事务缓慢而影响性能。

VFIO 模块默认不支持写入组合,但 ENA 团队提供了一个补丁和一个脚本来自动化向内核模块添加 WC 支持的过程。我最初在使用内核 5.15 时遇到了一些 问题,但 ENA 团队对修复它们非常敏感。该团队最近还表示,他们打算将 VFIO 补丁升级到上游,这有望在未来让事情变得更加轻松。

Running 5s test @ http://172.31.12.71:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   153.79us   31.63us    1.43ms   52.00us   68.70%
    Req/Sec    95.18k     2.31k   100.94k    89.75k    68.88%
  Latency Distribution
  50.00%  152.00us
  90.00%  195.00us
  99.00%  233.00us
  99.99%  352.00us
  7575198 requests in 5.00s, 1.09GB read
Requests/sec: 1515010.51

启用写入合并将性能从 1.19M req/s 提高到 1.51M req/s,性能提升 27%。

火焰图 - 带有写入组合的 DPDK

火焰图分析

我们的火焰图现在看起来更加平衡,eth_ena_xmit_pkts从火焰图的 53.1% 下降到仅 6.1%。

高要求

DPDK 以绝对大规模的表演打破了挑战。在4 个 vCPU实例上每秒 151 万个请求是巨大的。内核甚至可以接近吗?

基准内核性能

从未经修改的 AL 2022 AMI 开始,tcp_httpd性能开始时约为 358k req/s。从绝对意义上来说,这确实非常非常快,但相比之下它就显得平淡无奇了。

Running 5s test @ http://172.31.XX.XX:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   711.06us   97.91us    1.65ms  108.00us   70.06%
    Req/Sec    22.48k   205.46     23.10k    21.83k    68.62%
  Latency Distribution
  50.00%  696.00us
  90.00%    0.85ms
  99.00%    0.96ms
  99.99%    1.10ms
  1789658 requests in 5.00s, 264.55MB read
Requests/sec: 357927.16
Linux内核与DPDK之HTTP性能对决插图3

操作系统级别优化

我不会详细介绍我所做的特定 Linux 更改。在高层次上,这些更改在本质上与我在上一篇文章中对 Amazon Linux 2/kernel 4.14 所做的调整非常相似。话虽如此,从内核 4.14 和 5.15 开始,这个工作负载实际上出现了显着的性能下降,并且需要做很多工作才能使性能恢复到标准水平。但我现在想专注于内核与 DPDK 的比较,所以我将把这些细节留到另一天,另一篇文章。以下是使用的操作系统优化的高级概述:

  • 禁用推测执行缓解
  • 配置 RSS 和 XPS 以获得完美的位置
  • 中断调节和忙轮询
  • 禁用原始/数据包套接字(仅供参考,这次不是同一个爱管闲事的邻居)
  • GRO、拥塞控制和静态中断调节
  • 一些新的优化

我们的操作系统优化将吞吐量从 358k req/s 提高到了惊人的 726k req/s。坚实的 103% 性能提升。

Running 5s test @ http://172.31.XX.XX:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   346.76us   86.26us    1.51ms   62.00us   72.62%
    Req/Sec    45.61k     0.88k    48.82k    42.50k    70.15%
  Latency Distribution
  50.00%  347.00us
  90.00%  455.00us
  99.00%  564.00us
  99.99%  758.00us
  3630818 requests in 5.00s, 536.71MB read
Requests/sec: 726153.58
火焰图 - 操作系统优化

完美的位置和繁忙的投票

在正确配置应用程序之前,操作系统级别的更改以启用完美的局部性/繁忙轮询实际上并没有太大影响。我的下一步是为我的 Seastar 分支添加SO_ATTACH_REUSEPORT_CBPF支持,以便完成完美的位置设置。

Running 5s test @ http://172.31.XX.XX:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   338.93us   90.62us    1.56ms   61.00us   68.11%
    Req/Sec    46.57k     2.67k    54.00k    40.32k    64.29%
  Latency Distribution
  50.00%  330.00us
  90.00%  466.00us
  99.00%  562.00us
  99.99%  759.00us
  3706485 requests in 5.00s, 547.89MB read
Requests/sec: 741286.62

吞吐量从 736k req/s 下降到 741k req/s。2% 的性能提升低于我对这一变化的预期。

Linux内核与DPDK之HTTP性能对决

火焰图分析

火焰图显示繁忙轮询的零证据。完美的局部性和繁忙的轮询在一个良性循环中协同工作,因此缺少繁忙的轮询是我们的设置出现问题的一个强有力的指标。完美的局部性需要配置操作系统和应用程序,以便一旦网络数据包到达给定队列,所有进一步的处理都由相同的 vCPU/队列筒仓处理传入和传出数据。这意味着必须控制进程/线程的启动顺序,以及它们被固定到的 CPU。

完美的位置和繁忙的投票:拿两个

我创建了一个bftrace 脚本来仔细查看实际发生的情况。该脚本将 kprobes 附加到reuseport_alloc()reuseport_add_sock()跟踪进程/线程启动顺序和 cpu 亲和性。结果立即显示了问题。即使反应器线程是按顺序启动的(tcp_httpd/reactor-0、reactor-1、reactor-2、reactor-3),CPU pinning 也是无序的(0、2、1、3)。

tcp_httpd, cpu=0, socket 0
reactor-1, cpu=2, socket 1
reactor-2, cpu=1, socket 2
reactor-3, cpu=3, socket 3

进一步调查显示,Seastar 用于hwloc了解硬件拓扑并进行相应优化。但是默认的 CPU 分配策略对于我们的用例来说并不是最优的,所以在邮件列表中提出了这个问题之后,我在我的 fork 中添加了一个函数,它将反应堆 shard ids 和 cpu ids 之间的映射暴露给基于 Seastar 构建的应用程序。

我修改了 tcp_httpd以确保 cpu id 和套接字 id 匹配。这导致了我的 bpftrace 脚本的预期输出。

tcp_httpd, cpu=0, socket 0
reactor-2, cpu=1, socket 1
reactor-1, cpu=2, socket 2
reactor-3, cpu=3, socket 3

性能略有提高,但仍有很多不足之处。

Running 5s test @ http://172.31.XX.XX:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   317.99us   74.65us    1.39ms   78.00us   76.29%
    Req/Sec    49.51k     2.01k    54.74k    44.35k    68.88%
  Latency Distribution
  50.00%  312.00us
  90.00%  405.00us
  99.00%  531.00us
  99.99%  749.00us
  3938893 requests in 5.00s, 582.25MB read
Requests/sec: 787768.20

这次性能提高了 6%,但仍远低于预期。

Linux内核与DPDK之HTTP性能对决

火焰图分析

火焰图也没有显示出太多的变化,并且仍然没有发生繁忙的轮询,所以还有其他问题。我挖掘了我的性能分析工具包,看看我是否能弄清楚发生了什么。

迫不及待地等待

我能够使用libreactor作为一个完全优化的基于 epoll 的 HTTP 服务器应该如何运行的参考点,并将其与 tcp_httpd 进行对比。在为 libreactor 和 tcp_httpd 运行基准测试时运行 10 秒的syscount跟踪 ( ) 产生了一些启发性的结果:syscount -d 10

自由反应器

SYSCALL                   COUNT
recvfrom                9755167
sendto                  9353652
epoll_wait               754685
read                         94
bpf                          43
newfstatat                   18
ppoll                        11
pselect6                      7
futex                         5
write                         5

tcp_httpd

SYSCALL                   COUNT
epoll_pwait             7525419
read                    7272935
sendto                  6926720
epoll_ctl                824992
poll                      76612
timerfd_settime           34276
rt_sigprocmask            11356
ioctl                      6447
membarrier                 5676
newfstatat                   18

对于 libreactor,前两个系统调用send/recvepoll_wait在第三位。与 tcp_httpd 相反,epoll_pwait是排名第一的系统调用。这是一个很好的指标,我需要看看在epoll_pwaitSeastar 代码库中是如何调用的。

系统epoll_pwait调用等待与文件描述符关联的事件。在我们的例子中,我们正在专门处理套接字文件描述符(表示 TCP 连接),每个事件都表明准备好发送或接收数据。

原始的系统调用可以被认为是参数epoll_(p)wait的 3 种类型的值timeout

libreactor 使用完全围绕 构建的相对简单的反应器引擎epoll,因此它可以无限期地等待下一个事件。另一方面,Seastar 则更复杂一些。Seastar 支持多种不同的高分辨率计时器、轮询函数和跨反应堆消息队列;并且它试图强制执行有关任务预计运行多长时间的某些保证。在主do_run循环中,Seastar 调用epoll_pwait超时为 0(它根本不等待),这就是为什么我们没有看到任何繁忙的轮询发生的原因。使用无限超时调用epoll_pwait对 Seastar 来说是行不通的,甚至使用epoll_pwait1ms 的最小值可能有点太长了。事实证明,Seastar 的默认任务应在单个周期中运行多长时间 ( task-quota-ms) 为 0.5 (500us)。

为了在框架的延迟预期和我的性能目标之间取得平衡,我决定使用相对较新的epoll_pwait2系统调用epoll_pwait2等价于epoll_pwait,但可以使用纳秒分辨率指定超时参数。我选择了 100us 的超时值作为性能和延迟保证之间的良好平衡。新的系统调用从内核 5.11 开始可用,但相应的 glibc 包装器在 glibc 2.35 之前不可用,并且 Amazon Linux 2022 附带 glibc 2.34。为了解决这个问题,我破解了一个名为 的包装函数epoll_pwait_us,并更新了我的 Seastar fork以将其调用为 100。

Running 5s test @ http://172.31.XX.XX:8080/
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   273.38us   39.11us    1.37ms   79.00us   71.32%
    Req/Sec    57.48k   742.64     59.34k    55.62k    67.98%
  Latency Distribution
  50.00%  271.00us
  90.00%  322.00us
  99.00%  378.00us
  99.99%  613.00us
  4575332 requests in 5.00s, 676.32MB read
Requests/sec: 915053.04

性能从 788k req/s 上升到 915k req/s,增长了 16%。

火焰图分析

查看火焰图,您可以清楚地看到繁忙的轮询终于开始了,并且查看我们的系统调用计数,我们看到了预期的模式出现。

tcp_httpd

SYSCALL                   COUNT
read                    8422317
sendto                  7964784
epoll_ctl                450827
epoll_pwait2             375947
poll                      79836
ioctl                       202
bpf                          49
newfstatat                   18
ppoll                        11

恒定上下文切换

我继续使用更多性能工具将 tcp_httpd 与 libreactor 进行比较,以查看是否可以发现更多异常。果然,使用sar -w 1监视上下文切换为 tcp_httpd 产生了一些令人瞠目结舌的数字。

自由反应器

01:13:50 AM    proc/s   cswch/s
01:13:57 AM      0.00    277.00
01:13:58 AM      0.00    229.00
01:13:59 AM      0.00    290.00
01:14:00 AM      0.00    340.00

tcp_httpd

01:03:03 AM    proc/s   cswch/s
01:03:04 AM      0.00  17132.00
01:03:05 AM      0.00  17060.00
01:03:06 AM      0.00  17048.00
01:03:07 AM      0.00  17026.00

在没有放大的情况下查看火焰图,我注意到对于每个反应器线程,Seastar 都会创建一个匹配的计时器线程,命名为 timer-0、timer-1 等。起初我没有太注意它们,因为我没有明确说明设置任何计时器,它们在火焰图上几乎不可见,但鉴于上下文切换数字,我决定仔细看看。

对于每个反应器/cpu 核心,使用该函数start_tick() 启动一个线程。task_quota_timer_thread_fn()该函数等待反应堆_task_quota_timer到期,然后通过调用中断主线程request_preemption()。这样做是为了确保主线程上的任务不会在没有抢占的情况下运行超过 X 毫秒来占用资源。但是对于我们特定的工作负载,它会导致过多的上下文切换和性能下降。我们要做的是将它设置得足够长,以便reactor::run_some_tasks()可以完成所有任务并重置抢占而不会被中断。需要注意的是,Seastar 的默认 aio 后端似乎利用了一些 aio 特定的抢占功能来处理任务配额,因此这种特殊行为仅限于 epoll 后端。

--task-quota-msSeastar 允许用户通过命令行传入一个值来设置。默认值为 0.5,但我发现 10 毫秒对于这个工作负载来说是一个更合理的值。

tcp_httpd 与 –task-quota-ms 10

01:04:58 AM    proc/s   cswch/s
01:04:59 AM      0.00   1327.00
01:05:00 AM      0.00   1303.00
01:05:01 AM      0.00   1339.00
01:05:02 AM      0.00   1296.00

每秒上下文切换次数从 17k 急剧下降到 1.3k

Running 5s test @ http://172.31.XX.XX:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   259.14us   29.51us    1.51ms   77.00us   71.92%
    Req/Sec    60.55k   532.97     61.77k    58.82k    66.71%
  Latency Distribution
  50.00%  257.00us
  90.00%  296.00us
  99.00%  337.00us
  99.99%  557.00us
  4820680 requests in 5.00s, 712.59MB read
Requests/sec: 964121.54

吞吐量从 915k req/s 提高到 964k req/s,提高了 5.3%。

Linux内核与DPDK之HTTP性能对决

火焰图分析

火焰图的变化非常微妙。如果您缩小到“全部”视图然后搜索,timer-您将看到最右边的一小部分前一个火焰图的 0.7% 变为当前火焰图的0.1%。火焰图非常有用,但它们并不总是以成比例的方式捕捉性能影响。有时您必须在性能工具箱中四处翻找才能找到合适的工具来发现异常。

最好是RECV

这是我在优化 libreactor 时发现的一个简单修复。在使用套接字时,使用 Linux 的recv/send函数比使用更通用的read/更有效write。通常差异可以忽略不计,但是当您超过 50k req/s 时,它开始累加。Seastar 已经send用于传出数据,但它read用于传入请求,因此我进行了相对简单的更改以将其切换到recv

Running 5s test @ http://172.31.XX.XX:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   253.53us   30.51us    1.21ms   93.00us   74.63%
    Req/Sec    61.72k   597.21     62.99k    58.46k    71.81%
  Latency Distribution
  50.00%  250.00us
  90.00%  291.00us
  99.00%  342.00us
  99.99%  652.00us
  4911503 requests in 5.00s, 726.02MB read
Requests/sec: 982287.44

吞吐量从 964k req/s 提高到 982k req/s,性能提升略低于 2%。

Linux内核与DPDK之HTTP性能对决

火焰图分析

如果您查看火焰图左侧的 read/recv 堆栈,您会发现它__libc_recv__libc_read.

记得刷新

我通过在代码库中漫游并打开/关闭事物以查看它们做了什么来找到最终的优化。使用 epoll reactor 后端时,延迟调用batch_flushes选项在被调用时立即调用。它旨在优化可能多次调用的 RPC 工作负载,但它对我们简单的请求/响应工作负载没有任何好处。事实上,它增加了一点开销,所以作为快速修复我只是禁用了

output_streamsend()
flush()
flush()
batch_flushes
Running 5s test @ http://172.31.XX.XX:8080/json
  16 threads and 256 connections
  Thread Stats   Avg     Stdev       Max       Min   +/- Stdev
    Latency   246.66us   34.32us    1.25ms   61.00us   74.07%
    Req/Sec    63.30k     0.88k    65.72k    61.63k    66.84%
  Latency Distribution
  50.00%  246.00us
  90.00%  288.00us
  99.00%  333.00us
  99.99%  436.00us
  5038933 requests in 5.00s, 744.85MB read
Requests/sec: 1007771.89

吞吐量从 982k req/s 提高到 1.0M req/s,性能提升了 2.2%。

火焰图 - 完全优化

火焰图分析

火焰图显示发送堆栈从移动batch_flush_pollfn::polloutput_stream<char>::flush

我们的优化工作为我们的最终数字带来了心理上令人满意的以 10 为底的数字:1.0M 请求/秒,仅使用良好的旧 Linux 内核。

最终获胜者是…

最后,DPDK 仍然保持着超过内核 51% 的稳定性能领先。是多还是少取决于你的观点。在我看来,当您比较内核/应用程序的未优化和优化版本时,我们将 DPDK 的性能优势从 4.2 倍缩小到仅 1.5 倍。

图 - 内核与 DPDK

DPDK 警告

DPDK 的 51% 优势不容小觑,但是如果我在没有添加一些关于 DPDK 挑战的免责声明的情况下将您送入 DPDK 兔子洞,那将是我的失职。

  1. 首先,它是一种小众技术,因此在线查找文章和示例(尤其是对于已建立领域之外的用例)可能具有挑战性。
  2. 绕过内核意味着你也绕过了它经过时间考验的 TCP 堆栈。如果您的应用程序使用基于 TCP 的协议(如 HTTP),则需要在用户空间中提供自己的 TCP 网络堆栈。有像 Seastar 和F-Stack这样的框架可以提供帮助,但是将您的应用程序迁移到它们可能并非易事。
  3. 使用自定义框架也可能意味着您被绑定到它支持的特定 DPDK 版本,这可能不是您的网络驱动程序或内核支持的版本。
  4. 在绕过内核时,您还绕过了用于保护、监控和配置网络流量的现有工具和功能的丰富生态系统。您习惯使用的许多工具和技术不再有效。
  5. 如果您使用轮询模式处理,您的 CPU 使用率将始终为 100%。除了不节能/不环保之外,它还使得使用 CPU 使用率作为衡量标准来快速评估/排除工作负载变得困难。
  6. 基于 DPDK 的应用程序完全控制网络接口,这意味着:
    • 您必须有多个接口。
    • 如果要修改设备设置,必须在启动前或通过应用程序进行。
    • 如果要捕获指标,则必须将应用程序配置为执行此操作;即时进行故障排除要困难得多。

话虽如此,除了纯粹的性能之外,可能还有其他理由追求自定义 TCP/IP 堆栈。应用程序内 TCP 堆栈允许应用程序精确控制内存分配(避免应用程序和内核之间的内存争用)和调度(避免 CPU 时间争用)。这对于不仅追求最大吞吐量而且还追求出色的 p99 延迟的应用程序非常重要。

归根结底,它是关于平衡优先事项。例如,即使 ScyllaDB 团队偶尔会收到与内核网络堆栈相关的反应堆停顿的报告,但他们仍然选择坚持使用内核作为其旗舰产品,因为切换到 DPDK绝非易事。

推测性执行缓解措施

在这篇文章的开头,我对在开始优化应用程序之前所做的操作系统级别的优化进行了掩饰。从高层次来看,这些变化与我之前的帖子相似,对于那些想要深入了解更多细节的人,我计划“在某个时候”写一篇关于 4.14 与 5.15 的内核帖子。然而,在这个内核与 DPDK 的对决中,有一个特别的优化值得进一步分析:禁用推测执行缓解。

我不会重复我对这些缓解措施的看法,你可以在这里阅读。出于本文的目的,我关闭了它们,但是如果您查看下图,您会发现重新打开它们会显示一些有趣的结果。

图表 - 内核与 DPDK 与缓解打开/关闭

如您所见,虽然禁用缓解在内核端产生了 33% 的性能提升,但它对DPDK 性能的影响为零。这导致了两个主要结论:

  1. 对于必须进行推测执行缓解的环境,DPDK 代表了比内核 TCP 堆栈更大的性能改进。
  2. 像 io_uring 这样绕过 I/O 系统调用接口的内核技术在提高大多数工作负载的性能方面具有更大的潜力。

大多数人不会禁用 Spectre 缓解措施,因此启用它们的解决方案很重要。我不能 100% 确定所有缓解开销都来自系统调用,但有理由认为其中很大一部分来自用户到内核和内核到用户转换中的安全强化。这种影响在火焰图上的系统调用相关函数中肯定是可见的。

Linux内核与DPDK之HTTP性能对决

结论

我们已经证明,即使操作系统和应用程序被优化到极致,DPDK 仍然比内核网络堆栈有 51% 的性能领先。我没有将这种差异视为无法克服的障碍,而是将差距视为内核方面未实现的潜力。这种差距只是提出了一个问题:Linux 内核可以在多大程度上针对每核线程应用程序进行进一步优化,而不会影响其通用性?

DPDK 让我们了解在理想情况下什么是可能的,并作为努力的目标。即使差距不能完全弥合,它也会量化任务并将障碍放在更清晰的焦点上。

一个非常明显的障碍是每秒执行数百万个系统调用系统调用接口的开销。值得庆幸的是,io_uring 似乎为这一特定挑战提供了潜在的解决方案。我一直在密切关注 io_uring,因为它仍处于相当繁重的开发阶段。我特别兴奋地看到最近一波以网络为重点的优化,如忙轮询支持、recv 提示,甚至是无锁 TCP 支持等实验性探索。在我“很快”测试的事情清单上仍然很高。

附录

特别感谢我的审阅者:Dor 和 Kenia,以及 Seastar Slack 频道和邮件列表中的每个人,特别是 Piotr、Avi 和 Max。

C/C++ 入门

我使用我有限的 C 知识,结合基本的模式识别,在 Seastar 的 C++ 代码库中摸索的时间比我应该拥有的时间要长得多,但是到了添加get_cpu_to_shard_mapping()函数的时候,我决定停止自欺欺人并学习一点 C++。如果您发现自己处于类似的困境中,我推荐A Tour of C++作为不错的入门读物。如果您还需要快速复习 C 语言,我推荐Essential C和Pointers and Memory。