Udp的反向代理:nginx


在實時性要求較高的特殊場景下,簡單的UDP協議仍然是我們的主要手段。UDP協議沒有重傳機制,還適用於同時向多台主機廣播,因此在諸如多人會議、實時競技游戲、DNS查詢等場景里很適用,視頻、音頻每一幀可以允許丟失但絕對不能重傳,網絡不好時用戶可以容忍黑一下或者聲音嘟一下,如果突然把幾秒前的視頻幀或者聲音重播一次就亂套了。使用UDP協議作為信息承載的傳輸層協議時,就要面臨反向代理如何選擇的挑戰。通常我們有數台企業內網的服務器向客戶端提供服務,此時需要在下游用戶前有一台反向代理服務器做UDP包的轉發、依據各服務器的實時狀態做負載均衡,而關於UDP反向代理服務器的使用介紹網上並不多見。本文將講述udp協議的會話機制原理,以及基於nginx如何配置udp協議的反向代理,包括如何維持住session、透傳客戶端ip到上游應用服務的3種方案等。

許多人眼中的udp協議是沒有反向代理、負載均衡這個概念的。畢竟,udp只是在IP包上加了個僅僅8個字節的包頭,這區區8個字節又如何能把session會話這個特性描述出來呢?

?

圖1 UDP報文的協議分層

在TCP/IP或者?OSI網絡七層模型中,每層的任務都是如此明確:

  • 物理層專注於提供物理的、機械的、電子的數據傳輸,但這是有可能出現差錯的;
  • 數據鏈路層在物理層的基礎上通過差錯的檢測、控制來提升傳輸質量,並可在局域網內使數據報文跨主機可達。這些功能是通過在報文的前后添加Frame頭尾部實現的,如上圖所示。每個局域網由於技術特性,都會設置報文的最大長度MTU(Maximum Transmission Unit),用netstat -i(linux)命令可以查看MTU的大小:
  • 而IP網絡層的目標是確保報文可以跨廣域網到達目的主機。由於廣域網由許多不同的局域網,而每個局域網的MTU不同,當網絡設備的IP層發現待發送的數據字節數超過MTU時,將會把數據拆成多個小於MTU的數據塊各自組成新的IP報文發送出去,而接收主機則根據IP報頭中的Flags和Fragment Offset這兩個字段將接收到的無序的多個IP報文,組合成一段有序的初始發送數據。IP報頭的格式如下圖所示:

圖2 IP報文頭部

IP協議頭(本文只談IPv4)里最關鍵的是Source IP Address發送方的源地址、Destination IP Address目標方的目的地址。這兩個地址保證一個報文可以由一台windows主機到達一台linux主機,但並不能決定一個chrome瀏覽的GET請求可以到達linux上的nginx。

4、傳輸層主要包括TCP協議和UDP協議。這一層最主要的任務是保證端口可達,因為端口可以歸屬到某個進程,當chrome的GET請求根據IP層的destination IP到達linux主機時,linux操作系統根據傳輸層頭部的destination port找到了正在listen或者recvfrom的nginx進程。所以傳輸層無論什么協議其頭部都必須有源端口和目的端口。例如下圖的UDP頭部:

圖3 UDP的頭部

TCP的報文頭比UDP復雜許多,因為TCP除了實現端口可達外,它還提供了可靠的數據鏈路,包括流控、有序重組、多路復用等高級功能。由於上文提到的IP層報文拆分與重組是在IP層實現的,而IP層是不可靠的所有數組效率低下,所以TCP層還定義了MSS(Maximum Segment Size)最大報文長度,這個MSS肯定小於鏈路中所有網絡的MTU,因此TCP優先在自己這一層拆成小報文避免的IP層的分包。而UDP協議報文頭部太簡單了,無法提供這樣的功能,所以基於UDP協議開發的程序需要開發人員自行把握不要把過大的數據一次發送。

對報文有所了解后,我們再來看看UDP協議的應用場景。相比TCP而言UDP報文頭不過8個字節,所以UDP協議的最大好處是傳輸成本低(包括協議棧的處理),也沒有TCP的擁塞、滑動窗口等導致數據延遲發送、接收的機制。但UDP報文不能保證一定送達到目的主機的目的端口,它沒有重傳機制。所以,應用UDP協議的程序一定是可以容忍報文丟失、不接受報文重傳的。如果某個程序在UDP之上包裝的應用層協議支持了重傳、亂序重組、多路復用等特性,那么他肯定是選錯傳輸層協議了,這些功能TCP都有,而且TCP還有更多的功能以保證網絡通訊質量。因此,通常實時聲音、視頻的傳輸使用UDP協議是非常合適的,我可以容忍正在看的視頻少了幾幀圖像,但不能容忍突然幾分鍾前的幾幀圖像突然插進來:-)

有了上面的知識儲備,我們可以來搞清楚UDP是如何維持會話連接的。對話就是會話,A可以對B說話,而B可以針對這句話的內容再回一句,這句可以到達A。如果能夠維持這種機制自然就有會話了。UDP可以嗎?當然可以。例如客戶端(請求發起者)首先監聽一個端口Lc,就像他的耳朵,而服務提供者也在主機上監聽一個端口Ls,用於接收客戶端的請求。客戶端任選一個源端口向服務器的Ls端口發送UDP報文,而服務提供者則通過任選一個源端口向客戶端的端口Lc發送響應端口,這樣會話是可以建立起來的。但是這種機制有哪些問題呢?

問題一定要結合場景來看。比如:1、如果客戶端是windows上的chrome瀏覽器,怎么能讓它監聽一個端口呢?端口是會沖突的,如果有其他進程占了這個端口,還能不工作了?2、如果開了多個chrome窗口,那個第1個窗口發的請求對應的響應被第2個窗口收到怎么辦?3、如果剛發完一個請求,進程掛了,新啟的窗口收到老的響應怎么辦?等等。可見這套方案並不適合消費者用戶的服務與服務器通訊,所以視頻會議等看來是不行。

有其他辦法么?有!如果客戶端使用的源端口,同樣用於接收服務器發送的響應,那么以上的問題就不存在了。像TCP協議就是如此,其connect方的隨機源端口將一直用於連接上的數據傳送,直到連接關閉。

這個方案對客戶端有以下要求:不要使用sendto這樣的方法,幾乎任何語言對UDP協議都提供有這樣的方法封裝。應當先用connect方法獲取到socket,再調用send方法把請求發出去。這樣做的原因是既可以在內核中保存有5元組(源ip、源port、目的ip、目的端口、UDP協議),以使得該源端口僅接收目的ip和端口發來的UDP報文,又可以反復使用send方法時比sendto每次都上傳遞目的ip和目的port兩個參數。

對服務器端有以下要求:不要使用recvfrom這樣的方法,因為該方法無法獲取到客戶端的發送源ip和源port,這樣就無法向客戶端發送響應了。應當使用recvmsg方法(有些編程語言例如python2就沒有該方法,但python3有)去接收請求,把獲取到的對端ip和port保存下來,而發送響應時可以仍然使用sendto方法。

?

接下來我們談談nginx如何做udp協議的反向代理。

Nginx的stream系列模塊核心就是在傳輸層上做反向代理,雖然TCP協議的應用場景更多,但UDP協議在Nginx的角度看來也與TCP協議大同小異,比如:nginx向upstream轉發請求時仍然是通過connect方法得到的fd句柄,接收upstream的響應時也是通過fd調用recv方法獲取消息;nginx接收客戶端的消息時則是通過上文提到過的recvmsg方法,同時把獲取到的客戶端源ip和源port保存下來。我們先看下recvmsg方法的定義:

相對於recvfrom方法,多了一個msghdr結構體,如下所示:

其中msg_name就是對端的源IP和源端口(指向sockaddr結構體)。以上是C庫的定義,其他高級語言類似方法會更簡單,例如python里的同名方法是這么定義的:

其中返回元組的第4個元素就是對端的ip和port。

以上是nginx在udp反向代理上的工作原理。實際配置則很簡單:

在listen配置中的udp選項告訴nginx這是udp反向代理。而proxy_timeout和proxy_responses則是維持住udp會話機制的主要參數。

UDP協議自身並沒有會話保持機制,nginx於是定義了一個非常簡單的維持機制:客戶端每發出一個UDP報文,通常期待接收回一個報文響應,當然也有可能不響應或者需要多個報文響應一個請求,此時proxy_responses可配為其他值。而proxy_timeout則規定了在最長的等待時間內沒有響應則斷開會話。

最后我們來談一談經過nginx反向代理后,upstream服務如何才能獲取到客戶端的地址?如下圖所示,nginx不同於IP轉發,它事實上建立了新的連接,所以正常情況下upstream無法獲取到客戶端的地址:

圖4 nginx反向代理掩蓋了客戶端的IP

上圖雖然是以TCP/HTTP舉例,但對UDP而言也一樣。而且,在HTTP協議中還可以通過X-Forwarded-For頭部傳遞客戶端IP,而TCP與UDP則不行。Proxy protocol本是一個好的解決方案,它通過在傳輸層header之上添加一層描述對端的ip和port來解決問題,例如:

但是,它要求upstream上的服務要支持解析proxy protocol,而這個協議還是有些小眾。最關鍵的是,目前nginx對proxy protocol的支持則僅止於tcp協議,並不支持udp協議,我們可以看下其代碼:

可見nginx目前並不支持udp協議的proxy protocol(筆者下的nginx版本為1.13.6)。

雖然proxy protocol是支持udp協議的。怎么辦呢?

可以用IP地址透傳的解決方案。如下圖所示:

圖5 nginx作為四層反向代理向upstream展示客戶端ip時的ip透傳方案

這里在nginx與upstream服務間做了一些hack的行為:

  • nginx向upstream發送包時,必須開啟root權限以修改ip包的源地址為client ip,以讓upstream上的進程可以直接看到客戶端的IP。
  • upstream上的路由表需要修改,因為upstream是在內網,它的網關是內網網關,並不知道把目的ip是client ip的包向哪里發。而且,它的源地址端口是upstream的,client也不會認的。所以,需要修改默認網關為nginx所在的機器。
  • nginx的機器上必須修改iptable以使得nginx進程處理目的ip是client的報文。

這套方案其實對TCP也是適用的。

除了上述方案外,還有個Direct Server Return方案,即upstream回包時nginx進程不再介入處理。這種DSR方案又分為兩種,第1種假定upstream的機器上沒有公網網卡,其解決方案圖示如下:

圖6 nginx做udp反向代理時的DSR方案(upstream無公網)

這套方案做了以下hack行為:

1、在nginx上同時綁定client的源ip和端口,因為upstream回包后將不再經過nginx進程了。同時,proxy_responses也需要設為0。

2、與第一種方案相同,修改upstream的默認網關為nginx所在機器(任何一台擁有公網的機器都行)。

3、在nginx的主機上修改iptables,托福口語使得nginx可以轉發upstream發回的響應,同時把源ip和端口由upstream的改為nginx的。例如:

DSR的另一套方案是假定upstream上有公網線路,這樣upstream的回包可以直接向client發送,如下圖所示:

圖6 nginx做udp反向代理時的DSR方案(upstream有公網)

這套DSR方案與上一套DSR方案的區別在於:由upstream服務所在主機上修改發送報文的源地址與源端口為nginx的ip和監聽端口,以使得client可以接收到報文。例如:

以上三套方案皆可以使用開源版的nginx向后端服務傳遞客戶端真實IP地址,但都需要nginx的worker進程跑在root權限下,這對運維並不友好。從協議層面,可以期待后續版本支持proxy protocol傳遞客戶端ip以解決此問題。在當下的諸多應用場景下,除非業務場景明確無誤的拒絕超時重傳機制,否則還是應當使用TCP協議,其完善的流量、擁塞控制都是我們必須擁有的能力,如果在UDP層上重新實現這套機制就得不償失了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM