1. 程式人生 > >用雙十一的故事串起碎片的網路協議(下)

用雙十一的故事串起碎片的網路協議(下)

本文來自網易雲社群

作者:劉超

上一節,我們封裝了一個長長的網路包,“大炮”準備完畢,開始傳送。

傳送的時候可以說是重重關隘,從手機到行動網路、網際網路,還要經過多個運營商才能到達資料中心,到了資料中心就進入第二個複雜的過程,從閘道器到VXLAN隧道,到負載均衡,到Controller層、組合服務層、基礎服務層,最終才下單入庫。今天,我們就來看這最後一段過程。

7.一座座城池一道道關,流控擁塞與重傳

網路包已經組合完畢,接下來我們來看,如何經過一道道城關,到達目標公網IP。

對於手機來講,預設的閘道器在PGW上。在行動網路裡面,從手機到SGW,到PGW是有一條隧道的。在這條隧道里面,會將上面的這個包作為隧道的乘客協議放在裡面,外面SGW和PGW在核心網機房的IP地址。網路包直到PGW(PGW是隧道的另一端)才將裡面的包解出來,轉發到外部網路。

所以,從手機發送出來的時候,網路包的結構為:

  • 源MAC:手機也即UE的MAC;

  • 目標MAC:閘道器PGW上面的隧道端點的MAC;

  • 源IP:UE的IP地址;

  • 目標IP:SLB的公網IP地址。

進入隧道之後,要封裝外層的網路地址,因而網路包的格式為:

  • 外層源MAC:E-NodeB的MAC;

  • 外層目標MAC:SGW的MAC;

  • 外層源IP:E-NodeB的IP;

  • 外層目標IP:SGW的IP;

  • 內層源MAC:手機也即UE的MAC;

  • 內層目標MAC:閘道器PGW上面的隧道端點的MAC;

  • 內層源IP:UE的IP地址;

  • 內層目標IP:SLB的公網IP地址。

當隧道在SGW的時候,切換了一個隧道,為從SGW到PGW的隧道,因而網路包的格式為:

  • 外層源MAC:SGW的MAC;

  • 外層目標MAC:PGW的MAC;

  • 外層源IP:SGW的IP;

  • 外層目標IP:PGW的IP;

  • 內層源MAC:手機也即UE的MAC;

  • 內層目標MAC:閘道器PGW上面的隧道端點的MAC;

  • 內層源IP:UE的IP地址;

  • 內層目標IP:SLB的公網IP地址。

在PGW的隧道端點將包解出來,轉發出去的時候,一般在PGW出外部網路的路由器上,會部署NAT服務,將手機的IP地址轉換為公網IP地址,當請求返回的時候,再NAT回來。

因而在PGW之後,相當於做了一次歐洲十國遊型的轉發,網路包的格式為:

  • 源MAC:PGW出口的MAC;

  • 目標MAC:NAT閘道器的MAC;

  • 源IP:UE的IP地址;

  • 目標IP:SLB的公網IP地址。

在NAT閘道器,相當於做了一次玄奘西遊型的轉發,網路包的格式變成:

  • 源MAC:NAT閘道器的MAC;

  • 目標MAC:A2路由器的MAC;

  • 源IP:UE的公網IP地址;

  • 目標IP:SLB的公網IP地址。

20180929092906f2187173-9267-4ab4-9c8e-dbe55abc590e.jpg

出了NAT閘道器,就從核心網到達了網際網路。在網路世界,每一個運營商的網路成為自治系統AS。每個自治系統都有邊界路由器,通過它和外面的世界建立聯絡。

對於雲平臺來講,它可以被稱為Multihomed AS,有多個連線連到其他的AS,但是大多拒絕幫其他的AS傳輸包。例如一些大公司的網路。對於運營商來說,它可以被稱為Transit AS,有多個連線連到其他的AS,並且可以幫助其他的AS傳輸包,比如主幹網。

如何從出口的運營商到達雲平臺的邊界路由器?在路由器之間需要通過BGP協議實現,BGP又分為兩類,eBGP和iBGP。自治系統間,邊界路由器之間使用eBGP廣播路由。內部網路也需要訪問其他的自治系統。

邊界路由器如何將BGP學習到的路由匯入到內部網路呢?通過執行iBGP,使內部的路由器能夠找到到達外網目的地最好的邊界路由器。

網站的SLB的公網IP地址早已經通過雲平臺的邊界路由器,讓全網都知道了。於是這個下單的網路包選擇了下一跳是A2,也即將A2的MAC地址放在目標MAC地址中。

到達A2之後,從路由表中找到下一跳是路由器C1,於是將目標MAC換成C1的MAC地址。到達C1之後,找到下一跳是C2,將目標MAC地址設定為C2的MAC。到達C2後,找到下一跳是雲平臺的邊界路由器,於是將目標MAC設定為邊界路由器的MAC地址。

你會發現,這一路,都是隻換MAC,不換目標IP地址。這就是所謂下一跳的概念。

在雲平臺的邊界路由器,會將下單的包轉發進來,經過核心交換,匯聚交換,到達外網閘道器節點上的SLB的公網IP地址。

我們可以看到,手機到SLB的公網IP,是一個端到端的連線,連線的過程傳送了很多包。所有這些包,無論是TCP三次握手,還是HTTPS的金鑰交換,都是要走如此複雜的過程到達SLB的,當然每個包走的路徑不一定一致。

當網路包走在這個複雜的道路上,很可能一不小心就丟了,怎麼辦?這就需要藉助TCP的機制重新發送。

既然TCP要對包進行重傳,就需要維護一個Sequence Number,看哪些包到了,哪些沒到,哪些需要重傳,傳輸的速度應該控制到多少,這就是TCP的滑動視窗協議。

20180929092933d9feaf92-b7d6-4cbb-8ae6-275782ba45d4.jpg

整個TCP的傳送,一開始會協商一個Sequence Number,從這個Sequence Number開始,每個包都有編號。滑動視窗將接收方的網路包分成四個部分:

  • 已經接收,已經ACK,已經交給應用層的包;

  • 已經接收,已經ACK,未傳送給應用層;

  • 已經接收,尚未傳送ACK;

  • 未接收,尚有空閒的快取區域。

對於TCP層來講,每一個包都有ACK。ACK需要從SLB回覆到手機端,將上面的那個過程反向來一遍,當然路徑不一定一致,可見ACK也不是那麼輕鬆的事情。

如果傳送方超過一定的時間沒有收到ACK,就會重新發送。只有TCP層ACK過的包,才會發給應用層,並且只會傳送一份,對於下單的場景,應用層是HTTP層。

你可能會問了,TCP老是重複傳送,會不會導致一個單下了兩遍?是否要求服務端實現冪?從TCP的機制來看,是不會的。只有收不到ACK的包才會重複發,發到接收端,在窗口裡面只儲存一份,所以在同一個TCP連線中,不用擔心重傳導致二次下單。

但是TCP連線會因為某種原因斷了,例如手機訊號不好,這個時候手機把所有的動作重新做一遍,建立一個新的TCP連線,在HTTP層呼叫兩次RESTful API。這個時候可能會導致兩遍下單的情況,因而RESTful API需要實現冪等。

當ACK過的包發給應用層之後,TCP層的快取就空了出來,這會導致上面圖中的大三角,也即接收方能夠容納的總快取,整體順時針滑動。小的三角形,也即接收方告知傳送方的視窗總大小,也即還沒有完全確認收到的快取大小,如果把這些填滿了,就不能再發了,因為沒確認收到,所以一個都不能扔。

8.從資料中心進閘道器,公網NAT成私網

包從手機端經歷千難萬險,終於到了SLB的公網IP所在的公網網口。由於匹配上了MAC地址和IP地址,因而將網路包收了進來。

2018092909295283824415-b8d7-42d1-a33a-cb411c2495a3.jpg

在虛擬閘道器節點的外網網口上,會有一個NAT規則,將公網IP地址轉換為VPC裡面的私網IP地址,這個私網IP地址就是SLB的HAProxy所在的虛擬機器的私網IP地址。

當然為了承載比較大的吞吐量,虛擬閘道器節點會有多個,物理網路會將流量分發到不同的虛擬閘道器節點。同樣HAProxy也會是一個大的叢集,虛擬閘道器會選擇某個負載均衡節點,將某個請求分發給它,負載均衡之後是Controller層,也是部署在虛擬機器裡面的。

當網路包裡面的目標IP變成私有IP地址地址之後,虛擬路由會查詢路由規則,將網路包從下方的私網網口發出來。這個時候包的格式為:

  • 源MAC:閘道器MAC;

  • 目標MAC:HAProxy虛擬機器的MAC;

  • 源IP:UE的公網IP;

  • 目標IP:HAProxy虛擬機器的私網IP。

9.進入隧道打標籤,RPC遠端呼叫下單

在虛擬路由節點上,也會有OVS,將網路包封裝在VXLAN隧道里面,VXLAN ID就是給你的租戶建立VPC的時候分配的。包的格式為:

  • 外層源MAC:閘道器物理機MAC;

  • 外層目標MAC:物理機A的MAC;

  • 外層源IP:閘道器物理機IP;

  • 外層目標IP:物理機A的IP;

  • 內層源MAC:閘道器MAC;

  • 內層目標MAC:HAProxy虛擬機器的MAC;

  • 內層源IP:UE的公網IP;

  • 內層目標IP:HAProxy虛擬機器的私網IP。

在物理機A上,OVS會將包從VXLAN隧道里面解出來,發給HAProxy所在的虛擬機器。HAProxy所在的虛擬機發現MAC地址匹配,目標IP地址匹配,就根據TCP埠,將包發給HAProxy程序,因為HAProxy是在監聽這個TCP埠的。因而HAProxy就是這個TCP連線的服務端,客戶端是手機。對於TCP的連線狀態,滑動視窗等,都是在HAProxy上維護的。

在這裡HAProxy是一個四層負載均衡,也即他只解析到TCP層,裡面的HTTP協議他不關心,就將請求轉發給後端的多個Controller層的一個。

HAProxy發出去的網路包就認為HAProxy是客戶端了,看不到手機端了。網路包格式如下:

  • 源MAC:HAProxy所在虛擬機器的MAC;

  • 目標MAC:Controller層所在虛擬機器的MAC;

  • 源IP:HAProxy所在虛擬機器的私網IP;

  • 目標IP:Controller層所在虛擬機器的私網IP。

當然這個包發出去之後,還是會被物理機上的OVS放入VXLAN隧道里面,網路包格式為:

  • 外層源MAC:物理機A的MAC;

  • 外層目標MAC:物理機B的MAC;

  • 外層源IP:物理機A的IP;

  • 外層目標IP:物理機B的IP;

  • 內層源MAC:HAProxy所在虛擬機器的MAC;

  • 內層目標MAC:Controller層所在虛擬機器的MAC;

  • 內層源IP:HAProxy所在虛擬機器的私網IP;

  • 內層目標IP:Controller層所在虛擬機器的私網IP。

在物理機B上,OVS會將包從VXLAN隧道里面解出來,發給Controller層所在的虛擬機器。Controller層所在的虛擬機發現MAC地址匹配,目標IP地址匹配,就根據TCP埠,將包發給Controller層的程序,因為他是在監聽這個TCP埠的。

在HAProxy和Controller層之間,維護一個TCP的連線。

Controller層收到包之後,他是關心HTTP裡面是什麼的,於是解開HTTP的包,發現是一個POST請求,內容是下單購買一個課程。

10.下單扣減庫存優惠券,資料入庫返回成功

下單是一個複雜的過程,因而往往在組合服務層會有一個專門管理下單的服務,Controller層會通過RPC呼叫這個組合服務層。

假設我們使用的是Dubbo,則Controller層需要讀取註冊中心,將下單服務的程序列表拿出來,選出一個來呼叫。

Dubbo中預設的RPC協議是Hessian2。Hessian2將下單的遠端呼叫序列化為二進位制進行傳輸。

Netty是一個非阻塞的基於事件的網路傳輸框架。Controller層和下單服務之間,使用了Netty的網路傳輸框架。有了Netty,就不用自己編寫複雜的非同步Socket程式了。Netty使用的方式,就是咱們講Socket程式設計的時候,一個專案組支撐多個專案(IO多路複用,從派人盯著到有事通知)這種方式。

Netty還是工作在Socket這一層的,傳送的網路包還是基於TCP的。在TCP的下層,還是需要封裝上IP頭和MAC頭。如果跨物理機通訊,還是需要封裝的外層的VXLAN隧道里面。當然底層的這些封裝,Netty都不感知,它只要做好它的非同步通訊即可。

在Netty的服務端,也即下單服務中,收到請求後,先用Hessian2的格式進行解壓縮。然後將請求分發到執行緒中進行處理,線上程中,會呼叫下單的業務邏輯。

下單的業務邏輯比較複雜,往往要呼叫基礎服務層裡面的庫存服務、優惠券服務等,將多個服務呼叫完畢,才算下單成功。下單服務呼叫庫存服務和優惠券服務,也是通過Dubbo的框架,通過註冊中心拿到庫存服務和優惠券服務的列表,然後選一個呼叫。

呼叫的時候,統一使用Hessian2進行序列化,使用Netty進行傳輸,底層如果跨物理機,仍然需要通過VXLAN的封裝和解封裝。

咱們以庫存為例子的時候,講述過冪等的介面實現的問題。因為如果扣減庫存,僅僅是誰呼叫誰減一。這樣存在的問題是,如果扣減庫存因為一次呼叫失敗,而多次呼叫,這裡指的不是TCP多次重試,而是應用層呼叫的多次重試,就會存在庫存扣減多次的情況。

這裡常用的方法是,使用樂觀鎖(Compare and Set,簡稱CAS)。CAS要考慮三個方面,當前的庫存數、預期原來的庫存數和版本,以及新的庫存數。在操作之前,查詢出原來的庫存數和版本,真正扣減庫存的時候,判斷如果當前庫存的值與預期原值和版本相匹配,則將庫存值更新為新值,否則不做任何操作。

這是一種基於狀態而非基於動作的設計,符合REST的架構設計原則。這樣的設計有利於高併發場景。當多個執行緒嘗試使用CAS同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。

最終,當下單更新到分散式資料庫中之後,整個下單過程才算真正告一段落。

好了,經過了十個過程,下單終於成功了,你是否對這個過程瞭如指掌了呢?如果發現對哪些細節比較模糊,可以回去看一下相應的章節,相信會有更加深入的理解。

網易雲物件儲存服務 NOS(Netease Object Storage)是高效能、高可用、高可靠的雲端儲存服務。點選可免費體驗