版權聲明:本文由黃日成 原創文章,轉載請注明出處:
文章原文鏈接:https://www.qcloud.com/community/article/848077001486437077
來源:騰雲閣 https://www.qcloud.com/community
作者介紹:黃日成,手Q游戲中心后台開發,騰訊高級工程師。從事C++服務后台開發4年多,主要負責手Q游戲中心后台基礎系統、復雜業務系統開發,主導過手Q游戲公會、企鵝電競App-對戰系統等項目的后台系統設計,有豐富的后台架構經驗。
引言
作為文章”《從TCP三次握手說起—淺析TCP協議中的疑難雜症》”的姊妹篇,很早就計划寫篇關於UDP的文章,盡管UDP協議遠沒TCP協議那么龐大、復雜,但是,要想將UDP描述清楚,用好UDP卻要比TCP難不少,於是文章從下筆寫,到最終寫成,斷斷續續拖了好幾個月。
對應系列的上一篇:
3. UDP疑難雜症
3.1 UDP的傳輸方式:面向報文
面向報文的傳輸方式決定了UDP的數據發送方式是一份一份的,也就是應用層交給UDP多長的報文,UDP就照樣發送,即一次發送一個報文。那么UDP的報文大小由哪些影響因素呢?UDP數據包的理論長度是多少,合適的UDP數據包應該是多少呢?
(1)UDP報文大小的影響因素,主要有以下3個
[1] UDP協議本身,UDP協議中有16位的UDP報文長度,那么UDP報文長度不能超過2^16=65536.
[2] 以太網(Ethernet)數據幀的長度,數據鏈路層的MTU(最大傳輸單元)。
[3] socket的UDP發送緩存區大小
(2) UDP數據包最大長度
根據UDP協議,從UDP數據包的包頭可以看出,UDP的最大包長度是2^16-1的個字節。由於UDP包頭占8個字節,而在IP層進行封裝后的IP包頭占去20字節,所以這個是UDP數據包的最大理論長度是2^16 - 1 - 8 - 20 = 65507字節。如果發送的數據包超過65507字節,send或sendto函數會錯誤碼1(Operation not permitted, Message too long),當然啦,一個數據包能否發送65507字節,還和UDP發送緩沖區大小(linux下UDP發送緩沖區大小為:cat /proc/sys/net/core/wmem_default)相關,如果發送緩沖區小於65507字節,在發送一個數據包為65507字節的時候,send或sendto函數會錯誤碼1(Operation not permitted, No buffer space available)。
(3) UDP數據包理想長度
理論上UDP報文最大長度是65507字節,實際上發送這么大的數據包效果最好嗎?我們知道UDP是不可靠的傳輸協議,為了減少UDP包丟失的風險,我們最好能控制UDP包在下層協議的傳輸過程中不要被切割。相信大家都知道MTU這個概念。 MTU最大傳輸單元,這個最大傳輸單元實際上和鏈路層協議有着密切的關系,EthernetII幀的結構DMAC+SMAC+Type+Data+CRC由於以太網傳輸電氣方面的限制,每個以太網幀都有最小的大小64字節,最大不能超過1518字節,對於小於或者大於這個限制的以太網幀我們都可以視之為錯誤的數據幀,一般的以太網轉發設備會丟棄這些數據幀。由於以太網EthernetII最大的數據幀是1518字節,除去以太網幀的幀頭(DMAC目的MAC地址48bit=6Bytes+SMAC源MAC地址48bit=6Bytes+Type域2bytes)14Bytes和幀尾CRC校驗部分4Bytes那么剩下承載上層協議的地方也就是Data域最大就只能有1500字節這個值我們就把它稱之為MTU。
在下層數據鏈路層最大傳輸單元是1500字節的情況下,要想IP層不分包,那么UDP數據包的最大大小應該是1500字節 – IP頭(20字節) – UDP頭(8字節) = 1472字節。不過鑒於Internet上的標准MTU值為576字節,所以建議在進行Internet的UDP編程時,最好將UDP的數據長度控制在 (576-8-20)548字節以內。
3.2 UDP數據包的發送和接收問題
(1) UDP的通信有界性
在阻塞模式下,UDP的通信是以數據包作為界限的,即使server端的緩沖區再大也要按照client發包的次數來多次接收數據包,server只能一次一次的接收,client發送多少次,server就需接收多少次,即客戶端分幾次發送過來,服務端就必須按幾次接收。
(2) UDP數據包的無序性和非可靠性
client依次發送1、2、3三個UDP數據包,server端先后調用3次接收函數,可能會依次收到3、2、1次序的數據包,收包可能是1、2、3的任意排列組合,也可能丟失一個或多個數據包。
(3) UDP數據包的接收
client發送兩次UDP數據,第一次 500字節,第二次300字節,server端阻塞模式下接包,第一次recvfrom( 1000 ),收到是 1000,還是500,還是300,還是其他?
由於UDP通信的有界性,接收到只能是500或300,又由於UDP的無序性和非可靠性,接收到可能是300,也可能是500,也可能一直阻塞在recvfrom調用上,直到超時返回(也就是什么也收不到)。
在假定數據包是不丟失並且是按照發送順序按序到達的情況下,server端阻塞模式下接包,先后三次調用:recvfrom( 200),recvfrom( 1000),recvfrom( 1000),接收情況如何呢?
由於UDP通信的有界性,第一次recvfrom( 200)將接收第一個500字節的數據包,但是因為用戶空間buf只有200字節,於是只會返回前面200字節,剩下300字節將丟棄。第二次recvfrom( 1000)將返回300字節,第三次recvfrom( 1000)將會阻塞。
(4) UDP包分片問題
如果MTU是1500,Client發送一個8000字節大小的UDP包,那么Server端阻塞模式下接包,在不丟包的情況下,recvfrom(9000)是收到1500,還是8000。如果某個IP分片丟失了,recvfrom(9000),又返回什么呢?
根據UDP通信的有界性,在buf足夠大的情況下,接收到的一定是一個完整的數據包,UDP數據在下層的分片和組片問題由IP層來處理,提交到UDP傳輸層一定是一個完整的UDP包,那么recvfrom(9000)將返回8000。如果某個IP分片丟失,udp里有個CRC檢驗,如果包不完整就會丟棄,也不會通知是否接收成功,所以UDP是不可靠的傳輸協議,那么recvfrom(9000)將阻塞。
3.3 UDP丟包問題
在不考慮UDP下層IP層的分片丟失,CRC檢驗包不完整的情況下,造成UDP丟包的因素有哪些呢?
[1] UDP socket緩沖區滿造成的UDP丟包
通過 cat /proc/sys/net/core/rmem_default 和cat /proc/sys/net/core/rmem_max可以查看socket緩沖區的缺省值和最大值。如果socket緩沖區滿了,應用程序沒來得及處理在緩沖區中的UDP包,那么后續來的UDP包會被內核丟棄,造成丟包。在socket緩沖區滿造成丟包的情況下,可以通過增大緩沖區的方法來緩解UDP丟包問題。但是,如果服務已經過載了,簡單的增大緩沖區並不能解決問題,反而會造成滾雪球效應,造成請求全部超時,服務不可用。
[2] UDP socket緩沖區過小造成的UDP丟包
如果Client發送的UDP報文很大,而socket緩沖區過小無法容下該UDP報文,那么該報文就會丟失。
[3] ARP緩存過期導致UDP丟包
ARP的緩存時間約10分鍾,APR緩存列表沒有對方的MAC地址或緩存過期的時候,會發送ARP請求獲取MAC地址,在沒有獲取到MAC地址之前,用戶發送出去的UDP數據包會被內核緩存到arp_queue這個隊列中,默認最多緩存3個包,多余的UDP包會被丟棄。被丟棄的UDP包可以從/proc/net/stat/arp_cache的最后一列的unresolved_discards看到。當然我們可以通過echo 30 > /proc/sys/net/ipv4/neigh/eth1/unres_qlen來增大可以緩存的UDP包。
UDP的丟包信息可以從cat /proc/net/udp 的最后一列drops中得到,而倒數第四列inode是丟失UDP數據包的socket的全局唯一的虛擬i節點號,可以通過這個inode號結合lsof(lsof -P -n | grep 25445445)來查到具體的進程。
3.4 UDP冗余傳輸
在外網通信鏈路不穩定的情況下,有什么辦法可以降低UDP的丟包率呢?一個簡單的辦法來采用冗余傳輸的方式。如下圖,一般采用較多的是延時雙發,雙發指的是將原本單發的前后連續的兩個包合並成一個大包發送,這樣發送的數據量是原來的兩倍。這種方式提高丟包率的原理比較簡單,例如本例的冗余發包方式,在偶數包全丟的情況下,依然能夠還原出完整的數據,也就是在這種情況下,50%的丟包率,依然能夠達到100%的數據接收。
4 UDP真的比TCP要高效嗎
相信很多同學都認為UDP無連接,無需重傳和處理確認,UDP比較高效。然而UDP在大多情況下並不一定比TCP高效,TCP發展至今天,為了適應各種復雜的網絡環境,其算法已經非常豐富,協議本身經過了很多優化,如果能夠合理配置TCP的各種參數選項,那么在多數的網絡環境下TCP是要比UDP更高效的。
4.1 影響UDP高效因素
(1) 無法智能利用空閑帶寬導致資源利用率低
一個簡單的事實是UDP並不會受到MTU的影響,MTU只會影響下層的IP分片,對此UDP一無所知。在極端情況下,UDP每次都是發小包,包是MTU的幾百分之一,這樣就造成UDP包的有效數據占比較小(UDP頭的封裝成本);或者,UDP每次都是發巨大的UDP包,包大小MTU的幾百倍,這樣會造成下層IP層的大量分片,大量分片的情況下,其中某個分片丟失了,就會導致整個UDP包的無效。由於網絡情況是動態變化的,UDP無法根據變化進行調整,發包過大或過小,從而導致帶寬利用率低下,有效吞吐量較低。而TCP有一套智能算法,當發現數據必須積攢的時候,就說明此時不積攢也不行,TCP的復雜算法會在延遲和吞吐量之間達到一個很好的平衡。
(2) 無法動態調整發包
由於UDP沒有確認機制,沒有流量控制和擁塞控制,這樣在網絡出現擁塞或通信兩端處理能力不匹配的時候,UDP並不會進行調整發送速率,從而導致大量丟包。在丟包的時候,不合理的簡單重傳策略會導致重傳風暴,進一步加劇網絡的擁塞,從而導致丟包率雪上加霜。更加嚴重的是,UDP的無秩序性和自私性,一個瘋狂的UDP程序可能會導致這個網絡的擁塞,擠壓其他程序的流量帶寬,導致所有業務質量都下降。
(3) 改進UDP的成本較高
可能有同學想到針對UDP的一些缺點,在用戶態做些調整改進,添加上簡單的重傳和動態發包大小優化。然而,這樣的改進並比簡單的,UDP編程可是比TCP要難不少的,考慮到改造成本,為什么不直接用TCP呢?當然可以拿開源的一些實現來抄一下(例如:libjingle),或者擁抱一下Google的QUIC協議,然而,這些都需要不少成本的。
上面說了這么多,難道真的不該用UDP了嗎?其實也不是的,在某些場景下,我們還是必須UDP才行的。那么UDP的較為合適的使用場景是哪些呢?
5 UDP的使用場合
5.1 通信實時性和持續性
在分組交換通信當中,協議棧的成本主要表現在以下兩方面:
[1] 封裝帶來的空間復雜度[2] 緩存帶來的時間復雜度
以上兩者是對立影響的,如果想減少封裝消耗,那么就必須緩存用戶數據到一定量在一次性封裝發送出去,這樣每個協議包的有效載荷將達到最大化,這無疑是節省了帶寬空間,帶寬利用率較高,但是延時增大了。如果想降低延時,那么就需要將用戶數據立馬封裝發出去,這樣顯然會造成消耗更多的協議頭等消耗,浪費帶寬空間。
因此,我們進行協議選擇的時候,需要重點考慮一下空間復雜度和時間復雜度間的平衡。通信的持續性對兩者的影響比較大,根據通信的持續性有兩種通信類型:[1] 短連接通信 [2] 長連接通信。對於短連接通信,一方面如果業務只需要發一兩個包並且對丟包有一定的容忍度,同時業務自己有簡單的輪詢或重復機制,那么采用UDP會較為好些。在這樣的場景下,如果用TCP,僅僅握手就需要3個包,這樣顯然有點不划算,一個典型的例子是DNS查詢。另一方面,如果業務實時性要求非常高,並且不能忍受重傳,那么首先就是UDP了或者只能用UDP了,例如NTP 協議,重傳NTP消息純屬添亂(為什么呢?重傳一個過期的時間包過來,還不如發一個新的UDP包同步新的時間過來)。如果NTP協議采用TCP,撇開握手消耗較多數據包交互的問題,由於TCP受Nagel算法等影響,用戶數據會在一定情況下會被內核緩存延后發送出去,這樣時間同步就會出現比較大的偏差,協議將不可用。
5.2 多點通信
對於一些多點通信的場景,如果采用有連接的TCP,那么就需要和多個通信節點建立其雙向連接,然后有時在NAT環境下,兩個通信節點建立其直接的TCP連接不是一個容易的事情,在涉及NAT穿越的時候,UDP協議的無連接性使得穿透成功率更高(原因詳見:由於UDP的無連接性,那么其完全可以向一個組播地址發送數據或者輪轉地向多個目的地持續發送相同的數據,從而更為容易實現多點通信。)
一個典型的場景是多人實時音視頻通信,這種場景下實時性要求比較高,可以容忍一定的丟包率。比如:對於音頻,對端連續發送p1、p2、p3三個包,另一端收到了p1和p3,在沒收到p2的保持p1的最后一個音(也是為什么有時候網絡丟包就會聽到嗞嗞嗞嗞嗞嗞…或者卟卟卟卟卟卟卟卟…重音的原因),等到到p3就接着播p3了,不需要也不能補幀,一補就越來越大的延時。對於這樣的場景就比較合適用UDP了,如果采用TCP,那么在出現丟包的時候,就可能會出現比較大的延時。
5.3 UDP使用原則
通常情況下,UDP的使用范圍是較小的,在以下的場景下,使用UDP才是明智的
[1] 實時性要求很高,並且幾乎不能容忍重傳
例子:NTP協議,實時音視頻通信,多人動作類游戲中人物動作、位置
[2] TCP實在不方便實現多點傳輸的情況
[3] 需要進行NAT穿越
[4] 對網絡狀態很熟悉,確保udp網絡中沒有氓流行為,瘋狂搶帶寬
[5] 熟悉UDP編程
參考資料
http://blog.csdn.net/dog250/article/details/6896949