測量每秒數據包數(pps)比測量每秒字節數(Bps)更有趣。妳可以通過更好的管道傳輸和發送更長的數據包來獲得更高的Bps。相比之下,pps的提升就困難多了。
因為我們對pps感興趣,所以我們的實驗將使用較短的UDP消息。準確的說是32字節的UDP負載,相當於以太網層的74字節。
在實驗中,我們將使用兩個物理服務器:“接收者”和“發送者”。
它們都有兩個六核2 GHz至強處理器。每臺服務器都為24個處理器啟用了超線程(HT),采用Solarflare的10G多隊列網卡和11接收隊列配置。後面會詳細介紹。
測試程序的源代碼是:udpsender和udpreceiver。
基本原理
我們使用4321作為UDP數據包的端口。開始之前,我們必須確保傳輸不會受到iptables的幹擾:
殼
接收器$ iptables -I輸入1 -p udp - dport 4321 -j接受
接收器$ iptables -t raw -I預路由1-p UDP-dport 4321-j no track
為了方便後面的測試,我們明確定義了IP地址:
殼
“seq 1 20”中I的接收方$;做
ip地址add 192.168.254。$ I/24 dev eth 2;
完成的
發件人$ ip地址add 192.168.254 . 30/24 dev eth 3
1.簡單方法
起初,我們做了壹些簡單的實驗。單純的收發會傳輸多少個包?
模擬發送者的偽代碼:
計算機編程語言
fd = socket.socket(socket。AF_INET,socket。SOCK_DGRAM)
fd.bind(("0.0.0.0 ",65400)) #選擇源端口以減少不確定性
FD . connect(((“192.168 . 254 . 1”,4321))
雖然正確:
FD . send mmsg([" x00 " * 32]* 1024)
因為我們用的是通用系統調用的send,效率不會很高。上下文切換到內核開銷很大,所以最好避免。幸運的是,Linux最近增加了壹個方便的系統調用sendmmsg。它允許我們在壹次通話中發送多個數據包。那我們就壹次發1024個包。
模擬接收者的偽代碼:
計算機編程語言
fd = socket.socket(socket。AF_INET,socket。SOCK_DGRAM)
FD . bind(((" 0 . 0 . 0 . 0 ",4321))
雖然正確:
數據包=[無] * 1024
fd.recvmmsg(數據包,MSG_WAITFORONE)
類似地,recvmmsg是比普通recv更有效的系統調用版本。
讓我們來試試:
殼
發送者美元。/UDP sender 192.168.254 . 1:4321
接收器美元。/UDP receiver 1 0 . 0 . 0 . 0:4321
0.352兆PPS 10.730兆字節/90.010兆字節
0.284兆pps 8.655MiB兆字節/72.603兆字節
0.262兆PPS 7.991兆字節/67.033兆字節
0.199兆PPS 6.081兆字節/51.01.03兆字節
0.195兆字節/49.966兆字節
0.199兆pps 6.060MiB兆字節/50.836兆字節
0.200兆pps 6.097MiB兆字節/51.147兆字節
0.197兆字節/50.509兆字節
測試表明,197K–350K PPS可以用最簡單的方式實現。看起來不錯,可惜很不穩定。這是因為內核在內核之間交換我們的程序,所以如果我們將進程附加到CPU上,將會有所幫助。
殼
發件人$ taskset -c 1。/UDP sender 192.168.254 . 1:4321
接收器$ taskset -c 1。/UDP receiver 1 0 . 0 . 0 . 0:4321
0.362兆PPS 11.058兆字節/92.760兆字節
0.374m PPS 11.411 MIB/95.723 MB
0.369兆PPS 11.252兆字節/94.389兆字節
0.370兆PPS 11.289兆字節/94.696兆字節
0.365兆PPS 11.152兆字節/93.552兆字節
0.360兆PPS 10.971兆字節/92.033兆字節
現在內核調度器在特定的CPU上運行進程,提高了處理器緩存,使數據更加壹致,這就是我們想要的!
2.發送更多數據包
雖然370k的pps對於簡單的程序來說已經不錯了,但是距離我們1Mpps的目標還很遠。為了接收更多,我們必須首先發送更多的數據包。我們用兩個獨立的線程發送怎麽樣?
殼
發件人$ taskset -c 1,2。/udpsender
192.168.254.1:4321 192.168.254.1:4321
接收器$ taskset -c 1。/UDP receiver 1 0 . 0 . 0 . 0:4321
0.349兆PPS 10.651兆字節/89.343兆字節
0.354兆PPS 10.815兆字節/90.724兆字節
0.354兆PPS 10.806兆字節/90.646兆字節
0.354兆PPS 10.811兆字節/90.690兆字節
接收端的數據沒有增加,ethtool–s命令將顯示數據包的實際去向:
殼
receiver $ watch ' sudo ethtool-S eth 2 | grep rx '
rx _ nodesc _ drop _ CNT:451.3k/s
rx-0 . rx _ packets:8.0/秒
rx-1 . rx _ packets:0.0/秒
rx-2 . rx _ packets:0.0/秒
rx-3 . rx _ packets:0.5/秒
rx-4.rx_packets: 355.2k/s
rx-5 . rx _ packets:0.0/秒
rx-6 . rx _ packets:0.0/秒
rx-7 . rx _ packets:0.5/秒
rx-8 . rx _ packets:0.0/秒
rx-9 . rx _ packets:0.0/秒
rx-10 . rx _ packets:0.0/秒
通過這些統計,NIC顯示RX queue 4已經成功傳輸了大約350Kpps。Rx_nodesc_drop_cnt是Solarflare的唯壹計數器,表示NIC向內核發送450kpps失敗。
有時候,不發送這些包的原因不是很清楚,但是在我們的情況下,很清楚:RX隊列4號向CPU 4號發送包,但是CPU 4號太忙了,因為它最忙的時候只能讀取350kpps。在htop中顯示為:
多隊列網卡速成班
歷史上,網卡只有壹個RX隊列,用於在硬件和內核之間傳輸數據包。這種設計的壹個明顯的限制是不可能處理比單個CPU更多的數據包。
為了利用多核系統,網卡開始支持多個接收隊列。這種設計很簡單:每個RX隊列都連接到壹個單獨的CPU,因此將數據包發送到所有RX隊列的網卡可以使用所有的CPU。但另壹個問題出現了:NIC如何決定將數據包發送到哪個RX隊列?
循環平衡是不可接受的,因為它可能會導致單個連接中的數據包重新排序。另壹種方法是使用數據包的哈希值來確定RX號碼。哈希值通常由壹個元組(源IP、目的IP、源端口、目的端口)計算得出。這確保了從壹個流中生成的分組將在完全相同的RX隊列中結束,並且不可能在壹個流中重新排列分組。
在我們的示例中,哈希值可能如下所示:
殼
1
RX _ queue _ number = hash(' 192.168.254 . 30 ',' 192.168.254 . 1 ',65400,4321) %隊列數
多隊列哈希算法
哈希算法由ethtool配置,設置如下:
殼
接收器$ ethtool -n eth2 rx-flow-hash udp4
IPV4上的UDP流使用這些字段來計算哈希流密鑰:
IP SA
IP DA
對於IPv4 UDP數據包,NIC將散列(源IP,目標IP)地址。也就是
殼
1
RX _ queue _ number = hash(' 192.168.254 . 30 ',' 192.168.254 . 1 ')% number _ of _ queues
這是非常有限的,因為它忽略了端口號。許多網卡允許自定義哈希。同樣,使用ethtool,我們可以選擇元組(源IP、目的IP、源端口和目的端口)來生成哈希值。
殼
接收器$ eth tool-N eth 2 rx-flow-hash UDP 4 sdfn
無法更改RX網絡流哈希選項:不支持該操作
可惜我們的NIC不支持定制,只能選擇(源IP,目的IP)生成hash。
NUMA業績報告
到目前為止,我們所有的數據包都流向壹個RX隊列和壹個CPU。我們可以以此為基準來衡量不同CPU的性能。在我們設置為接收器的主機上有兩個獨立的處理器,每個處理器都是壹個不同的NUMA節點。
在我們的設置中,您可以將單線程接收器連接到四個CPU中的壹個。這四個選項如下:
接收器運行在另壹個CPU上,但是同壹個NUMA節點被用作RX隊列。從上面我們可以看到,性能大約是360 kpps。
以運行接收方的同壹個CPU作為RX隊列,我們可以得到大約430 kpps。但是,這樣也會有很高的不穩定性。如果網卡被數據包淹沒,性能將下降到零。
當接收器運行在處理RX隊列的HT對應的CPU上時,性能是平時的壹半,大約200kpps。
接收器位於不同的NUMA節點上,而不是RX隊列的CPU上,其性能約為330 kpps。但數字會有所不同。
雖然在不同的NUMA節點上運行要花費10%,聽起來可能不算太糟糕,但是隨著規模越來越大,問題只會越來越嚴重。在壹些測試中,每個內核只能發出250 kpps,在所有跨NUMA測試中,這種不穩定性非常糟糕。在吞吐量較高的情況下,NUMA節點的性能損失更加明顯。在壹次測試中,發現當接收器在斷開的NUMA節點上運行時,其性能降低了4倍。
3.接收更多IP。
由於我們的網卡上哈希算法的限制,通過RX隊列分發數據包的唯壹方法是使用多個IP地址。以下是如何將數據包發送到不同的目的IP:
1
發件人$ taskset -c 1,2。/UDP sender 192.168.254 . 1:4321 192.168.254 . 2:4321
Ethtool確認數據包流向不同的接收隊列:
殼
receiver $ watch ' sudo ethtool-S eth 2 | grep rx '
rx-0 . rx _ packets:8.0/秒
rx-1 . rx _ packets:0.0/秒
rx-2 . rx _ packets:0.0/秒
rx-3.rx_packets: 355.2k/s
rx-4 . rx _ packets:0.5/秒
rx-5.rx_packets: 297.0k/s
rx-6 . rx _ packets:0.0/秒
rx-7 . rx _ packets:0.5/秒
rx-8 . rx _ packets:0.0/秒
rx-9 . rx _ packets:0.0/秒
rx-10 . rx _ packets:0.0/秒
接收部分:
殼
接收器$ taskset -c 1。/UDP receiver 1 0 . 0 . 0 . 0:4321
0.609兆PPS 18.599兆字節/156.019兆字節
0.657兆pps 20.039MiB兆字節/168.102兆字節
0.649兆PPS 19.803兆字節/166.120兆字節
萬歲!有兩個核心忙於處理RX隊列,第三個在運行應用程序時可以達到650 kpps左右!
我們可以通過向三個或四個RX隊列發送數據來增加這個數字,但很快這個應用程序就會出現另壹個瓶頸。Rx_nodesc_drop_cnt這次沒有增加,但是netstat收到了以下錯誤:
殼
接收器$ watch 'netstat -s - udp '
Udp:
收到437.0k/s數據包
收到0.0/s到未知端口的數據包。
386.9k/s數據包接收錯誤
每秒發送0.0個數據包
RcvbufErrors: 123.8k/s
SndbufErrors: 0
消費者錯誤數:0
這意味著盡管NIC可以向內核發送數據包,但內核不能向應用程序發送數據包。在我們的例子中,只能提供440 kpps,剩下的390 kpps+123 kpps會減少,因為應用程序接收它們的速度不夠快。
4.多線程接收
我們需要擴展接收器應用程序。最簡單的方法是使用多線程接收,但它不起作用:
殼
發件人$ taskset -c 1,2。/UDP sender 192.168.254 . 1:4321 192.168.254 . 2:4321
接收器$ taskset -c 1,2。/udpreceiver 1 0 . 0 . 0:4321 2
0.495兆PPS 15.108兆字節/126.733兆字節
0.480兆PPS 14.636兆字節/122.775兆字節
0.461M PPS 14.071 MIB/118.038 MB
0.486兆PPS 14.820兆字節/124.322兆字節
接收性能低於單線程,這是由UDP接收緩沖區上的鎖競爭造成的。因為兩個線程使用相同的套接字描述符,所以它們花費太多時間來競爭UDP接收緩沖區的鎖。本文對此問題進行了詳細描述。
似乎使用多線程從描述符接收不是最佳解決方案。
5.SO_REUSEPORT
幸運的是,Linux最近增加了壹個解決方案——so _ reuse端口標誌。當在套接字描述符上設置這個標誌位時,Linux將允許許多進程綁定到同壹個端口。事實上,將允許綁定任意數量的進程,並且負載將均勻分布。
使用SO_REUSEPORT,每個進程都有壹個獨立的套接字描述符。因此每個都有壹個專用的UDP接收緩沖區。這就避免了以前遇到的競爭問題:
殼
1
2
三
四
接收器$ taskset -c 1,2,3,4。/UDP receiver 1 0 . 0 . 0 . 0:4321 4 1
1.114M PPS 34.007 MIB/285.271Mb
1.147兆字節/293.518兆字節
1.126兆pps 34.374MiB兆字節/288.354兆字節
現在更喜歡了,吞吐量很好!
更多的調查顯示,還有進壹步改善的空間。即使我們啟動四個接收線程,負載也不會均勻分布:
兩個進程接收所有工作,而另外兩個進程根本沒有數據包。這是因為哈希沖突,但這次是在SO_REUSEPORT層。
結束語
我做了進壹步的測試。使用完全壹致的RX隊列,接收線程在單個NUMA節點上可以達到1.4Mpps。在不同的NUMA節點上運行接收器將導致這個數字下降到1Mpps。
簡而言之,如果妳想要完美的表現,妳需要做到以下幾點:
確保流量均勻分布在許多RX隊列和SO_REUSEPORT進程上。在實踐中,只要有很多連接(或流),負載通常是分布的。
需要有足夠的CPU能力從內核獲取數據包。
讓事情變得更困難的是,接收隊列和接收方進程應該在同壹個NUMA節點上。
為了使事情更加穩定,RX隊列和接收過程應該在同壹個NUMA節點上。
盡管我們已經證明了在Linux機器上接收1Mpps在技術上是可行的,但是應用程序不會對接收到的數據包進行任何實際的處理——甚至不會查看內容流量。不要對這樣的性能期望太高,因為對於任何實際應用來說都不是很有用。