前言

最近忙于网络三层,四层,七层的测试工作团团转。在解决项目的问题时偶然浏览到了一片国外大牛写的博客。看了之后收到了很多启发决定翻译一下。这篇文章主要讲述了如何使用linux内核单网卡收发UDP达到百万级别pps。该博主的一些实验和实验数据给予了很多启发,借此机会想让更多的人了解。

源博文出处:https://blog.cloudflare.com/how-to-receive-a-million-packets/

上周,在一次闲聊中我无意中听到以为同事说:“linux网络协议栈太慢了!你不要期望linux能够在单核跑到5万pss。”这让我思考到,诚然我同意5万PPS是实际应用中可能打到的极限值,那linux网络协议栈的性能能到多少。让我们换个测试目标来找点乐子。

在linux系统上,写一个每秒接收一百万UDP包的程序有多难?希望回答这个问题将会是关于现代网络栈设计的一个很好的思路。

首先让我们做这样的一个估计:

1.测试pps(packets per second)值比测试Bps(bytes per second)值将更加有价值。你可以通过使用更好的流水线技术(pipelining)和发送更长字节的包来获得更高的Bps值。但是提高pps值显得更加困难。
2.由于我们现在针对pps,我们实验将会使用短UDP包来测试。这意味着32字节的UDP负载,74字节的二层以太网包长。
3.实验中我们将会使用两台服务器,一台作为“receiver”发包端,一台作为“sender”收包端。
4.两台服务器均有两颗6核2GHZ的Xeon处理器。在开启了超线程之后每台服务器上有24颗core。每台服务器上有一个由solarflare提供的10G多队列网卡,服务器上配置了11个多队列。稍后会对此多更多的介绍。
5.测试的源码可以从git上下载。
udpsender:https://github.com/majek/dump/blob/master/how-to-receive-a-million-packets/udpsender.c
udpreceiver:https://github.com/majek/dump/blob/master/how-to-receive-a-million-packets/udpreceiver1.c

先前准备:
我们使用端口4321来发送UDP包。在运行测试pps程序之前我们首先确保端到端的网络链路不会被iptables防火墙阻拦。

receiver$ iptables -I INPUT 1 -p udp --dport 4321 -j ACCEPT
receiver$ iptables -t raw -I PREROUTING 1 -p udp --dport 4321 -j NOTRACK

我们来定义几个测试IP来为后续测试提供便利:
定义发送端的测试IP:
receiver$ for i in seq 1 20; do
ip addr add 192.168.254.KaTeX parse error: Expected 'EOF', got '\ ' at position 16: i/24 dev eth2; \̲ ̲ done … ip addr add 192.168.254.30/24 dev eth3

  1. 最简单的实验

首先让我们做一个最简单的实验。定义一个简单的发送和接收,将发送多少个包。

fd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
fd.bind(("0.0.0.0", 4321))
while True:packets = [None] * 1024fd.recvmmsg(packets, MSG_WAITFORONE)

recvmmsg是通用recv系统调用中比较有效的版本。我们查看一下收包输出结果:

sender$ ./udpsender 192.168.254.1:4321
receiver$ ./udpreceiver1 0.0.0.0:43210.352M pps  10.730MiB /  90.010Mb0.284M pps   8.655MiB /  72.603Mb0.262M pps   7.991MiB /  67.033Mb0.199M pps   6.081MiB /  51.013Mb0.195M pps   5.956MiB /  49.966Mb0.199M pps   6.060MiB /  50.836Mb0.200M pps   6.097MiB /  51.147Mb0.197M pps   6.021MiB /  50.509Mb

用这种简单的方法我们可以得到197K到350K的pps输出值,不幸的是这种方式测得每次输出结果误差都比较大。这是由于程序运行在内核中,内核发生上下文切换导致的。将进程和CPU做和绑定将会有效的改善这种情况。

sender$ taskset -c 1 ./udpsender 192.168.254.1:4321
receiver$ taskset -c 1 ./udpreceiver1 0.0.0.0:43210.362M pps  11.058MiB /  92.760Mb0.374M pps  11.411MiB /  95.723Mb0.369M pps  11.252MiB /  94.389Mb0.370M pps  11.289MiB /  94.696Mb0.365M pps  11.152MiB /  93.552Mb0.360M pps  10.971MiB /  92.033Mb

现在系统内核调度会将程序放在默认设定和绑定的内核中执行。 这改进了处理器缓存局部性,使测试结果更加一致,这正是我们想要的。

  1. 发送更多的包

370k的pps对于一个程序来说并不坏,但是它里我们目标的一百万pps仍然有差距。为了能够收到更多的包,首先我们必须发送更多的包。为何不考虑用两个独立的线程来发送包。

sender$ taskset -c 1,2 ./udpsender \192.168.254.1:4321 192.168.254.1:4321
receiver$ taskset -c 1 ./udpreceiver1 0.0.0.0:43210.349M pps  10.651MiB /  89.343Mb0.354M pps  10.815MiB /  90.724Mb0.354M pps  10.806MiB /  90.646Mb0.354M pps  10.811MiB /  90.690Mb

从接收方可以看到收包数量并没有增加。使用ethtool -s可以看到包实际去向去哪了。

receiver$ watch 'sudo ethtool -S eth2 |grep rx'rx_nodesc_drop_cnt:    451.3k/srx-0.rx_packets:     8.0/srx-1.rx_packets:     0.0/srx-2.rx_packets:     0.0/srx-3.rx_packets:     0.5/srx-4.rx_packets:  355.2k/srx-5.rx_packets:     0.0/srx-6.rx_packets:     0.0/srx-7.rx_packets:     0.5/srx-8.rx_packets:     0.0/srx-9.rx_packets:     0.0/srx-10.rx_packets:    0.0/s

通过这个状态可以看到,网卡已经成功的将350Kpps包交付给了收包队列4号。rx_nodesc_drop_cnt是Solarflare特有的计数器,计数器显示的数字告知网卡有发送给内核的450Kpps包被丢弃。有些时候并不清楚为什么包会被丢弃。但在我们的例子当中,原因非常的明显:RX四号队列将包交付给了第四号CPU。但是四号CPU无法提供更多的运算能力来处理。对于这颗核来说处理350Kpps已经是极限值。这里通过“htop”命令来查看CPU状态可以得知:

  • 使用网卡多队列特性

过去,网卡只有一个RX队列,用于在硬件和内核之间传递数据包。这个设计有着很明显的限制,它不能传输超过单核可以处理包的上限,更多的包发送给该核只能被丢弃。利用多内核系统,网卡开始支持网卡多队列特性。这个设计可以简单地表述如下:每个接收队列都会绑定到与之对应的一颗核上。因此,一个网卡的所有传输队列可以与之对应到特定CPU上将网卡的性能最大化。但是这也导致了一个问题,网卡如何决定一个包交给哪个队列来处理。

round-robin(轮询策略)这种策略是不行的,因为这样会导致在单链接的情况下数据包重新排序。 另一种方案是根据对包进行hash来决定RX队列号。通常对以(src IP, dst IP, src port, dst port)的一组序列来进行哈希。这保证了单个流的包将始终位于完全相同的RX队列上,在单链接的情况下对包重新排序的情况将不会发生。

在我们实验用例中,要进行哈希的队列可能如下所示:

RX_queue_number = hash('192.168.254.30', '192.168.254.1', 65400, 4321) % number_of_queues
  • 多队列哈希算法

该哈希算法可以通过ethtool来进行配置,在我们示例中配置可以如下:

receiver$ ethtool -n eth2 rx-flow-hash udp4
UDP over IPV4 flows use these fields for computing Hash flow key:
IP SA
IP DA

针对IPV4的UDP协议,网卡会对数组(srcIP,dstIP)进行哈希。

receiver$ ethtool -N eth2 rx-flow-hash udp4 sdfn
Cannot change RX network flow hashing options: Operation not supported

不幸的是我们的网卡不支持sdfn算法,我们还是考虑使用(src IP,dst IP)进行哈希运算。

  • 关于NUMA特性的说明

到目前为止,我们所有的包只流向一个RX队列,并且只核绑到一个CPU。让我们借此机会对不同cpu的性能进行基准测试。在我们做测试的服务器中,接收主机有两个独立的处理器,每一个都是不同的NUMA节点。

在我们选择对线程做pinning的时候,我们可以参考以下四个选项来选择所要pinning的核。
1.在另一个核上运行receiver,但是该核在与RX队列所pinning的核在相同的NUMA节点上。我们在上面实验可以看到性能大约是360kpps。
2.接收端与RX队列pinning到完全相同的核上,我们可以达到~430kpps。但它造成了高度的可变性。如果网卡被包压得喘不过气来,性能就会下降到零。
3.当接收端进程运行在处理RX队列的核上并且也是对应的HT上时,性能是通常在200kpps。
4.当运行程序和接收队列运行在不同的NUMA节点上不同的核时,测试收包大概能到330kpps。不过所得到的结果不稳定并没有太大实用价值。

  1. 接收端实用多个IP

使用NIC上的散列算法优化得到的结果非常有限,所以跨RX队列分发数据包的惟一方法是使用多IP地址。这里演示是如何发送数据包到不同的目的地ip:

sender$ taskset -c 1,2 ./udpsender 192.168.254.1:4321 192.168.254.2:4321

ethtool确认数据包进入不同的RX队列:

receiver$ watch 'sudo ethtool -S eth2 |grep rx'rx-0.rx_packets:     8.0/srx-1.rx_packets:     0.0/srx-2.rx_packets:     0.0/srx-3.rx_packets:  355.2k/srx-4.rx_packets:     0.5/srx-5.rx_packets:  297.0k/srx-6.rx_packets:     0.0/srx-7.rx_packets:     0.5/srx-8.rx_packets:     0.0/srx-9.rx_packets:     0.0/srx-10.rx_packets:    0.0/s

The receiving part:
receiver$ taskset -c 1 ./udpreceiver1 0.0.0.0:4321
0.609M pps 18.599MiB / 156.019Mb
0.657M pps 20.039MiB / 168.102Mb
0.649M pps 19.803MiB / 166.120Mb
两个核心忙于处理RX队列,第三个核心运行应用程序,可以获得~650k pps。 我们可以通过将流量发送到3或4个RX队列来进一步增加这个数字,但是很快测试程序遇到另一个限制。这一次rx_nodesc_drop_cnt没有增长,但是使用netstat命令查看“接收错误”的状态可以考到该数在增加。

receiver$ watch 'netstat -s --udp'
Udp:437.0k/s packets received0.0/s packets to unknown port received.386.9k/s packet receive errors0.0/s packets sentRcvbufErrors:  123.8k/sSndbufErrors: 0InCsumErrors: 0

这意味着即便网卡有能力接收包将包转发给内核,内核也无力将包回转给应用程序。在我们的测试用例中单核仅能够转发440kpps,剩余的390kpps + 123kpps由于测试程序接收它们的速度不够快而被丢弃。

  1. 接收端开启多队列

我们需要扩展接收应用程序。从开启多线程来增加接收数据的天真方法并不会很好地工作。

sender$ taskset -c 1,2 ./udpsender 192.168.254.1:4321 192.168.254.2:4321
receiver$ taskset -c 1,2 ./udpreceiver1 0.0.0.0:4321 20.495M pps  15.108MiB / 126.733Mb0.480M pps  14.636MiB / 122.775Mb0.461M pps  14.071MiB / 118.038Mb0.486M pps  14.820MiB / 124.322Mb

与单线程程序相比,接收性能下降。这是由UDP接收缓冲区端上的锁争用引起的。由于两个线程都使用相同的套接字描述符,因此它们在争夺UDP接收缓冲区周围的锁上花费了不成比例的时间。详细有关缓冲区的问题描述可以参考此文章:
http://www.jcc2014.ucm.cl/jornadas/WORKSHOP/WSDP 2014/WSDP-4.pdf

使用多个线程从单个描述符接收数据并不是最优的。

  1. SO_REUSEPORT

幸运的是,Linux最近添加了一个解决方案:the SO_REUSEPORT flag(详情可参考:https://lwn.net/Articles/542629/) 当在套接字描述符上设置此标志时,Linux将允许许多进程绑定到同一个端口。事实上,任何数量的进程都可以绑定,并且各个进程将分摊负载之间。

使用SO_REUSEPORT,每个进程都有一个单独的套接字描述符。因此,每个都将拥有一个专用的UDP接收缓冲区。这避免了以前遇到的进程争用问题:

receiver$ taskset -c 1,2,3,4 ./udpreceiver1 0.0.0.0:4321 4 11.114M pps  34.007MiB / 285.271Mb1.147M pps  34.990MiB / 293.518Mb1.126M pps  34.374MiB / 288.354Mb

这才像话!吞吐量现在还不错!在进行了进一步调查之后显示出更多的改进空间。即使我们启动了四个接收线程,负载也没有均匀地分布在它们之间:

两个线程接收了所有的工作,另外两个线程根本没有收到包。这是由散列碰撞引起的,但是这次是在SO_REUSEPORT层。

结束语
我还做了一些进一步的测试,在一个NUMA节点上使用完全对齐的RX队列和接收线程可以获得1.4 mpp。在不同的NUMA节点上运行接收器导致数字下降,达到最多1mpp。

总之,如果你想要一个完美的表现,你需要作如下改进:

  1. 确保流量均匀分布在RX队列和SO_REUSEPORT进程中。在实践中,只要有大量的连接(或流),负载通常是均匀分布的。

后言:
该博文的的小程序可以很好地测试三层网络,通过增加“udpsender”的数量可以达到限速,不失为除开pktgen工具之外另外一种很好地测试方法。git上的代码经过编译之后可以使用,以下链接为编译好的可以直接在centos系统直接使用。
https://download.csdn.net/download/minxihou/10838692

三层网络PPS极限测试相关推荐

  1. 三层网络渗透测试实验

    前言 在渗透测试过程中我们会遇到很多不同的网络情况,或许普通的内网渗透我们已经很熟练了,但在现实环境中或许会有多层网络结构,如果遇到多层网络结构我们应该如何进行渗透呢? 利用这个实验我们就可以更清晰的 ...

  2. Kubernetes系列教程(三)---纯三层网络方案

    来自:指月 https://www.lixueduan.com 原文:https://www.lixueduan.com/post/kubernetes/03-pure-layer-3-network ...

  3. 精读-软件测试的艺术之调试,极限测试和因特尔应用系统的测试

    本文是关于精读书籍<软件测试的艺术>的一些学习笔记和分享 本书共有九章包括测试思想(心理,经济),代码检查,测试用例设计,模块测试,更高级别的测试,调试,极限测试和因特尔应用系统的测试. ...

  4. 基于360虚拟防火墙实现neutron三层网络功能

    本文转载 总结 360虚拟防火墙基于虚拟机形态集成了三层网络和安全防护功能,该虚拟机作为租户网络的三层路由器,通过手动创建内网网关port和外网port,并将这些port绑定到虚拟机中,从而连通租户v ...

  5. Neutron 理解 (6): 如何实现虚拟三层网络

    Neutron 理解 (1): Neutron 所实现的虚拟化网络 Neutron 理解 (2): 使用 Open vSwitch + VLAN 组网 Neutron 理解 (3): Open vSw ...

  6. 软件测试基础-更高级别的测试、调试、极限测试

    更高级别的测试 开发过程和测试过程存在一一对应关系 模块测试的目的是发现程序模块与其接口规格说明之间的不一致 功能测试的目的是为了证明程序未能符合其外部规格说明 系统测试的目的是为了证明软件产品与其初 ...

  7. linux下的CPU、内存、IO、网络的压力测试

    linux下的CPU.内存.IO.网络的压力测试 要远程测试其实很简单了,把结果放到一个微服务里直接在web里查看就可以了,或者同步到其他服务器上 一.对CPU进行简单测试: 1.通过bc命令计算特别 ...

  8. 三层网络靶场搭建MSF内网渗透

    三层网络靶场搭建&MSF内网渗透 在最近的CTF比赛中,综合靶场出现的次数越来越多,这种形式的靶场和真实的内网渗透很像,很贴合实际工作,但我们往往缺少多层网络的练习环境.本文通过VMware搭 ...

  9. 【网络】叶脊(Spine-Leaf)网络拓扑下全三层网络设计与实践(三) - 交换设备互联

    3. 交换机互联 3.1 本节目标 了解Spine Leaf网络基本拓扑结构: 网络设备互联接口配置: 网络设备bgp配置: 3.2 拓扑结构: 如下图所示, 本节使用的Spine Leaf网络拓扑, ...

最新文章

  1. 如何制作EDM邮件的内容
  2. 招不招新人?IT经理很纠结.
  3. nodejs 面向对象 私有变量_Java准备校招之面向对象总结
  4. 使用Spring发送带附件的电子邮件(站内和站外传送)
  5. Android 音频均衡器,可通过拖动调节音频EQ
  6. excel制作窗体查询界面_利用 VBA窗体制作excel登录界面
  7. Java POI 读取Excel-从开始到实例
  8. 警告 1 warning C4996: ‘scanf‘: This function or variable may be unsafe.
  9. 安卓 linux arm go,go arm、android版本
  10. 2021年6月23日,我们毕业啦!!!
  11. 琢磨事琢磨人琢磨钱,成大事也!
  12. 青龙2.10.13 稳定版+xdd-plus+阿东教程保姆教程(2022年7月11日更新)
  13. 图像检索基于BOF(Bag-Of-Features Models)
  14. 手机格局再变,诺基亚和传音入前十,魅族和中兴出局
  15. 在设备后台安装CAB而不让用户发觉
  16. 易点易动【设备管理】产品全新上线
  17. Hadoop HA在停掉active namenode后无法自动切换到standby namenode
  18. python导入库的方式有几种_python库导入的三种方式
  19. 首席谈判官的定义和职责
  20. c语言行列式运算程序,行列式计算程序,没事看看吧,已经验证

热门文章

  1. 女朋友看电影没字幕,让我想办法搞定!谁让我是程序员呢!
  2. 苹果蓝牙耳机怎么接电话_苹果蓝牙耳机哪种好?音质最佳的五大爆款耳机
  3. word删除不了最后一页怎么办?【已解决】
  4. 关于安装淘宝镜像不能使用问题解决
  5. 会声会影X4模板:在浪漫星光下 震撼婚礼庆典 模板下载
  6. self-attention代码
  7. 论张飞杀猪,关羽卖枣,他们的绝世武功是如何练成的?
  8. mysql打错字撤回_发消息打错字要“撤回”,不小心却按了“删除”,你中枪了吗...
  9. 一天一模式之24 相关模式比较
  10. Android音频驱动学习(一) Audio HAL