高併發架構的TCP知識介紹

作爲一個有追求的程序員,不能只知足增刪改查,咱們要對系統全方面無死角掌控。掌握了這些基本的網絡知識後,相信一方面平常排錯中會事半功倍,另外一方面平常架構中不得不考慮的高併發問題,理解了這些底層協議也是會如虎添翼。linux

本文不會單純給你們講講TCP三次握手、四次揮手就完事了。若是隻是哪樣的話,我直接貼幾個鏈接就完事了。我但願把實際工做中的不少點可以串起來說給你們。固然爲了文章完整,我依然會從 三次握手 起頭。nginx

再說TCP狀態變動過程

無論是三次握手、仍是四次揮手,他們都是完成了TCP不一樣狀態的切換。進而影響各類數據的傳輸狀況。下面從三次握手開始分析。程序員

本文圖片有部分來自網絡,如有侵權,告知即焚面試

三次握手

來看看三次握手的圖,估計你們看這圖都快看吐了,不過爲何每次面試、回憶的時候仍是想不起呢?我再來抄抄這鍋剩飯吧! 數組

tcp-1st

首先當服務端處於 listen 狀態的時候,咱們就能夠再客戶端發起監聽了,此時客戶端會處於 SYN_SENT 狀態。服務端收到這個消息會返回一個 SYN 而且同時 ACK 客戶端的請求,以後服務端便處於 SYN_RCVD 狀態。這個時候客戶端收到了服務端的 SYN&ACK,就會發送對服務端的 ACK,以後便處於 ESTABLISHED 狀態。服務端收到了對本身的 ACK 後也會處於 ESTABLISHED 狀態。bash

常常在面試中可能有人提問:爲何握手要3次,不是2次或者4次呢?服務器

首先說4次握手,其實爲了保證可靠性,這個握手次數能夠一直循環下去;可是這沒有一個終止就沒有意義了。因此3次,保證了各方消息有來有回就足夠了。固然這裏可能有一種狀況是,客戶端發送的 ACK 在網絡中被丟了。那怎麼辦?markdown

  1. 其實大部分時候,咱們鏈接創建完成就會馬上發送數據,因此若是服務端沒有收到 ACK 不要緊,當收到數據就會認爲鏈接已經創建;
  2. 若是鏈接創建後不立馬傳輸數據,那麼服務端認爲鏈接沒有創建成功會週期性重發 SYN&ACK 直到客戶端確認成功。

再說爲何2次握手不行呢?2次握手咱們能夠想象是沒有三次握手最後的 ACK, 在實際中確實會出現客戶端發送 ACK 服務端沒有收到的狀況(上面的狀況一),那麼這是否說明兩次握手也是可行的呢? 看下狀況二,2次握手當服務端發送消息後,就認爲創建成功,而恰巧此時又沒有數據傳輸。這就會帶來一種資源浪費的狀況。好比:客戶端可能因爲延時發送了多個鏈接狀況,當服務端每收到一個請求回覆後就認爲鏈接創建成功,可是這其中不少求情都是延時產生的重複鏈接,浪費了不少寶貴的資源。網絡

所以綜上所述,從資源節省、效率3次握手都是最合適的。話又回來三次握手的真實意義其實就是協商傳輸數據用的:序列號與窗口大小多線程

下面咱們經過抓包再來看一下真實的狀況是否如上所述。

20:33:26.583598 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [S], seq 621839080, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 1050275400 ecr 0,sackOK,eol], length 0
20:33:26.660754 IP 103.235.46.39.80 > 192.168.0.102.58165: Flags [S.], seq 1754967387, ack 621839081, win 8192, options [mss 1452,nop,wscale 5,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,sackOK,eol], length 0
20:33:26.660819 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [.], ack 1754967388, win 4096, length 0
複製代碼

抓包: sudo tcpdump -n host www.baidu.com -S

  • S 表示 SYN
  • . 表示 ACK
  • P 表示 傳輸數據
  • F 表示 FIN

四次揮手

揮手,就是說數據傳完了,同志們再見!

tcp-3th

這裏有個問題須要注意下,其實客戶端、服務端都可以主動發起關閉操做,誰調用 close() 就先發送關閉的請求。固然通常的流程,發起創建鏈接的一方會主動發起關閉請求(http中)。

關於4次揮手的過程,我就很少解釋了,這裏有兩個重要的狀態我須要解釋下,這都是我親自經歷過的線上故障,close_waittime_wait

先給你們一個命令,統計tcp的各類狀態狀況。下面表格內容就來自這個命令的統計。

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

Tcp狀態 鏈接數
CLOSE_WAIT 505
ESTABLISHED 808
TIME_WAIT 3481
SYN_SENT 1
SYN_RECV 1
LAST_ACK 2
FIN_WAIT2 2
FIN_WAIT1 1

大量的CLOSE_WAIT 這個在我以前的一篇文章 線上大量CLOSE_WAIT緣由分析 已經有過介紹,它會致使大量的socket沒法釋放。而每一個socket都是一個文件,是會佔用資源的。這個問題主要是代碼問題。它出如今被動關閉的一方(習慣稱爲server)。

大量的TIME_WAIT 這個問題在平常中常常看到,流量一高就出現大量的該狀況。該狀態出如今主動發起關閉的一方。該狀態通常等待的時間設爲 2MSL後自動關閉,MSL是Maximum Segment Lifetime,報文最大生存時間,若是報文超過這個時間,就會被丟棄。處於該狀態下的socket也是不能被回收使用的。線上我就遇到這種狀況,每次大流量的時候,每臺機器處於該狀態的socket就多達10w+,遠遠比處於 Established 狀態的socket多的多,致使不少時候服務響應能力降低。這個一方面能夠經過調整內核參數處理,另外一方面避免使用太多的短連接,能夠採用鏈接池來提高性能。另外在代碼層面多是因爲某些地方沒有關閉鏈接致使的,也須要檢查業務代碼。

上面兩個狀態必定要牢記發生在哪一方,這方便咱們快速定位問題。

最後這裏仍是放上揮手時的抓包數據:

20:33:26.750607 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [F.], seq 621839159, ack 1754967720, win 4096, length 0
20:33:26.827472 IP 103.235.46.39.80 > 192.168.0.102.58165: Flags [.], ack 621839160, win 776, length 0
20:33:26.827677 IP 103.235.46.39.80 > 192.168.0.102.58165: Flags [F.], seq 1754967720, ack 621839160, win 776, length 0
20:33:26.827729 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [.], ack 1754967721, win 4096, length 0
複製代碼

很少很多,恰好4次。

TCP狀態變動

網絡上有一張TCP狀態機的圖,我以爲太複雜了,用本身的方式搞個簡單點的容易理解的。我從兩個角度來講明狀態的變動。

  • 一個是客戶端
  • 一個是服務端

看下面兩張圖的時候,請必定結合上面三次握手、四次揮手的時序圖一塊兒看,加深理解。

客戶端狀態變動

tcp-4th

經過這張圖,你們是否可以清晰明瞭的知道 TCP 在客戶端上的變動狀況了呢?

服務端狀態變動

tcp-5th

這一張圖描述了 TCP 狀態在服務端的變遷。

TCP的流量控制與擁塞控制

咱們常說TCP是面向鏈接的,UDP是無鏈接的。那麼TCP這個面向鏈接主要解決的是什麼問題呢?

這裏繼續把三次握手的抓包數據貼出來分析下:

20:33:26.583598 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [S], seq 621839080, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 1050275400 ecr 0,sackOK,eol], length 0
20:33:26.660754 IP 103.235.46.39.80 > 192.168.0.102.58165: Flags [S.], seq 1754967387, ack 621839081, win 8192, options [mss 1452,nop,wscale 5,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,sackOK,eol], length 0
20:33:26.660819 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [.], ack 1754967388, win 4096, length 0
複製代碼

上面咱們說到 TCP 的三次握手最重要的就是協商傳輸數據用的序列號。那這個序列號究竟有些什麼用呢?這個序號可以幫助後續兩端進行確認數據包是否收到,解決順序、丟包問題;另外咱們還能夠看到有一個 win 字段,這是雙方交流的窗口大小,這在每次傳輸數據過程當中也會攜帶。主要是告訴對方,我窗口是這麼大,別發多了或者別發太少。

總結下,TCP的幾個特色是:

  • 順序問題,依靠序號
  • 丟包問題,依靠序號
  • 流量控制,依靠滑動窗口
  • 擁塞控制,依靠擁塞窗口+滑動窗口
  • 鏈接維護,三次握手/四次揮手

順序與丟包問題

這個問題其實應該很好理解。因爲數據在傳輸前咱們已經有序號了,這裏注意一下這個序號是隨機的,重複的機率極地,避免了程序發生亂入的可能性。

因爲咱們每一個數據包有序號,雖然發送與到達可能不是順序的,可是TCP層收到數據後,能夠根據序號進行從新排列;另外在這個排列過程當中,發現有了1,2,3,5,6這幾個包,一檢查就知道4要麼延時未到達,要麼丟包了,等待重傳。

這裏須要重要說明的一點是。爲了提高效率,TCP其實並非收到一個包就發一個ack。那是如何ACK的呢?仍是以上面爲例,TCP收到了1,2,3,5,6這幾個包,它可能會發送一個 ack ,seq=3 的確認包,這樣次一次確認了3個包。可是它不會發送 5,6 的ack。由於4沒有收到啊!一旦4延時到達或者重發到達,就會發送一個 ack, seq=6,又一次確認了3個包。

流量控制與擁塞控制

這兩個概念說實話,讓我理解了挺長時間,主要是對它們各自控制的內容以及相互之間是否有做用一直沒有鬧清楚。

先大概說下:

  • 流量控制:是根據接收方的窗口大小來感知我此次可以傳多少數據給對方;———— 滑動窗口
  • 擁塞控制:而擁塞控制主要是避免網絡擁塞,它考慮的問題更多。根據綜合因素來以爲發多少數據給對方;———— 滑動窗口&擁塞窗口

舉個例子說下,好比:A給B發送數據,經過握手後,A知道B一次能夠收1000的數據(B有這麼大的處理能力),那麼這個時候滑動窗口就能夠設置成1000。那是否是最後真的能夠一次發這麼多數據給B呢?還不是,這時候得問問擁塞窗口,老兄,如今網絡狀況怎麼樣?一次運1000的數據有壓力嗎?擁塞窗口一通計算說不行,如今是高峯期,最多隻能有600的貨上路。最終此次傳數據的時候就是 600 的標註。你們也能夠關注抓包數據的 win 值,一直在動態調整。

固然另一種狀況是滑動窗口比擁塞窗口小,雖然運輸能力強,可是接收能力有限,這時候就要取滑動窗口的值來實際發生。因此它們兩者之間是有關係的。

因此具體到每次可以發送多少數據,有這麼一個公式:

LastByteSend - LastByteAcked <= min{cwnd,rwnd}

  • LastByteSend 是最後一個發送的字節的序號
  • LastByteAcked 最後一個被確認的字節的序號

這兩個相減獲得的是本次可以發送的數據,這個數據必定小於或等於 cwnd 與 rwnd 中最小的一個值。相信你們可以理清楚。

那麼這部分知識對於實際工做中有什麼做用呢?指導意義就是:若是你的業務很重要、很核心必定不要混布;二是若是你的服務忽快忽慢,而確信依賴服務沒有問題,檢查下機器對應的網絡狀況;三是窗口這個速度控制機制,在咱們進行服務設計的時候,很是具備參考意義。是否是有點消息隊列的感受?(不少消息隊列都是勻速的,咱們是否能夠加一個窗口的概念來進行優化呢?)

是什麼限制了你的鏈接

到了最關鍵的地方了,精華我都是留到最後講。下面放一張網上找的socket操做步驟圖,畫的太好了我就直接用了。

tcp-6th

咱們假設個人服務端就是 Nginx ,我來嘗試解讀一下。當客戶端調用 connect() 時候就會發起三次握手,此次握手的時候有幾個元素惟一肯定了此次通訊(或者說這個socket),[源IP:源Port, 目的IP:目的Port] ,固然這個socket還不是最終用來傳輸數據的socket,一旦握手完成後,服務端會在返回一個 socket 專門用來後續的數據傳輸。這裏暫且把第一個socket叫 監聽socket,第二個叫 傳輸socket 方便後文敘述。

爲何要這麼設計呢?你們想想,若是監聽的socket還要負責數據的收發,請問這個服務端的效率如何提高?什麼東西、誰都往這個socket裏邊丟,太複雜!

提升鏈接經常使用套路

到了這一步,咱們如今先停下來算算本身的服務器機器可以有多少鏈接呢?這個極限又是如何一步步被突破呢?

先說 監聽socket ,服務器的prot通常都是固定的,服務器的ip固然也是固定的(單機)。那麼上面的結構 [源IP:源Port, 目的IP:目的Port] 其實只有客戶端的ip與端口能夠發生變化。假設客戶端用的是IPv4,那麼理論鏈接數是:2^32(ip數) * 2^16(端口數) = 2^48。

看起來這個值蠻大的。可是真的可以有這麼多鏈接嗎?不可能的,由於每個socket都須要消耗內存;以及每個進程的文件描述符是有上限的。這些都限制了最終的鏈接數。

那麼如何進行調和呢?我知道的操做有:多進程、多線程、IO多路服用、協程等手段組合使用。

多進程

也就是監聽是一個進程,一旦accept後,對於 傳輸socket 咱們就fork一個新的子進程來處理。可是這種方式過重,fork一個進程、銷燬一個進程都是特別費事的。單機對進程的建立上限也是有限制的。

多線程

線程比進程要輕量級的多,它會共享父進程的不少資源,好比:文件描述符、進程空間,它就是多了一個引用。所以它的建立、銷燬更加容易。每個 傳輸socket 在這裏就交給了線程來處理。

可是無論是多進程、仍是多線程都存在一個問題,一個鏈接對應一個進程或者協程。這都很難逃脫 C10K 的問題。那麼該怎麼辦呢?

IO多路複用

IO多路複用是什麼意思呢?在上面單純的多進程、多線程模型中,一個進程或線程只能處理一個鏈接。用了IO多路複用後,我一個進程或線程就能處理多個鏈接。

咱們都知道 Nginx 很是高效,它的結構是:master + worker,worker 會在 80、443端口上來監聽請求。它的worker通常設置爲 cpu 的cores數,那麼這麼少的子進程是如何解決超多鏈接的呢?這裏其實每一個worker就採用了 epoll 模型(固然IO多路複用還有個select,這裏就不說了)。

處於監聽狀態的worker,會把全部 監聽socket 加入到本身的epoll中。當這些socket都在epoll中時,若是某個socket有事件發生就會當即被回調喚醒(這涉及epoll的紅黑樹,講不清楚不細說了)。這種模式,大大增長了每一個進程能夠管理的socket數量,上限直接能夠上升到進程可以操做的最大文件描述符。

通常機器能夠設置百萬級別文件描述符,因此單機單進程就是百萬鏈接,epoll是解決C10K的利器,不少開源軟件用到了它。

這裏說下,並非全部的worker都是同時處於監聽端口的狀態,這涉及到nginx驚羣、搶自旋鎖的問題,再也不本文範圍內很少說。

關於ulimit

在文章的最後,補充一些單機文件描述符設置的問題。咱們常說鏈接數受限於文件描述符,這是爲何?

由於在linux上一切皆文件,故每個socket都是被看成一個文件看待,那麼每一個文件就會有一個文件描述符。在linux中每個進程中都有一個數組保存了該進程須要的全部文件描述符。這個文件描述符其實就是這個數組的 key ,它的 value 是一個指針,指向的就是打開的對應文件。

關於文件描述符有兩點注意:

  1. 它對應的實際上是一個linux上的文件
  2. 文件描述符自己這個值在不一樣進程中是能夠重複的

另外補充一點,單機設置的ulimit的上線受限與系統的兩個配置:

fs.nr_open,進程級別

fs.file-max,系統級別

fs.nr_open 老是應該小於等於 fs.file-max,這兩個值的設置也不是隨意能夠操做,由於設置的越大,系統資源消耗越多,因此須要根據真實狀況來進行設置。


至此,本篇長文就完結了。這跟上篇 高併發架構的CDN知識介紹 屬於一個系列,高併發架構須要理解的網絡基礎知識。

後面還會寫一下 HTTP/HTTPS 的知識。而後關於高併發網絡相關的東西就算完結。我會開啓下一個篇章。

相關文章
相關標籤/搜索