云服务器价格_云数据库_云主机【优惠】最新活动-搜集站云资讯

云存储_阿里云排名第几_精选特惠

小七 141 0

如何每秒接收一百万个数据包

上周在一次偶然的谈话中,我无意中听到一位同事说:"Linux网络堆栈太慢了!你不能指望它每核心每秒能处理超过5万个数据包!"这让我开始思考。虽然我同意对于任何实际的应用程序来说,每个内核50kps可能是一个限制,但是Linux网络栈能做什么呢?让我们重新措辞,让它更有趣:在Linux上,编写一个每秒接收100万个UDP数据包的程序有多难?希望对这个问题的回答将是现代网络堆栈设计的一个很好的教训。CC BY-SA 2.0图片作者:Bob McCaffrey首先,让我们假设:测量每秒包数(pps)比测量每秒字节数(Bps)有趣得多。您可以通过更好的流水线和发送更长的包来实现高Bps。改善pps要困难得多。由于我们对pps感兴趣,我们的实验将使用短UDP信息。准确地说:32字节的UDP有效负载。也就是说74岁以太网层上的字节。在实验中,我们将使用两个物理服务器:"receiver"和"发送者"。它们都有两个6核2GHz Xeon处理器。启用超线程(HT)后,每个机箱上有24个处理器。这些盒子有一个Solarflare的多队列10G网卡,配置了11个接收队列。稍后再谈。测试程序的源代码可以在这里找到:udpsender,udpreciver。先决条件让我们使用端口4321作为UDP数据包。在开始之前,我们必须确保流量不会受到iptables的干扰:接收方$iptables-I输入1-p udp--dport 4321-j ACCEPT接收器$iptables-t raw-I预路由1-p udp--dport 4321-j NOTRACK两个明确定义的IP地址稍后将变得方便:在'seq 120'中接收$\ip地址添加192.168.254.$i/24 dev eth2\完成发件人$ip addr add 192.168.254.30/24 dev eth3天真的方法首先让我们做一个最简单的实验。一次简单的发送和接收将传送多少个数据包?发送方伪代码:fd=插座.插座(插座, 承插式插座)fd.绑定(("0.0.0.0",65400))#选择源端口以减少不确定性fd.连接("192.168.254.1",4321)如果是真的:fd.sendmmsg公司(\x00"*32]*1024)虽然我们可以使用通常的send syscall,但它不会有效。上下文切换到内核是有代价的,最好避免它。幸运的是,最近在Linux中添加了一个方便的系统调用:sendmsg。它允许我们一次发送许多包。让我们一次做1024个包。接收机伪码:fd=插座.插座(插座, 承插式插座)fd.绑定("0.0.0.0",4321)如果是真的:数据包=[无]*1024fd.recvmmsg公司(数据包,MSG\u WAITFORONE)类似地,recvmmsg是common recv syscall的一个更高效的版本。让我们试试看:发件人$/udpsender 192.168.254.1:4321收款人$/udpreceiver1 0.0.0.0:43210.352MPPS 10.730MiB/90.010Mb0.284M pps 8.655MiB/72.603Mb262万pps 7.991MiB/67.033Mb0.199M pps 6.081MiB/51.013Mb195M pps 5.956MB/49.966Mb0.199M pps 6.060MiB/50.836Mb200万pps 6.097兆字节/51.147Mb0.197M pps 6.021MB/50.509Mb用这种天真的方法,我们可以做到197k到350k pps。还不错。不幸的是,有相当多的可变性。它是由内核在内核之间对程序进行洗牌造成的。将进程固定到CPU将有助于:发件人$taskset-c 1./udpsender 192.168.254.1:4321接收者$taskset-c 1./udpreceiver1 0.0.0.0:43210.362Mpps 11.058MiB/92.760Mb0.374M pps 11.411MiB/95.723Mb0.369M pps 11.252MiB/94.389Mb0.370M pps 11.289MiB/94.696Mb0.365MPPS 11.152MiB/93.552Mb0.36百万pps 10.971MiB/92.033Mb现在,内核调度器将进程保存在定义的cpu上。这提高了处理器缓存的局部性,并使数字更加一致,这正是我们想要的。发送更多数据包虽然370k pps对于一个幼稚的程序来说并不坏,但它离1Mpps的目标仍然有相当大的距离。要接收更多的信息,首先我们必须发送更多的数据包。如何从两个线程独立发送:发件人$taskset-c 1,2./udpsender\192.168.254.1:4321 192.168.254.1:4321接收者$taskset-c 1./udpreceiver1 0.0.0.0:43210.349M pps 10.651MiB/89.343Mb0.354M pps 10.815MiB/90.724Mb0.354M pps 10.806MB/90.646Mb0.354M pps 10.811MiB/90.690Mb接收端的数字没有增加。ethtool-S将显示数据包的实际去向:接收器$watch"sudo ethtool-S eth2 | grep rx"接收节点下降:451.3k/srx-0.rx_数据包:8.0/srx-1.rx_数据包:0.0/srx-2.rx_数据包:0.0/srx-3.rx_数据包:0.5/srx-4.rx_数据包:355.2k/srx-5.rx_数据包:0.0/srx-6.rx_数据包:0.0/srx-7.rx_数据包:0.5/srx-8.rx_数据包:0.0/srx-9.rx_数据包:0.0/srx-10.rx_数据包:0.0/s通过这些统计数据,NIC报告它已经成功地向RX队列编号4发送了大约350kps的数据。rx_nodesc_drop_cnt是一个Solarflare特定的计数器,表示NIC无法向内核传输450kps。有时不清楚为什么包裹没有送到。不过,在我们的例子中,这是非常清楚的:RX队列4向CPU 4发送数据包。CPU 4不能再做更多的工作了——它只是忙于读取350kps的数据。以下是htop中的外观:多队列NIC的紧急课程过去,网卡有一个用于在硬件和内核之间传递数据包的RX队列。这种设计有一个明显的局限性——它不可能传递超过单个CPU所能处理的数据包。为了利用多核系统,网卡开始支持多个接收队列。设计很简单:每个接收队列都固定在一个单独的CPU上,因此,通过向所有RX队列发送数据包,NIC可以利用所有CPU。但它提出了一个问题:给定一个包,NIC如何决定将其推送到哪个RX队列?循环平衡是不可接受的,因为它可能会在单个连接中重新排列数据包。另一种方法是使用来自数据包的散列来决定接收队列号。散列通常从元组(src IP、dst IP、src port、dst port)计数。这保证了单个流的数据包将始终以完全相同的RX队列结束,并且不会在单个流中重新排序数据包。在我们的例子中,哈希可以这样使用:RX_queue_number=hash('192.168.254.30','192.168.254.1',65400,4321)%_队列数量多队列哈希算法哈希算法可以使用ethtool进行配置。我们的设置是:接收方$ethtool-n eth2 rx flow hash udp4IPV4上的UDP流使用以下字段计算哈希流密钥:IP SA公司IP地址这读作:对于IPv4 UDP数据包,NIC将哈希(src IP,dst IP)地址。即。:RX_queue_number=hash('192.168.254.30','192.168.254.1')%number_of_queues这是相当有限的,因为它忽略了端口号。许多NIC允许自定义哈希。同样,使用ethtool,我们可以选择元组(src IP,dst IP,src port,dst port)进行哈希运算:接收方$ethtool-N eth2 rx flow hash udp4 sdfn无法更改RX网络流哈希选项:不支持操作不幸的是,我们的网卡不支持它-我们被限制为(src IP,dst IP)哈希。关于NUMA性能的一点注记到目前为止,我们所有的数据包只流向一个RX队列,只命中一个CPU。让我们利用这个机会来测试不同CPU的性能。在我们的设置中,接收主机有两个独立的处理器组,每个都是不同的NUMA节点。我们可以将单线程接收器固定到我们设置的四个有趣的cpu中的一个。这四个选项是:在另一个CPU上运行receiver,但在与RX队列相同的NUMA节点上。我们在上面看到的性能大约是360kps。如果接收器与接收队列在同一个CPU上,我们可以得到大约430kps。但它产生了很高的可变性。如果网卡被数据包压得喘不过气来,性能就会下降到零。当接收器运行在处理RX队列的CPU对应的HT上时,性能是通常的200 kps左右的一半。如果接收器位于不同于RX队列的NUMA节点上的CPU上,我们可以得到大约330k pps。不过,数字并不太一致。虽然在不同的NUMA节点上运行10%的惩罚听起来并不算太糟糕,但问题只会随着规模的扩大而变得更糟。在一些测试中,我只能挤出每个核心250kps。在所有的交叉NUMA测试中,可变性都很差。在较高的吞吐量下,NUMA节点的性能损失更为明显。在一个测试中,当我在一个错误的NUMA节点上运行接收器时,我得到了4倍的惩罚。多个接收IP由于我们的网卡上的哈希算法非常有限,在RX队列中分发数据包的唯一方法是使用多个IP地址。以下是如何将数据包发送到不同的目标IP:发件人$taskset-c 1,2./udpsender 192.168.254.1:4321 192.168.254.2:4321ethtool确认数据包进入不同的接收队列:接收器$watch"sudo ethtool-S eth2 | grep rx"rx-0.rx_数据包:8.0/srx-1.rx_数据包:0.0/srx-2.rx_数据包:0.0/srx-3.rx_数据包:355.2k/srx-4.rx_数据包:0.5/srx-5.rx_数据包:297.0k/srx-6.rx_数据包:0.0/srx-7.rx_数据包:0.5/srx-8.rx_数据包:0.0/srx-9.rx_数据包:0.0/srx-10.rx_数据包:0.0/s接收部分:接收者$taskset-c 1./udpreceiver1 0.0.0.0:43210.609M pps 18.599MiB/156.019Mb0.657M每秒20.039MiB/168.102Mb120969万兆字节/兆字节/兆字节万岁!由于两个内核忙于处理RX队列,而第三个内核则在运行应用程序,因此可以获得大约650k个pps!我们可以通过将流量发送到三到四个接收队列来进一步增加这个数量,但是很快应用程序将达到另一个限制。这一次rx_nodesc_drop_cnt没有增长,但是netstat"receiver errors"是:接收器$watch"netstat-s--udp"Udp协议:收到437.0k/s数据包接收到0.0/s到未知端口的数据包。386.9k/s数据包接收错误发送0.0/s包RcvbufErrors:123.8k/sSndbufErrors(错误):