最近工做中遇到某個服務器應用程序 UDP 丟包,在排查過程當中查閱了不少資料,總結出來這篇文章,供更多人蔘考。linux
在開始以前,咱們先用一張圖解釋 linux 系統接收網絡報文的過程。nginx
在接收 UDP 報文的過程當中,圖中任何一個過程均可能會主動或者被動地把報文丟棄,所以丟包可能發生在網卡和驅動,也可能發生在系統和應用。緩存
之因此沒有分析發送數據流程,一是由於發送流程和接收相似,只是方向相反;另外發送流程報文丟失的機率比接收小,只有在應用程序發送的報文速率大於內核和網卡處理速率時纔會發生。bash
本篇文章假定機器只有一個名字爲 eth0
的 interface,若是有多個 interface 或者 interface 的名字不是 eth0,請按照實際狀況進行分析。服務器
NOTE:文中出現的 RX
(receive) 表示接收報文,TX
(transmit) 表示發送報文。網絡
要查看網卡是否有丟包,可使用 ethtool -S eth0
查看,在輸出中查找 bad
或者 drop
對應的字段是否有數據,在正常狀況下,這些字段對應的數字應該都是 0。若是看到對應的數字在不斷增加,就說明網卡有丟包。異步
另一個查看網卡丟包數據的命令是 ifconfig
,它的輸出中會有 RX
(receive 接收報文)和 TX
(transmit 發送報文)的統計數據:socket
~# ifconfig eth0 ... RX packets 3553389376 bytes 2599862532475 (2.3 TiB) RX errors 0 dropped 1353 overruns 0 frame 0 TX packets 3479495131 bytes 3205366800850 (2.9 TiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 ...
此外,linux 系統也提供了各個網絡協議的丟包信息,可使用 netstat -s
命令查看,加上 --udp
能夠只看 UDP 相關的報文數據:tcp
[root@holodesk02 GOD]# netstat -s -u IcmpMsg: InType0: 3 InType3: 1719356 InType8: 13 InType11: 59 OutType0: 13 OutType3: 1737641 OutType8: 10 OutType11: 263 Udp: 517488890 packets received 2487375 packets to unknown port received. 47533568 packet receive errors 147264581 packets sent 12851135 receive buffer errors 0 send buffer errors UdpLite: IpExt: OutMcastPkts: 696 InBcastPkts: 2373968 InOctets: 4954097451540 OutOctets: 5538322535160 OutMcastOctets: 79632 InBcastOctets: 934783053 InNoECTPkts: 5584838675
對於上面的輸出,關注下面的信息來查看 UDP 丟包的狀況:
packet receive errors
不爲空,而且在一直增加說明系統有 UDP 丟包packets to unknown port received
表示系統接收到的 UDP 報文所在的目標端口沒有應用在監聽,通常是服務沒有啓動致使的,並不會形成嚴重的問題receive buffer errors
表示由於 UDP 的接收緩存過小致使丟包的數量NOTE: 並非丟包數量不爲零就有問題,對於 UDP 來講,若是有少許的丟包極可能是預期的行爲,好比丟包率(丟包數量/接收報文數量)在萬分之一甚至更低。
以前講過,若是 ethtool -S eth0
中有 rx_***_errors
那麼極可能是網卡有問題,致使系統丟包,須要聯繫服務器或者網卡供應商進行處理。
# ethtool -S eth0 | 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 eth0 Ring parameters for eth0: 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 系統丟包的緣由不少,常見的有:UDP 報文錯誤、防火牆、UDP buffer size 不足、系統負載太高等,這裏對這些丟包緣由進行分析。
若是在傳輸過程當中UDP 報文被修改,會致使 checksum 錯誤,或者長度錯誤,linux 在接收到 UDP 報文時會對此進行校驗,一旦發明錯誤會把報文丟棄。
若是但願 UDP 報文 checksum 及時有錯也要發送給應用程序,能夠在經過 socket 參數禁用 UDP checksum 檢查:
int disable = 1; setsockopt(sock_fd, SOL_SOCKET, SO_NO_CHECK, (void*)&disable, sizeof(disable)
若是系統防火牆丟包,表現的行爲通常是全部的 UDP 報文都沒法正常接收,固然不排除防火牆只 drop 一部分報文的可能性。
若是遇到丟包比率很是大的狀況,請先檢查防火牆規則,保證防火牆沒有主動 drop UDP 報文。
linux 系統在接收報文以後,會把報文保存到緩存區中。由於緩存區的大小是有限的,若是出現 UDP 報文過大(超過緩存區大小或者 MTU 大小)、接收到報文的速率太快,均可能致使 linux 由於緩存滿而直接丟包的狀況。
在系統層面,linux 設置了 receive buffer 能夠配置的最大值,能夠在下面的文件中查看,通常是 linux 在啓動的時候會根據內存大小設置一個初始值。
可是這些初始值並非爲了應對大流量的 UDP 報文,若是應用程序接收和發送 UDP 報文很是多,須要講這個值調大。可使用 sysctl
命令讓它當即生效:
sysctl -w net.core.rmem_max=26214400 # 設置爲 25M
也能夠修改 /etc/sysctl.conf
中對應的參數在下次啓動時讓參數保持生效。
若是報文報文過大,能夠在發送方對數據進行分割,保證每一個報文的大小在 MTU 內。
另一個能夠配置的參數是 netdev_max_backlog
,它表示 linux 內核從網卡驅動中讀取報文後能夠緩存的報文數量,默認是 1000,能夠調大這個值,好比設置成 2000:
sudo sysctl -w 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)
經過這些信息,找到對應的內核代碼處,就能知道內核在哪一個步驟中把報文丟棄,以及大體的丟包緣由。
此外,還可使用 linux perf 工具監聽 kfree_skb
(把網絡報文丟棄時會調用該函數) 事件的發生:
sudo perf record -g -a -e skb:kfree_skb sudo perf script
關於 perf 命令的使用和解讀,網上有不少文章能夠參考。