轉自:https://blog.csdn.net/xingzheouc/article/details/49946191
1. UDP概念
用戶數據報協議(英語:User Datagram Protocol,縮寫為 UDP),又稱使用者資料包協定,是一個簡單的面向數據報的傳輸層協議,正式規范為RFC 768
在TCP/IP模型中,UDP為網絡層以上和應用層以下提供了一個簡單的接口。UDP只提供數據的不可靠傳遞,它一旦把應用程序發給網絡層的數據發送出去,就不保留數據備份(所以UDP有時候也被認為是不可靠的數據報協議)。UDP在IP數據報的頭部僅僅加入了復用和數據校驗(字段)。
2. UDP丟包問題分析

由於UDP協議只提供數據的不可靠傳輸,數據包發出去后就不管了,數據包在網絡的傳輸過程中都可能丟失。甚至即使數據包成功到達了接收端節點,也不意味着應用程序能夠收到,因為要從網卡到達應用程序還需要經歷很多階段,每個階段都可能丟包。
上圖描述了一種應用程序接受網絡數據包的典型路徑圖。
首先,NIC(網卡)接收和處理網絡數據包。網卡有自己的硬件數據緩沖區,當網絡數據流量太大,大到超過網卡的接收數據硬件緩沖區,這時新進入的數據包將覆蓋之前緩沖區的數據包,導致丟失。網卡是否丟包取決於網卡本身的計算性能和硬件緩沖區的大小。
其次,網卡處理后把數據包交給操作系統緩沖區。數據包在操作系統階段丟包主要取決於以下因素:
- 操作系統緩沖區的大小
- 系統的性能
- 系統的負載
- 網絡相關的系統負載
最后,當數據包到達應用程序的socket緩沖區,如果應用程序不能及時從socket緩沖區把數據包取走,累積的數據包將會超出應用程序socket緩沖區閥值,導致緩沖區溢出,數據包丟失。數據包在應用程序階段丟包主要取決於以下因素:
- 應用程序緩沖區大小
- 應用程序處理數據包的能力,即如何盡可能快的從緩沖區取走數據包
3. 針對UDP丟包問題,進行系統層面和程序層面調優
3.1 診斷
n 網卡緩沖區溢出診斷
在Linux操作系統中,可以通過netstat -i –udp <NIC> 命令來診斷網卡緩沖區是否溢出,RX-DRP列顯示網卡丟失的數據包個數。
For example: netstat -i –udp eth1
[root@TENCENT64 /usr/local/games/udpserver]# netstat -i –udp eth1
Kernel Interface table
Iface MTU Met RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg
eth1 1500 0 1295218256 0 3 0 7598497 0 0 0 BMRU
上圖的輸出顯示3個數據包被網卡丟掉
可以通過增大網卡緩沖區來有效減少網卡緩沖區溢出。
n 操作系統內核網絡緩沖區溢出診斷
在Linux操作系統中可以通過cat /proc/net/snmp | grep -w Udp命令來查看,InErrors 列顯示當操作系統UDP隊列溢出時丟失的UDP數據包總個數。
[root@TENCENT64 /usr/local/games/udpserver]# cat /proc/net/snmp | grep -w Udp
Udp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors
Udp: 859428331 12609927 166563611 151449 166563611 0
1) 增大操作系統內核網絡緩沖區的大小
2) 在數據包路徑圖中直接繞過操作系統內核緩沖區,通過使用用戶空間棧或一些可以 繞過內核緩沖區的中間件 (e.g. Solarflare's OpenOnload).
3) 關閉未使用的網絡相關的應用和服務使操作系統的負載降到最低
4) 系統中僅保留適當數量的工作的網卡,最大效率的合理化利用網卡和系統資源
n 應用程序socket緩沖區溢出診斷
在Linux操作系統中可以通過cat /proc/net/snmp | grep -w Udp命令來查看,RcvbufErrors 列顯示當應用程序socket緩沖區溢出時丟失的UDP數據包總個數。
[root@TENCENT64 /usr/local/games/udpserver]# cat /proc/net/snmp | grep -w Udp
Udp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors
Udp: 859428331 12609927 166563611 151449 166563611 0
有如下幾種方法可以有效減緩應用程序socket緩沖區溢出:
1) 接受緩沖區盡可能快地處理接受到的數據包(e.g.通過使用NIO的方式來異步非阻塞接受UDP數據包或者提高接受UDP數據包線程的優先級)
2) 增大應用程序接受socket緩沖區大小,注意這個受限於全局socket緩沖區大小,如果應用程序的socket緩沖區大於全局socket緩沖區將沒有效果。
3) 把應用程序或接受線程指定到CPU專用的核上
4) 提高應用程序的io優先級(e.g.使用nice或ionice命令來調節)
5) 關閉所有未使用的網絡相關的應用和服務使操作系統的負載降到最低
3.2 調優
n 網卡緩沖區調優
Linux下運行ethtool -g <NIC> 命令查詢網卡的緩沖設置,如下:
[root@TENCENT64 /usr/local/games/udpserver]# ethtool -g eth1
Ring parameters for eth1:
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
通過命令ethtool -G d<NIC> rx NEW-BUFFER-SIZE可以設置RX ring的緩沖區大小,改變會立即生效不需要重啟操作系統或刷新網絡棧,這種變化直接作用於網卡本身而不影響操作系統,不影響操作系統內核網絡棧但是會影響網卡固件參數。更大的ring size能承受較大的數據流量而不會丟包,但是因為工作集的增加可能會降低網卡效率,影響性能,所以建議謹慎設置網卡固件參數。
n 操作系統內核緩沖區調優
運行命令sysctl -A | grep net | grep 'mem\|backlog' | grep 'udp_mem\|rmem_max\|max_backlog'查看當前操作系統緩沖區的設置。如下:
[root@TENCENT64 /usr/local/games]# sysctl -A | grep net | grep 'mem\|backlog' | grep 'udp_mem\|rmem_max\|max_backlog'net.core.netdev_max_backlog = 1000net.core.rmem_max = 212992net.ipv4.udp_mem = 188169 250892 376338
增加最大socket接收緩沖區大小為32MB:
sysctl -w net.core.rmem_max=33554432
增加最大可分配的緩沖區空間總量,數值以頁面為單位,每個頁面單位等於4096 bytes:
sysctl -w net.ipv4.udp_mem="262144 327680 393216"
增加接收數據包隊列大小:
sysctl -w net.core.netdev_max_backlog=2000
修改完成后,需要運行命令 sysctl –p 使之生效
n 應用程序調優
要減少數據包丟失,應用程序必須盡可能快從緩沖區取走數據,可以通過適當增大socket緩沖區和采用異步非阻塞的IO來快速從緩沖區取數據,測試采用JAVA NIO構建一個Asynchronous UDP server。
//建立 DatagramChannel dc = DatagramChannel.open(); dc.configureBlocking(false); //本地綁定端口 SocketAddress address = new InetSocketAddress(port); DatagramSocket ds = dc.socket(); ds.setReceiveBufferSize(1024 * 1024 * 32);//設置接收緩沖區大小為32M ds.bind(address); //注冊 Selector select = Selector.open(); dc.register(select, SelectionKey.OP_READ); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); System.out.println("Listening on port " + port); while (true) { int num = select.select(); if (num == 0) { continue; } //得到選擇鍵列表 Set Keys = select.selectedKeys(); Iterator it = Keys.iterator(); while (it.hasNext()) { SelectionKey k = (SelectionKey) it.next(); if ((k.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) { DatagramChannel cc = (DatagramChannel) k.channel(); //非阻塞 cc.configureBlocking(false);
4. 其他減少丟包策略
UDP發送端增加流量控制,控制每秒發送的數據包,盡量避免由於發送端發包速率過快,導致UDP接收端緩沖區很快被填滿從而出現溢出丟包。測試采用google提供的工具包guava。RateLimiter類來做流控,采用了一種令牌桶的流控算法,RateLimiter會按照一定的頻率往桶里扔令牌,線程拿到令牌才能執行,比如你希望自己的應用程序QPS不要超過1000,那么RateLimiter設置1000的速率后,就會每秒往桶里扔1000個令牌。
采用流控后每秒發指定數量的數據包,而且每秒都會出現波谷波峰,如果不做流控,UDP發送端會全力發包一直在波峰附近抖動,大流量會一直持續,隨着時間的增加,UDP發送端生產的速率肯定會超過UDP接收端消費的速率,丟包是遲早的。
5. 真實測試數據
n 機器類型
發送端和接收端均采用C1類型機器,配置如下:
|
Intel(R) Xeon(R) CPU X3440 @ 2.53GHz:8
|
8G
|
500G:7200RPM:1:SATA
|
NORAID
|
接收端網卡信息如下:
[root@TENCENT64 /usr/local/games]# ethtool eth1 Settings for eth1: Supported ports: [ TP ] Supported link modes: 10baseT/Half 10baseT/Full 100baseT/Half 100baseT/Full 1000baseT/Full Supports auto-negotiation: Yes Advertised link modes: 10baseT/Half 10baseT/Full 100baseT/Half 100baseT/Full 1000baseT/Full Advertised pause frame use: Symmetric Advertised auto-negotiation: Yes Speed: 1000Mb/s Duplex: Full Port: Twisted Pair PHYAD: 1 Transceiver: internal Auto-negotiation: on MDI-X: on Supports Wake-on: pumbg Wake-on: g Current message level: 0x00000007 (7) Link detected: yes[root@TENCENT64 /usr/local/games]# ethtool -g eth1Ring parameters for eth1:Pre-set maximums:RX: 4096RX Mini: 0RX Jumbo: 0TX: 4096Current hardware settings:RX: 256RX Mini: 0RX Jumbo: 0TX: 256
n 實際調優
接收端服務器調優后的參數如下:
[root@TENCENT64 /usr/local/games]# sysctl -A | grep net | grep 'mem\|backlog' | grep 'udp_mem\|rmem_max\|max_backlog'net.core.rmem_max = 67108864net.core.netdev_max_backlog = 20000net.ipv4.udp_mem = 754848 1006464 1509696
發送端是否做發送流量控制在測試場景中體現
n 測試場景
場景1:發送100w+數據包,每個數據包大小512byte,數據包都包含當前的時間戳,不限流,全速發送。發送5次,測試結果如下:
測試客戶端:
發100w個512字節的udp包,發100w數據包耗時4.625s,21wQPS

測試服務器端:
客戶端發5次包,每次發包100w(每個包512字節),第一次服務端接受90w丟約10w,第二次服務端接受100w不丟,第三次接受100w不丟,第四次接受97w丟3w,第五次接受100w不丟
服務端記錄日志:

服務端操作系統接收UDP記錄情況:(和日志記錄結果完全一致)

場景2:發送端增加流量控制,每秒4w數據包,每個數據包512byte,包含當前時間戳,發送時間持續2小時,測試結果如下:
1.Udpclient端,加入流量控制:
QPS:4W
datapacket:512byte,包含發送的時間戳
持續發送時長:2h
累計發包數: 287920000(2.8792億)
CPU平均消耗: 16% (8cpu)
內存平均消耗: 0.3%(8G)
2.Udpserver端:
Server端接受前網卡記錄的UDP 詳情:
[root@TENCENT64 ~]# cat /proc/net/snmp | grep -w UdpUdp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrorsUdp: 1156064488 753197150 918758960 1718431901 918758960 0
Server端接受完所有的udp數據包后網卡記錄的UDP詳情:
[root@TENCENT64 ~]# cat /proc/net/snmp | grep -w UdpUdp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrorsUdp: 1443984568 753197150 918758960 1718432045 918758960 0
前后變化分析:
InDatagrams: (1443984568-1156064488)= 287920080
InErrors:0 (記錄操作系統層面udp丟包,丟包可能是因為系統udp隊列滿了)
RcvbufErrors:0(記錄應用程序層面udp丟包),丟包可能是因為應用程序socket buffer滿了)
Server端日志情況:
總記錄日志文件:276個,總大小:138G
日志總數: 287920000 (和udpclient發送數據包總量一致,沒有丟包)
根據日志時間戳,簡單計算處理能力:
time cost:(1445410477654-1445403277874)/1000=7199.78s
process speed: 287920000/7199.78 = 3.999w/s
CPU消耗: 平均46% (8cpu),要不停異步寫日志,IO操作頻繁,消耗比較多cpu資源
內存消耗: 平均4.7%(8G)
場景3:發送端增加流量控制,每秒6w數據包,每個數據包512byte,包含當前時間戳,發送時間持續2小時,出現丟包,測試結果如下:
1.Udpclient端,加入流量控制:
QPS:6W
datapacket:512byte,包含發送的時間戳
持續發送時長:2h
累計發包數: 432000000 (4.32億)
CPU平均消耗: 70% (8cpu)
內存平均消耗: 0.3%(8G)
2.Udpserver端:
Server端接受前網卡記錄的UDP 詳情:
[root@TENCENT64 ~]# cat /proc/net/snmp | grep -w UdpUdp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrorsUdp: 2235178200 753197150 918960131 1720242603 918960131 0
Server端接受完所有的udp數據包后網卡記錄的UDP詳情:
[root@TENCENT64 ~]# cat /proc/net/snmp | grep -w UdpUdp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrorsUdp: 2667158153 753197150 918980378 1720242963 918980378 0
前后變化分析:
InDatagrams: (2667158153 -2235178200)= 431979953
InErrors: (918980378 -918960131)= 20247 (記錄操作系統層面udp丟包,丟包可能是因為系統udp隊列滿了)
RcvbufErrors: (918980378 -918960131)= 20247 (記錄應用程序層面udp丟包),丟包可能是因為應用程序socket buffer滿了)
Server端日志情況:
總記錄日志文件:413個,總大小:207G
日志總數: 431979753 (和網卡收到udp包總數一致,寫日志文件沒有丟包)
丟包情況:
Client端發送:432000000,
服務端網卡接受udp包總數:431979953,
日志記錄:431979953,
udp網卡接受丟包:20247,
丟包率:1/20000
由於測試服務器硬盤資源有限,只測試了2個小時,隨着發送和接受時間增長,丟包率可能會增大。
對比圖:不加流控和加流控(限流4w)發送100w個512byte數據包,每毫秒發送數據包雷達波型對比圖,雷達波型圖中,外圍波型值為發送數據包的毫秒值,雷達軸距為每毫秒發送的數據包數取值范圍。按順序,圖1為限流4w生成的圖,圖2為不限流生成的圖。從圖中可以看出限流時每秒都會出現波谷波峰,不會一直持續高流量發送,能適當緩解UDP接收端的壓力;不限流時數據在波峰附近波動,持續高流量發送,對UDP接收端有很多壓力,接收端如沒及時從緩沖區取走數據或消費能力低於發送端的生成能力,則很容易丟包。

----------------------------------------------------------------------------------------------
總結:UDP發包在不做流控的前提下,發送端很快到達一個相對穩定的波峰值並一直持續發送,接收端網卡或操作系統緩沖區始終有限,隨着發包時間不斷增加,到某個時間點必定填滿接收端網卡和系統的緩沖區,而且發送端的生產速率將遠遠超過接收端消費速率,必然導致丟包。發送端做了流量控制后,發送速率得到有效控制,不會一直持續高流量發送,每秒都會出現波谷波峰,有效緩解了接收端的壓力,在合理發包速率的前提下,通過相關系統調優,基本可以保證不丟包,但要確保數據的高完整性,由於UDP協議的天生不可靠性,還是要在UDP協議基礎上做相關擴展,增加數據完整性校驗,方能確保業務數據的完整。
【注】文章第2和第3部分翻譯國外一篇文章,原文如下:
http://ref.onixs.biz/lost-multicast-packets-troubleshooting.html
