Udp的反向代理:nginx

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

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

?linux

圖1 UDP報文的協議分層nginx

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

  • 物理層專一於提供物理的、機械的、電子的數據傳輸,但這是有可能出現差錯的;
  • 數據鏈路層在物理層的基礎上經過差錯的檢測、控制來提高傳輸質量,並可在局域網內使數據報文跨主機可達。這些功能是經過在報文的先後添加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。windows

四、傳輸層主要包括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發送響應端口,這樣會話是能夠創建起來的。可是這種機制有哪些問題呢?

問題必定要結合場景來看。好比:一、若是客戶端是windows上的chrome瀏覽器,怎麼能讓它監聽一個端口呢?端口是會衝突的,若是有其餘進程佔了這個端口,還能不工做了?二、若是開了多個chrome窗口,那個第1個窗口發的請求對應的響應被第2個窗口收到怎麼辦?三、若是剛發完一個請求,進程掛了,新啓的窗口收到老的響應怎麼辦?等等。可見這套方案並不適合消費者用戶的服務與服務器通信,因此視頻會議等看來是不行。

有其餘辦法麼?有!若是客戶端使用的源端口,一樣用於接收服務器發送的響應,那麼以上的問題就不存在了。像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行爲:

一、在nginx上同時綁定client的源ip和端口,由於upstream回包後將再也不通過nginx進程了。同時,proxy_responses也須要設爲0。

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

三、在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層上從新實現這套機制就得不償失了。

相關文章
相關標籤/搜索