如何解決Linux系統(tǒng)UDP丟包問題?辦法都在這里!
作者:CloudDeveloper
鏈接:https://cizixs.com/2018/01/13/linux-udp-packet-drop-debug/
最近工作中遇到某個服務(wù)器應(yīng)用程序 UDP 丟包,在排查過程中查閱了很多資料,我在排查過程中基本都是通過使用 tcpdump 在出現(xiàn)問題的各個環(huán)節(jié)上進(jìn)行抓包、分析在那個環(huán)節(jié)出現(xiàn)問題、針對性去排查解決問題,對癥下藥,最后終究能夠解決問題。但是這種情況大多是因?yàn)榉?wù)本身的問題,如果是環(huán)境問題、操作系統(tǒng)、甚至硬件的問題,可能從服務(wù)本身出發(fā)不能解決問題,但是這篇文章另辟蹊徑,從外部環(huán)境分析可能丟包的原因,看完之后,很受用,部分章節(jié)對原文有所修改,下面分享出來供更多人參考。
在開始之前,我們先用一張圖解釋 linux 系統(tǒng)接收網(wǎng)絡(luò)報文的過程。
首先網(wǎng)絡(luò)報文通過物理網(wǎng)線發(fā)送到網(wǎng)卡
網(wǎng)絡(luò)驅(qū)動程序會把網(wǎng)絡(luò)中的報文讀出來放到 ring buffer 中,這個過程使用 DMA(Direct Memory Access),不需要 CPU 參與
內(nèi)核從 ring buffer 中讀取報文進(jìn)行處理,執(zhí)行 IP 和 TCP/UDP 層的邏輯,最后把報文放到應(yīng)用程序的 socket buffer 中
應(yīng)用程序從 socket buffer 中讀取報文進(jìn)行處理
在接收 UDP 報文的過程中,圖中任何一個過程都可能會主動或者被動地把報文丟棄,因此丟包可能發(fā)生在網(wǎng)卡和驅(qū)動,也可能發(fā)生在系統(tǒng)和應(yīng)用。
之所以沒有分析發(fā)送數(shù)據(jù)流程,一是因?yàn)榘l(fā)送流程和接收類似,只是方向相反;另外發(fā)送流程報文丟失的概率比接收小,只有在應(yīng)用程序發(fā)送的報文速率大于內(nèi)核和網(wǎng)卡處理速率時才會發(fā)生。
本篇文章假定機(jī)器只有一個名字為 eth0
的 interface,如果有多個 interface 或者 interface 的名字不是 eth0,請按照實(shí)際情況進(jìn)行分析。
NOTE:文中出現(xiàn)的 RX
(receive) 表示接收報文,TX
(transmit) 表示發(fā)送報文。
確認(rèn)有 UDP 丟包發(fā)生
要查看網(wǎng)卡是否有丟包,可以使用 ethtool -S eth0
查看,在輸出中查找 bad
或者 drop
對應(yīng)的字段是否有數(shù)據(jù),在正常情況下,這些字段對應(yīng)的數(shù)字應(yīng)該都是 0。如果看到對應(yīng)的數(shù)字在不斷增長,就說明網(wǎng)卡有丟包。
另外一個查看網(wǎng)卡丟包數(shù)據(jù)的命令是 ifconfig
,它的輸出中會有 RX
(receive 接收報文)和 TX
(transmit 發(fā)送報文)的統(tǒng)計數(shù)據(jù):
[root@k8s-master ng]# 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 系統(tǒng)也提供了各個網(wǎng)絡(luò)協(xié)議的丟包信息,可以使用 netstat -s
命令查看,加上 --udp
可以只看 UDP 相關(guān)的報文數(shù)據(jù):
[root@k8s-master ng]# 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
對于上面的輸出,關(guān)注下面的信息來查看 UDP 丟包的情況:
packet receive errors
不為空,并且在一直增長說明系統(tǒng)有 UDP 丟包packets to unknown port received
表示系統(tǒng)接收到的 UDP 報文所在的目標(biāo)端口沒有應(yīng)用在監(jiān)聽,一般是服務(wù)沒有啟動導(dǎo)致的,并不會造成嚴(yán)重的問題receive buffer errors
表示因?yàn)?UDP 的接收緩存太小導(dǎo)致丟包的數(shù)量
NOTE:并不是丟包數(shù)量不為零就有問題,對于 UDP 來說,如果有少量的丟包很可能是預(yù)期的行為,比如丟包率(丟包數(shù)量/接收報文數(shù)量)在萬分之一甚至更低。
網(wǎng)卡或者驅(qū)動丟包
之前講過,如果 ethtool -S eth0
中有 rx_***_errors
那么很可能是網(wǎng)卡有問題,導(dǎo)致系統(tǒng)丟包,需要聯(lián)系服務(wù)器或者網(wǎng)卡供應(yīng)商進(jìn)行處理。
[root@k8s-master ng]# 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
也會提供每個網(wǎng)卡的接發(fā)報文以及丟包的情況,正常情況下輸出中 error 或者 drop 應(yīng)該為 0。
如果硬件或者驅(qū)動沒有問題,一般網(wǎng)卡丟包是因?yàn)樵O(shè)置的緩存區(qū)(ring buffer)太小,可以使用 ethtool
命令查看和設(shè)置網(wǎng)卡的 ring buffer。
ethtool -g
可以查看某個網(wǎng)卡的 ring buffer,比如下面的例子
[root@k8s-master ng]# 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 表示網(wǎng)卡最大的 ring buffer 值,可以使用 ethtool -G eth0 rx 8192
設(shè)置它的值。
Linux 系統(tǒng)丟包
linux 系統(tǒng)丟包的原因很多,常見的有:UDP 報文錯誤、防火墻、UDP buffer size 不足、系統(tǒng)負(fù)載過高等,這里對這些丟包原因進(jìn)行分析。
UDP 報文錯誤
如果在傳輸過程中UDP 報文被修改,會導(dǎo)致 checksum 錯誤,或者長度錯誤,linux 在接收到 UDP 報文時會對此進(jìn)行校驗(yàn),一旦發(fā)明錯誤會把報文丟棄。
如果希望 UDP 報文 checksum 及時有錯也要發(fā)送給應(yīng)用程序,可以在通過 socket 參數(shù)禁用 UDP checksum 檢查。
防火墻
如果系統(tǒng)防火墻丟包,表現(xiàn)的行為一般是所有的 UDP 報文都無法正常接收,當(dāng)然不排除防火墻只 drop 一部分報文的可能性。
如果遇到丟包比率非常大的情況,請先檢查防火墻規(guī)則,保證防火墻沒有主動 drop UDP 報文。
UDP buffer size 不足
linux 系統(tǒng)在接收報文之后,會把報文保存到緩存區(qū)中。因?yàn)榫彺鎱^(qū)的大小是有限的,如果出現(xiàn) UDP 報文過大(超過緩存區(qū)大小或者 MTU 大小)、接收到報文的速率太快,都可能導(dǎo)致 linux 因?yàn)榫彺鏉M而直接丟包的情況。
在系統(tǒng)層面,linux 設(shè)置了 receive buffer 可以配置的最大值,可以在下面的文件中查看,一般是 linux 在啟動的時候會根據(jù)內(nèi)存大小設(shè)置一個初始值。
/proc/sys/net/core/rmem_max:允許設(shè)置的 receive buffer 最大值
/proc/sys/net/core/rmem_default:默認(rèn)使用的 receive buffer 值
/proc/sys/net/core/wmem_max:允許設(shè)置的 send buffer 最大值
/proc/sys/net/core/wmem_dafault:默認(rèn)使用的 send buffer 最大值
但是這些初始值并不是為了應(yīng)對大流量的 UDP 報文,如果應(yīng)用程序接收和發(fā)送 UDP 報文非常多,需要講這個值調(diào)大??梢允褂?nbsp;sysctl
命令讓它立即生效:
[root@k8s-master ~]# sysctl -w net.core.rmem_max=26214400 # 設(shè)置為 25M
net.core.rmem_max = 26214400
也可以修改 /etc/sysctl.conf
中對應(yīng)的參數(shù)在下次啟動時讓參數(shù)保持生效。
如果報文報文過大,可以在發(fā)送方對數(shù)據(jù)進(jìn)行分割,保證每個報文的大小在 MTU 內(nèi)。
另外一個可以配置的參數(shù)是 netdev_max_backlog
,它表示 linux 內(nèi)核從網(wǎng)卡驅(qū)動中讀取報文后可以緩存的報文數(shù)量,默認(rèn)是 1000,可以調(diào)大這個值,比如設(shè)置成 2000:
[root@k8s-master ~]# sudo sysctl -w net.core.netdev_max_backlog=2000
net.core.netdev_max_backlog = 2000
[root@k8s-master ~]#
系統(tǒng)負(fù)載過高
系統(tǒng) CPU、memory、IO 負(fù)載過高都有可能導(dǎo)致網(wǎng)絡(luò)丟包,比如 CPU 如果負(fù)載過高,系統(tǒng)沒有時間進(jìn)行報文的 checksum 計算、復(fù)制內(nèi)存等操作,從而導(dǎo)致網(wǎng)卡或者 socket buffer 出丟包;memory 負(fù)載過高,會應(yīng)用程序處理過慢,無法及時處理報文;IO 負(fù)載過高,CPU 都用來響應(yīng) IO wait,沒有時間處理緩存中的 UDP 報文。
linux 系統(tǒng)本身就是相互關(guān)聯(lián)的系統(tǒng),任何一個組件出現(xiàn)問題都有可能影響到其他組件的正常運(yùn)行。對于系統(tǒng)負(fù)載過高,要么是應(yīng)用程序有問題,要么是系統(tǒng)不足。對于前者需要及時發(fā)現(xiàn),debug 和修復(fù);對于后者,也要及時發(fā)現(xiàn)并擴(kuò)容。
應(yīng)用丟包
上面提到系統(tǒng)的 UDP buffer size,調(diào)節(jié)的 sysctl 參數(shù)只是系統(tǒng)允許的最大值,每個應(yīng)用程序在創(chuàng)建 socket 時需要設(shè)置自己 socket buffer size 的值。
linux 系統(tǒng)會把接受到的報文放到 socket 的 buffer 中,應(yīng)用程序從 buffer 中不斷地讀取報文。所以這里有兩個和應(yīng)用有關(guān)的因素會影響是否會丟包:socket buffer size 大小以及應(yīng)用程序讀取報文的速度。
對于第一個問題,可以在應(yīng)用程序初始化 socket 的時候設(shè)置 socket receive buffer 的大小,比如下面的代碼把 socket buffer 設(shè)置為 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));
如果不是自己編寫和維護(hù)的程序,修改應(yīng)用代碼是件不好甚至不太可能的事情。很多應(yīng)用程序會提供配置參數(shù)來調(diào)節(jié)這個值,請參考對應(yīng)的官方文檔;如果沒有可用的配置參數(shù),只能給程序的開發(fā)者提 issue 了。
很明顯,增加應(yīng)用的 receive buffer 會減少丟包的可能性,但同時會導(dǎo)致應(yīng)用使用更多的內(nèi)存,所以需要謹(jǐn)慎使用。
另外一個因素是應(yīng)用讀取 buffer 中報文的速度,對于應(yīng)用程序來說,處理報文應(yīng)該采取異步的方式
包丟在什么地方
想要詳細(xì)了解 linux 系統(tǒng)在執(zhí)行哪個函數(shù)時丟包的話,可以使用 dropwatch
工具,它監(jiān)聽系統(tǒng)丟包信息,并打印出丟包發(fā)生的函數(shù)地址:
# 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)
通過這些信息,找到對應(yīng)的內(nèi)核代碼處,就能知道內(nèi)核在哪個步驟中把報文丟棄,以及大致的丟包原因。本人在排查這個問題過程中更傾向于在各個機(jī)器抓包,這個方法更適合追蹤自身業(yè)務(wù)出現(xiàn)問題導(dǎo)致丟包,如下所示:
tcpdump -i 網(wǎng)絡(luò)接口名稱 udp port 2020 -s0 -XX -nn
此外,還可以使用 linux perf 工具監(jiān)聽 kfree_skb
(把網(wǎng)絡(luò)報文丟棄時會調(diào)用該函數(shù)) 事件的發(fā)生:
sudo perf record -g -a -e skb:kfree_skb
sudo perf script
關(guān)于 perf 命令的使用和解讀,網(wǎng)上有很多文章可以參考。
總結(jié)
UDP 本身就是無連接不可靠的協(xié)議,適用于報文偶爾丟失也不影響程序狀態(tài)的場景,比如視頻、音頻、游戲、監(jiān)控等。對報文可靠性要求比較高的應(yīng)用不要使用 UDP,推薦直接使用 TCP。當(dāng)然,也可以在應(yīng)用層做重試、去重保證可靠性
如果發(fā)現(xiàn)服務(wù)器丟包,首先通過監(jiān)控查看系統(tǒng)負(fù)載是否過高,先想辦法把負(fù)載降低再看丟包問題是否消失
如果系統(tǒng)負(fù)載過高,UDP 丟包是沒有有效解決方案的。如果是應(yīng)用異常導(dǎo)致 CPU、memory、IO 過高,請及時定位異常應(yīng)用并修復(fù);如果是資源不夠,監(jiān)控應(yīng)該能及時發(fā)現(xiàn)并快速擴(kuò)容
對于系統(tǒng)大量接收或者發(fā)送 UDP 報文的,可以通過調(diào)節(jié)系統(tǒng)和程序的 socket buffer size 來降低丟包的概率
應(yīng)用程序在處理 UDP 報文時,要采用異步方式,在兩次接收報文之間不要有太多的處理邏輯
--END--
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!