在開始之前,我們先用一張圖解釋 linux 系統接收網絡報文的過程。
1、首先網絡報文通過物理網線發送到網卡
2、網絡驅動程序會把網絡中的報文讀出來放到 ring buffer 中,這個過程使用 DMA(Direct Memory Access),不需要 CPU 參與
3、內核從 ring buffer 中讀取報文進行處理,執行 IP 和 TCP/UDP 層的邏輯,最後把報文放到應用程式的 socket buffer 中
4、應用程式從 socket buffer 中讀取報文進行處理
在接收 UDP 報文的過程中,圖中任何一個過程都可能會主動或者被動地把報文丟棄,因此丟包可能發生在網卡和驅動,也可能發生在系統和應用。
之所以沒有分析發送數據流程,一是因為發送流程和接收類似,只是方向相反;另外發送流程報文丟失的概率比接收小,只有在應用程式發送的報文速率大於內核和網卡處理速率時才會發生。
本篇文章假定機器只有一個名字為 eth0 的 interface,如果有多個 interface 或者 interface 的名字不是 eth0,請按照實際情況進行分析。
NOTE:文中出現的 RX(receive) 表示接收報文,TX(transmit) 表示發送報文。
確認有 UDP 丟包發生要查看網卡是否有丟包,可以使用 ethtool -S eth0 查看,在輸出中查找 bad 或者 drop 對應的欄位是否有數據,在正常情況下,這些欄位對應的數字應該都是 0。如果看到對應的數字在不斷增長,就說明網卡有丟包。
另外一個查看網卡丟包數據的命令是 ifconfig,它的輸出中會有 RX(receive 接收報文)和 TX(transmit 發送報文)的統計數據:
# ifconfig enp1
enp2s0f1: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
ether 04:b0:e7:fa:75:9d txqueuelen 1000 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device memory 0x92200000-922fffff
此外,linux 系統也提供了各個網絡協議的丟包信息,可以使用 netstat -s 命令查看,加上 --udp 可以只看 UDP 相關的報文數據:
# netstat -s -u
IcmpMsg:
InType0: 17
InType3: 75
InType8: 77
OutType0: 77
OutType3: 692
OutType8: 249
Udp:
5728807 packets received
12 packets to unknown port received.
0 packet receive errors
982710 packets sent
0 receive buffer errors
0 send buffer errors
UdpLite:
IpExt:
InNoRoutes: 3
InBcastPkts: 497633
InOctets: 1044710406807
OutOctets: 17460621991142
InBcastOctets: 114600482
InNoECTPkts: 2886955071
對於上面的輸出,關注下面的信息來查看 UDP 丟包的情況:
packet receive errors 不為空,並且在一直增長說明系統有 UDP 丟包
packets to unknown port received 表示系統接收到的 UDP 報文所在的目標埠沒有應用在監聽,一般是服務沒有啟動導致的,並不會造成嚴重的問題
receive buffer errors 表示因為 UDP 的接收緩存太小導致丟包的數量
NOTE:並不是丟包數量不為零就有問題,對於 UDP 來說,如果有少量的丟包很可能是預期的行為,比如丟包率(丟包數量/接收報文數量)在萬分之一甚至更低。
網卡或者驅動丟包之前講過,如果 ethtool -S eth0 中有 rx_***_errors 那麼很可能是網卡有問題,導致系統丟包,需要聯繫伺服器或者網卡供應商進行處理。
# ethtool -S enp1 | grep rx_ | grep errors
rx_crc_errors: 0
rx_missed_errors: 0
rx_long_length_errors: 0
rx_short_length_errors: 0
rx_align_errors: 0
rx_errors: 0
rx_length_errors: 0
rx_over_errors: 0
rx_frame_errors: 0
rx_fifo_errors: 0
netstat -i 也會提供每個網卡的接發報文以及丟包的情況,正常情況下輸出中 error 或者 drop 應該為 0。
如果硬體或者驅動沒有問題,一般網卡丟包是因為設置的緩存區(ring buffer)太小,可以使用 ethtool 命令查看和設置網卡的 ring buffer。
ethtool -g 可以查看某個網卡的 ring buffer,比如下面的例子
# ethtool -g enp1
Ring parameters for enp2s0f1:
Pre-set maximums:
RX: 4096
RX Mini: 0
RX Jumbo: 0
TX: 4096
Current hardware settings:
RX: 256
RX Mini: 0
RX Jumbo: 0
TX: 256
Pre-set 表示網卡最大的 ring buffer 值,可以使用 ethtool -G eth0 rx 8192 設置它的值。
Linux 系統丟包linux 系統丟包的原因很多,常見的有:UDP 報文錯誤、防火牆、UDP buffer size 不足、系統負載過高等,這裡對這些丟包原因進行分析。
UDP 報文錯誤如果在傳輸過程中UDP 報文被修改,會導致 checksum 錯誤,或者長度錯誤,linux 在接收到 UDP 報文時會對此進行校驗,一旦發明錯誤會把報文丟棄。
如果希望 UDP 報文 checksum 及時有錯也要發送給應用程式,可以在通過 socket 參數禁用 UDP checksum 檢查。
防火牆如果系統防火牆丟包,表現的行為一般是所有的 UDP 報文都無法正常接收,當然不排除防火牆只 drop 一部分報文的可能性。
如果遇到丟包比率非常大的情況,請先檢查防火牆規則,保證防火牆沒有主動 drop UDP 報文。
UDP buffer size 不足linux 系統在接收報文之後,會把報文保存到緩存區中。因為緩存區的大小是有限的,如果出現 UDP 報文過大(超過緩存區大小或者 MTU 大小)、接收到報文的速率太快,都可能導致 linux 因為緩存滿而直接丟包的情況。
在系統層面,linux 設置了 receive buffer 可以配置的最大值,可以在下面的文件中查看,一般是 linux 在啟動的時候會根據內存大小設置一個初始值。
/proc/sys/net/core/rmem_max:允許設置的 receive buffer 最大值
/proc/sys/net/core/rmem_default:默認使用的 receive buffer 值
/proc/sys/net/core/wmem_max:允許設置的 send buffer 最大值
/proc/sys/net/core/wmem_dafault:默認使用的 send buffer 最大值
但是這些初始值並不是為了應對大流量的 UDP 報文,如果應用程式接收和發送 UDP 報文非常多,需要講這個值調大。可以使用 sysctl 命令讓它立即生效:
# sysctl -w net.core.rmem_max=26214400 # 設置為 25M
net.core.rmem_max = 26214400
也可以修改 /etc/sysctl.conf 中對應的參數在下次啟動時讓參數保持生效。
如果報文報文過大,可以在發送方對數據進行分割,保證每個報文的大小在 MTU 內。
另外一個可以配置的參數是 netdev_max_backlog,它表示 linux 內核從網卡驅動中讀取報文後可以緩存的報文數量,默認是 1000,可以調大這個值,比如設置成 2000:
# sudo sysctl -w net.core.netdev_max_backlog=2000
net.core.netdev_max_backlog = 2000
系統 CPU、memory、IO 負載過高都有可能導致網絡丟包,比如 CPU 如果負載過高,系統沒有時間進行報文的 checksum 計算、複製內存等操作,從而導致網卡或者 socket buffer 出丟包;memory 負載過高,會應用程式處理過慢,無法及時處理報文;IO 負載過高,CPU 都用來響應 IO wait,沒有時間處理緩存中的 UDP 報文。
linux 系統本身就是相互關聯的系統,任何一個組件出現問題都有可能影響到其他組件的正常運行。對於系統負載過高,要麼是應用程式有問題,要麼是系統不足。對於前者需要及時發現,debug 和修復;對於後者,也要及時發現並擴容。
應用丟包上面提到系統的 UDP buffer size,調節的 sysctl 參數只是系統允許的最大值,每個應用程式在創建 socket 時需要設置自己 socket buffer size 的值。
linux 系統會把接受到的報文放到 socket 的 buffer 中,應用程式從 buffer 中不斷地讀取報文。所以這裡有兩個和應用有關的因素會影響是否會丟包:socket buffer size 大小以及應用程式讀取報文的速度。
對於第一個問題,可以在應用程式初始化 socket 的時候設置 socket receive buffer 的大小,比如下面的代碼把 socket buffer 設置為 20MB:
uint64_t receive_buf_size = 20*1024*1024; //20 MB
setsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &receive_buf_size, sizeof(receive_buf_size));
如果不是自己編寫和維護的程序,修改應用代碼是件不好甚至不太可能的事情。很多應用程式會提供配置參數來調節這個值,請參考對應的官方文檔;如果沒有可用的配置參數,只能給程序的開發者提 issue 了。
很明顯,增加應用的 receive buffer 會減少丟包的可能性,但同時會導致應用使用更多的內存,所以需要謹慎使用。
另外一個因素是應用讀取 buffer 中報文的速度,對於應用程式來說,處理報文應該採取異步的方式
包丟在什麼地方想要詳細了解 linux 系統在執行哪個函數時丟包的話,可以使用 dropwatch 工具,它監聽系統丟包信息,並列印出丟包發生的函數地址:
# dropwatch -l kas
Initalizing kallsyms db
dropwatch> start
Enabling monitoring...
Kernel monitoring activated.
Issue Ctrl-C to stop monitoring
1 drops at tcp_v4_do_rcv+cd (0xffffffff81799bad)
10 drops at tcp_v4_rcv+80 (0xffffffff8179a620)
1 drops at sk_stream_kill_queues+57 (0xffffffff81729ca7)
4 drops at unix_release_sock+20e (0xffffffff817dc94e)
1 drops at igmp_rcv+e1 (0xffffffff817b4c41)
1 drops at igmp_rcv+e1 (0xffffffff817b4c41)
通過這些信息,找到對應的內核代碼處,就能知道內核在哪個步驟中把報文丟棄,以及大致的丟包原因。本人在排查這個問題過程中更傾向於在各個機器抓包,這個方法更適合追蹤自身業務出現問題導致丟包,如下所示:
tcpdump -i 網絡接口名稱 udp port 2020 -s0 -XX -nn
此外,還可以使用 linux perf 工具監聽 kfree_skb(把網絡報文丟棄時會調用該函數) 事件的發生:
sudo perf record -g -a -e skb:kfree_skb
sudo perf script
關於 perf 命令的使用和解讀,網上有很多文章可以參考。
總結
UDP 本身就是無連接不可靠的協議,適用於報文偶爾丟失也不影響程序狀態的場景,比如視頻、音頻、遊戲、監控等。對報文可靠性要求比較高的應用不要使用 UDP,推薦直接使用 TCP。當然,也可以在應用層做重試、去重保證可靠性;
如果發現伺服器丟包,首先通過監控查看系統負載是否過高,先想辦法把負載降低再看丟包問題是否消失;
如果系統負載過高,UDP 丟包是沒有有效解決方案的。如果是應用異常導致 CPU、memory、IO 過高,請及時定位異常應用並修復;如果是資源不夠,監控應該能及時發現並快速擴容;
對於系統大量接收或者發送 UDP 報文的,可以通過調節系統和程序的 socket buffer size 來降低丟包的概率;
應用程式在處理 UDP 報文時,要採用異步方式,在兩次接收報文之間不要有太多的處理邏輯。