tcp的无延时发送_高并发架构的TCP知识介绍
這是關于高并發架構網絡協議基礎知識的第二篇,編程路上的基礎心法!
做為一個有追求的程序員,不能只滿足增刪改查,我們要對系統全方面無死角掌控。掌握了這些基本的網絡知識后,相信一方面日常排錯中會事半功倍,另一方面日常架構中不得不考慮的高并發問題,理解了這些底層協議也是會如虎添翼。
本文不會單純給大家講講TCP三次握手、四次揮手就完事了。如果只是哪樣的話,我直接貼幾個連接就完事了。我希望把實際工作中的很多點能夠串起來講給大家。當然為了文章完整,我依然會從?三次握手?起頭。
再說TCP狀態變更過程
不管是三次握手、還是四次揮手,他們都是完成了TCP不同狀態的切換。進而影響各種數據的傳輸情況。下面從三次握手開始分析。
本文圖片有部分來自網絡,若有侵權,告知即焚
三次握手
來看看三次握手的圖,估計大家看這圖都快看吐了,不過為什么每次面試、回憶的時候還是想不起呢?我再來抄抄這鍋剩飯吧
首先當服務端處于?listen?狀態的時候,我們就可以再客戶端發起監聽了,此時客戶端會處于?SYN_SENT?狀態。服務端收到這個消息會返回一個?SYN?并且同時?ACK?客戶端的請求,之后服務端便處于?SYN_RCVD?狀態。這個時候客戶端收到了服務端的?SYN&ACK,就會發送對服務端的?ACK,之后便處于?ESTABLISHED?狀態。服務端收到了對自己的?ACK?后也會處于?ESTABLISHED?狀態。
經常在面試中可能有人提問:為什么握手要3次,不是2次或者4次呢?
首先說4次握手,其實為了保證可靠性,這個握手次數可以一直循環下去;但是這沒有一個終止就沒有意義了。所以3次,保證了各方消息有來有回就足夠了。當然這里可能有一種情況是,客戶端發送的?ACK?在網絡中被丟了。那怎么辦?
其實大部分時候,我們連接建立完成就會立刻發送數據,所以如果服務端沒有收到?ACK?沒關系,當收到數據就會認為連接已經建立;
如果連接建立后不立馬傳輸數據,那么服務端認為連接沒有建立成功會周期性重發?SYN&ACK?直到客戶端確認成功。
再說為什么2次握手不行呢?2次握手我們可以想象是沒有三次握手最后的?ACK, 在實際中確實會出現客戶端發送?ACK?服務端沒有收到的情況(上面的情況一),那么這是否說明兩次握手也是可行的呢?
看下情況二,2次握手當服務端發送消息后,就認為建立成功,而恰巧此時又沒有數據傳輸。這就會帶來一種資源浪費的情況。比如:客戶端可能由于延時發送了多個連接情況,當服務端每收到一個請求回復后就認為連接建立成功,但是這其中很多求情都是延時產生的重復連接,浪費了很多寶貴的資源。
因此綜上所述,從資源節省、效率3次握手都是最合適的。話又回來三次握手的真實意義其實就是協商傳輸數據用的:序列號與窗口大小。
下面我們通過抓包再來看一下真實的情況是否如上所述。
| 123 | 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 020: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 020: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
四次揮手
揮手,就是說數據傳完了,同志們再見!
這里有個問題需要注意下,其實客戶端、服務端都能夠主動發起關閉操作,誰調用?close()?就先發送關閉的請求。當然一般的流程,發起建立連接的一方會主動發起關閉請求(http中)。
關于4次揮手的過程,我就不多解釋了,這里有兩個重要的狀態我需要解釋下,這都是我親自經歷過的線上故障,close_wait?與?time_wait。
先給大家一個命令,統計tcp的各種狀態情況。下面表格內容就來自這個命令的統計。
netstat -n | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}’
| 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?這個問題在日常中經??吹?#xff0c;流量一高就出現大量的該情況。該狀態出現在主動發起關閉的一方。該狀態一般等待的時間設為 2MSL后自動關閉,MSL是Maximum Segment Lifetime,報文最大生存時間,如果報文超過這個時間,就會被丟棄。處于該狀態下的socket也是不能被回收使用的。線上我就遇到這種情況,每次大流量的時候,每臺機器處于該狀態的socket就多達10w+,遠遠比處于?Established?狀態的socket多的多,導致很多時候服務響應能力下降。這個一方面可以通過調整內核參數處理,另一方面避免使用太多的短鏈接,可以采用連接池來提升性能。另外在代碼層面可能是由于某些地方沒有關閉連接導致的,也需要檢查業務代碼。
上面兩個狀態一定要牢記發生在哪一方,這方便我們快速定位問題。
最后這里還是放上揮手時的抓包數據:
| 1234 | 20:33:26.750607 IP 192.168.0.102.58165 > 103.235.46.39.80: Flags [F.], seq 621839159, ack 1754967720, win 4096, length 020:33:26.827472 IP 103.235.46.39.80 > 192.168.0.102.58165: Flags [.], ack 621839160, win 776, length 020:33:26.827677 IP 103.235.46.39.80 > 192.168.0.102.58165: Flags [F.], seq 1754967720, ack 621839160, win 776, length 020: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 在客戶端上的變更情況了呢?
服務端狀態變更
這一張圖描述了 TCP 狀態在服務端的變遷。
TCP的流量控制與擁塞控制
我們常說TCP是面向連接的,UDP是無連接的。那么TCP這個面向連接主要解決的是什么問題呢?
這里繼續把三次握手的抓包數據貼出來分析下:
| 123 | 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 020: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 020: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操作步驟圖,畫的太好了我就直接用了。
我們假設我的服務端就是?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?是一個指針,指向的就是打開的對應文件。
關于文件描述符有兩點注意:
它對應的其實是一個linux上的文件
文件描述符本身這個值在不同進程中是可以重復的
另外補充一點,單機設置的ulimit的上線受限與系統的兩個配置:
fs.nr_open,進程級別
fs.file-max,系統級別
fs.nr_open 總是應該小于等于 fs.file-max,這兩個值的設置也不是隨意可以操作,因為設置的越大,系統資源消耗越多,所以需要根據真實情況來進行設置。
至此,本篇長文就完結了。這跟上篇?高并發架構的CDN知識介紹?屬于一個系列,高并發架構需要理解的網絡基礎知識。
后面還會寫一下 HTTP/HTTPS 的知識。然后關于高并發網絡相關的東西就算完結。我會開啟下一個篇章。
如果你想對網絡協議了解更多,推薦一個課程:
總結
以上是生活随笔為你收集整理的tcp的无延时发送_高并发架构的TCP知识介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 有十五个数按由大到小顺序存放在一个数组中
- 下一篇: 大于3小于4的整数bleem_比三大,比