天天看點

從生産環境遇到的問題聊聊TCP設計思路

當我們在學校學習網絡和網絡傳輸層時,我們可能總是感到枯草乏味、枯燥難懂。但事實上,在生産環境中,這是一個非常常見的問題,如果你不明白,可能會更困惑。

生産環境遇到的問題

談談我今年遇到的TCP層的幾個問題。

  • 問題1:長短連接配接的選擇?
  • 問題2:連接配接逾時了,為什麼逾時的時間是128s左右
  • 問題3:系統不可達,80端口連不通了,可是本地檢視80端口是正常的,這是為什麼?
  • 問題4:用戶端連接配接池很多處于CLOSE-WAIT?

傳輸層

要充分解釋這些問題,我們需要對傳輸層的協定有非常深入的了解。網絡層可能離軟體開發人員有點遠,但傳輸層,特别是廣泛使用的TCP,與我們的工作密切相關。

傳輸層的目的

  • 從上到下依次包括 應用層、傳輸層、網絡層、鍊路層、實體層。
  • 應用層就是對應不同的資料
  • 通過網絡層,包已經能夠正确的被路由到對應的主機
從生産環境遇到的問題聊聊TCP設計思路

我們需要有機制将不同的應用程式映射到同一主機的網絡層,并確定資料的準确到達。

差別不同的應用-UDP

從生産環境遇到的問題聊聊TCP設計思路
  • 每個應用程式對應一個端口,端口資訊也封裝在包中,以确定資料包屬于哪個應用的。
  • UDP的封包格式為傳輸層的簡單協定,也很簡單。
從生産環境遇到的問題聊聊TCP設計思路

保證傳輸的品質-TCP

UDP不保證資料的準确傳輸和品質保證。如果包丢失,網絡層不會重新傳輸。是以,傳輸層還設計了一種新的協定TCP。除了端口映射外,還在應用層和網絡層之間增加了一些處理機制,以確定傳輸品質。

傳輸的品質對應用而言實際上就2個方面:

  • 收到了對端發出的所有資料。
  • 對端發出的所有資料都按順序收到。
從生産環境遇到的問題聊聊TCP設計思路
  • C1.C2表示兩個用戶端與app1有資料互動。
  • 假設app1向C2發送資料,app1的傳輸層應確定所有資料都發送到C2,以確定沒有丢包事件。
    • 不丢包不是真的不丢包,而是如果丢包還能重傳,保證資料最終收到。
  • 假設C1向app1發送資料,則需要按順序正确接收發送的資料。

核心:Tcp的具體運作機制

TCP要幹什麼

依據以上的描叙,TCP主要的要做2件事:

  • 防止丢包

      a.丢包重傳

        一般情況下,收到包後會向發送者發送ack信号。

        逾時未收到會使用重傳機制。

      b. 減少丢包

        告訴我你的視窗:感受對端的處理能力。

        丢包原因及優化:感受網絡擁塞,控制傳輸速率。

  • 保證包到達的順序。

        使用序列号:確定資料包按正确順序傳遞。

基本試探-建立連接配接

正如上面提到的,TCP首先要試探兩個對端的收發能力,試探過程如下:

從生産環境遇到的問題聊聊TCP設計思路

主要是試探并确認了:

  • 确認A發送資料的能力
  • 确認B接收資料的能力
  • 确認B發送資料的能力
  • 确認A接收資料的能力

這個試探過程也叫做三次握手,通過三次握手兩個應用程式之間建立了一條TCP連接配接。

具體的過程和狀态

TCP連接配接是核心抽象給應用程式使用的。

我們可以更具體地看看這個過程,包括建立連接配接之前、中間和之後。

三次握手的狀态随時間變遷圖如下:

從生産環境遇到的問題聊聊TCP設計思路

三次握手就是三次發包的過程:

  • 1、發起端發送SYNC包:SYN,seq=x,自己進入Sync-Sent狀态

    注意:seq=x,下面還會有較長的描述

  • 2、監聽端收到SYNC包,發送SYN,ACK,seq=y,ack=x+1,進入Sync-RCVD狀态
  • 3、發起端收到SYNC,ACK包,發送           ACK,seq=x+1,ack=y+1,進入Established狀态

    a、RTT表示發送資料到收到ACK的時間

  • 4、監聽端收到ACK包,進入Established狀态

accept和LISTEN

伺服器的核心使用accept系統調用來幫助建立連接配接。伺服器調用accep函數後,處于LISTEN狀态。可以等待用戶端建立連接配接,并使用netstat檢視LISTEN狀态的連接配接。

# netstat -alpnt
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:11110           0.0.0.0:*        
  • *0.0.0.0:**表示接受網絡上所有端口的連接配接。
  • 核心使用socket作為真正的tcp連接配接對象,但accept的socket是特殊的,沒有建立tcp連接配接。

ESTABLISHED

三次握手成功後,這個連接配接将儲存在記憶體中。

# netstat -alpnt
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 9.13.73.14:38604      9.13.39.104:3306       ESTABLISHED 26100/java          
tcp        0      0 9.13.73.14:32926      9.21.210.17:8080       ESTABLISHED 26100/java          
  • ESTABLISHED:已建立連接配接。
  • 連接配接的資訊包括本地IP端口位址和遠端IP端口。

    a.本地9.13.73.14:38604與遠端9.13.39.104:3306建立連接配接。

    b.本地9.13.73.14:32926與遠端9.21.210.17:8080建立連接配接。

對于問題3的回答

有一次,我們的服務無法到達,即80端口無法連接配接,但登入機器并發送80端口仍然是正常的清單狀态,但我們發現許多連接配接處于Sync-RCVD狀态。檢視日志,我們發現系統已經運作,完全有理由懷疑服務建立連接配接的過程是由記憶體引起的。

是以,盡管80端口的LISTEN狀态正常,但外部無法正常連接配接。

TCP封包格式和資訊交換

三次握手中發的資料報都是TCP封包,TCP封包格式如下:

從生産環境遇到的問題聊聊TCP設計思路

從這個封包格式和上面三次握手的過程可以看出:

  • SYN标記位為1說明這個封包是一個SYN類型的包,用于握手
  1. 發起端發送SYNC包:SYN,seq=x
  2. 監聽端收到SYNC包後,也發送自己的SYN包:SYN,seq=y
  3. 發起端和監聽端的起始序号x和y是32位序号,它們是系統随機生成的
  4. 在SYN封包中交換了初始序列号之後,這個序列号就一直單調遞增
  5. 初始序列号ISN還用于關閉連接配接的嗎?
  • ACK标記位為1說明這個封包是一個ACK類型的包
  1. 32位确認号
  2. 監聽端收到SYNC包,還發送ACK,ack=x+1,小于x+1的全部位元組已經收到,**期待下一次收到seq=x+1的包
  3. 發起端收到SYNC,發送ACK,ack=y+1,小于y+1的全部位元組已經收到,期待下一次收到seq=y+1的包**
  • 用于SYN和ACK連接配接的包的資料為空

另外封包格式中還包含下面資訊:

  1. 接收到的SYN包的視窗大小代表對方的接收視窗的大小
  2. RST标記位為1: 可以用于強制斷開連接配接
  3. PSH标記位為1:告知對方這些資料包收到後應該馬上交給上層的應用
  4. 選項中的MSS:TCP允許的從對方接收的最大封包段。

連接配接建立後,傳輸資料

連接配接建立後,資料可以傳輸。

回顧上述TCP主要保證的兩件事和基本思路:

從生産環境遇到的問題聊聊TCP設計思路
  • 使用序列号seq確定資料包按正确的順序傳遞。
  • ack信号和逾時重傳機制防止丢包。
  • 用窗戶減少丢包。

建立連接配接的過程為這件事情做好了鋪墊。讓我們來看看具體的運作機制。

逾時重傳和ACK深層含義

  • 發送出去的資料如果一直沒收到ack就重傳,需要确定多久時間沒收到就重傳
從生産環境遇到的問題聊聊TCP設計思路
  • 接收端如果多次收到同一個seq的資料就丢棄
  • 需要大于RTT,又不能太大
  • RTT是在動态變化的,核心采樣,使用RTO來計算

每當遇到一次逾時重傳時,都會将下一次逾時間隔将設定為以前值的兩倍。多次逾時,表明網絡環境差,不宜頻繁重複發送,就會每次将成為原來的兩倍,可設定最大重傳次數。

現在就可以回答問題2了

初始1S逾時,然後因為系統設定的重傳次數是6,重傳了6次,1+2+4+8++16+32+64加起來大約是128s左右。

可以批量送出ack

從生産環境遇到的問題聊聊TCP設計思路

seq=4的ack沒有正确的收到, 但如果在逾時時間内收到了ack=6 則表示6之前的seq都已經正确接收到了 seq=4的資料也不會重傳

滑動的視窗和右移的指針

發送端的socket緩沖區,示意圖如下:

從生産環境遇到的問題聊聊TCP設計思路
  • 已發送并收到ACK确認的資料
  • 已發送但未收到ACK确認的資料
  • 未發送但總大小在接收方處理範圍内
  • 未發送但總大小超過接收方處理範圍

視窗右移

  • 圖示的發送視窗和收到ack封包中的window大小相關
  • ack指針右移則可用視窗變大
  • 發送seq指針右移則可用視窗變小

接收端的socket緩沖區,示意圖如下:

從生産環境遇到的問題聊聊TCP設計思路
  • ACK包,ack=16,且(window=16)
  • 已成功接收并确認的資料
  1. ack指針右移則可用視窗還是右移
  • 接收視窗是未收到資料但可以接收的資料
  1. 可用視窗和應用程式擷取資料的能力有關,如果應用程式一直不從socket緩沖區擷取資料,則接收視窗也會變得越來越小
  2. 滑動視窗并不是一成不變的。當接收方的應用讀取資料的速度非常快的話,接收視窗可以很快的空缺出來。
  3. 通過使用視窗可以起到流控的作用,可以減少不必要的丢包,并減少網絡擁塞。

順序的保證

如下圖:

從生産環境遇到的問題聊聊TCP設計思路

假設接收端未收到16,17的封包, 兒後面18-27的資料都收到了, 則并不會發ack給發送方 當發送端逾時重傳16,17之後,且接收端收到之後 則接收端傳回ack=28的封包給發送端。

  • ack指針隻能右移,不能往回走
  • 這樣可以保證順序傳遞

傳輸的總結

TCP的主要功能是防止包丢失,保證包到達的順序。本節通過描述TCP的具體工作機制證明了TCP确實達到了這一目的。

高并發系統和關閉連接配接的設計

最後,我完成了TCP連接配接建立和傳輸的基本原理和過程,認為我可以松一口氣。

關閉連接配接是TCP的一部分,但與TCP相比,這并不重要。

但最近發現,在生産活動中,大家也非常關注TCP四次揮手的過程。主要原因是系統并發量大。

TCP連接配接是衡量系統并發量的重要因素,如果關閉連接配接異常,必然會影響系統的運作。

對于連接配接對并發量的影響,首先可以分析開頭提出的問題。

長連接配接 VS 短連結

  • 短連接配接一般隻會在 client/server間傳遞一次請求操作
  1. 這時候雙方任意都可以發起close操作
  2. 短連接配接管理起來比較簡單,存在的連接配接都是有用的連接配接,不需要額外的控制手段。
  3. 通常浏覽器通路伺服器的時候一般就是短連接配接。
  • 長連接配接
  1. Client與server完成一次讀寫之後,它們之間的連接配接并不會主動關閉,後續的讀寫操作會繼續使用這個連接配接。
  2. 是以一條連接配接保持幾天、幾個月、幾年或者更長時間都有可能,隻要不出現異常情況或由使用者(應用層)主動關閉。
  3. 長連接配接可以省去較多的TCP建立和關閉的操作,減少網絡阻塞的影響,
  4. 減少CPU及記憶體的使用,因為不需要經常的建立及關閉連接配接。
  5. 連接配接數過多時,影響服務端的性能和并發數量。

是以,長短連接配接怎麼選擇呢?

  • 是以對于并發量大,請求頻率低的,建議使用短連接配接。
  1. 對于服務端來說,長連接配接會耗費服務端的資源
  2. 如果有幾十萬,上百萬的連接配接,服務端的壓力會非常大,甚至會崩潰
  • 對于并發量小,性能要求高的,建議選擇長連接配接
  1. 比如mysql連接配接池

TCP關閉連接配接

長短連接配接的選擇如此重要,如果連接配接不能正确關閉,就會造成很大的麻煩。TCP設計了完善的關閉機制,關閉連接配接過程如下:

從生産環境遇到的問題聊聊TCP設計思路
  • 關閉連接配接發起方 發起第一個FIN,處于FIN-WAIT1
  • 關閉連接配接被動方的核心代碼回複ACK,此時還可以發送資料,處于Close-WAIT狀态
  1. 關閉連接配接被動方等待應用程式發送FIN,如果上層應用一直不發FIN,就還可以繼續發送資料
  • 關閉連接配接發起方收到ACK後處于FIN-WAIT2狀态,還可以接收資料自己不再發送資料
  • 關閉連接配接被動方直到應用程式發出FIN,處于LAST-ACK狀态
  • 關閉連接配接發起方收到FIN,會發送ACK,自己會處于TIME-WAIT狀态,此時
  1. 若是關閉連接配接被動方收到ack,就close連接配接
  2. 若是是關閉連接配接被動方沒收到ack,則會重傳FIN

TIME-WAIT等多長時間

MSL是封包最大的生存時間。它是任何封包在網絡上存在的最長時間。超過這個時間,封包将被丢棄,即MSL的兩倍。TCP的TIME_WAIT狀态也稱為2MSL等待狀态。

從生産環境遇到的問題聊聊TCP設計思路

等待2MSL時間的主要目的是害怕對方沒有收到最後一個ACK包,是以對方會在逾時後重發第三次握手的FIN包。

從生産環境遇到的問題聊聊TCP設計思路

若之前互動異常,收到重傳的FIN最多使用2MSL,是以結論是等待2MSL的時間。

最後一個問題

有一次,同步資料的應用從一個伺服器并發同步大量資料,這個過程比較緩慢,是以考慮如何加快同步。

  • 首先檢視網絡連接配接,發現20個連接配接的連接配接池中有很多處于close_wait狀态的連接配接。

通過以上分析,我們知道Close-WAIT是對方可能認為連接配接空閑時間太長而關閉的連接配接,但我在這裡使用的連接配接池還沒有發送FIN釋放包。

可見看出,連接配接的數量并沒有成為系統的瓶頸,我們可以繼續增加并發線程的數量,以增加并發量。