最近工作中遇到某個服務(wù)器應(yīng)用程序 UDP 丟包,在排查過程中查閱了很多資料,總結(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ù)流程,一是因為發(fā)送流程和接收類似,只是方向相反;另外發(fā)送流程報文丟失的概率比接收小,只有在應(yīng)用程序發(fā)送的報文速率大于內(nèi)核和網(wǎng)卡處理速率時才會發(fā)生。
本篇文章假定機器只有一個名字為 eth0 的 interface,如果有多個 interface 或者 interface 的名字不是 eth0,請按照實際情況進(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ù):
~# 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 系統(tǒng)也提供了各個網(wǎng)絡(luò)協(xié)議的丟包信息,可以使用 netstat -s 命令查看,加上 --udp 可以只看 UDP 相關(guān)的報文數(shù)據(jù):
[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
對于上面的輸出,關(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 表示因為 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)行處理。
# 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 也會提供每個網(wǎng)卡的接發(fā)報文以及丟包的情況,正常情況下輸出中 error 或者 drop 應(yīng)該為 0。
如果硬件或者驅(qū)動沒有問題,一般網(wǎng)卡丟包是因為設(shè)置的緩存區(qū)(ring buffer)太小,可以使用 ethtool 命令查看和設(shè)置網(wǎng)卡的 ring buffer。
ethtool -g 可以查看某個網(wǎng)卡的 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 表示網(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)行校驗,一旦發(fā)明錯誤會把報文丟棄。
如果希望 UDP 報文 checksum 及時有錯也要發(fā)送給應(yīng)用程序,可以在通過 socket 參數(shù)禁用 UDP checksum 檢查:
int disable = 1;
setsockopt(sock_fd, SOL_SOCKET, SO_NO_CHECK, (void*)&disable, sizeof(disable)
防火墻
如果系統(tǒng)防火墻丟包,表現(xiàn)的行為一般是所有的 UDP 報文都無法正常接收,當(dāng)然不排除防火墻只 drop 一部分報文的可能性。
如果遇到丟包比率非常大的情況,請先檢查防火墻規(guī)則,保證防火墻沒有主動 drop UDP 報文。
UDP buffer size 不足
linux 系統(tǒng)在接收報文之后,會把報文保存到緩存區(qū)中。因為緩存區(qū)的大小是有限的,如果出現(xiàn) UDP 報文過大(超過緩存區(qū)大小或者 MTU 大小)、接收到報文的速率太快,都可能導(dǎo)致 linux 因為緩存滿而直接丟包的情況。
在系統(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)大??梢允褂?sysctl 命令讓它立即生效:
sysctl -w net.core.rmem_max=26214400 # 設(shè)置為 25M
也可以修改 /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:
sudo sysctl -w net.core.netdev_max_backlog=2000
系統(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)問題都有可能影響到其他組件的正常運行。對于系統(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)核在哪個步驟中把報文丟棄,以及大致的丟包原因。
此外,還可以使用 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 報文時,要采用異步方式,在兩次接收報文之間不要有太多的處理邏輯
-
Linux
+關(guān)注
關(guān)注
87文章
11326瀏覽量
209961 -
服務(wù)器
+關(guān)注
關(guān)注
12文章
9253瀏覽量
85743 -
UDP
+關(guān)注
關(guān)注
0文章
327瀏覽量
34005 -
報文
+關(guān)注
關(guān)注
0文章
38瀏覽量
4059
發(fā)布評論請先 登錄
相關(guān)推薦
評論