TCP三次握手原理,你到底知道多少?
最近碰到一個問題,Client 端連接服務(wù)器總是拋異常。在反復(fù)定位分析、并查閱各種資料搞懂后,我發(fā)現(xiàn)并沒有文章能把這兩個隊(duì)列以及怎么觀察他們的指標(biāo)說清楚。
因此寫下這篇文章,希望借此能把這個問題說清楚。歡迎大家一起交流探討。
問題描述
場景:Java 的 Client 和 Server,使用 Socket 通信。Server 使用 NIO。
問題:
- 間歇性出現(xiàn) Client 向 Server 建立連接三次握手已經(jīng)完成,但 Server 的 Selector 沒有響應(yīng)到該連接。
- 出問題的時間點(diǎn),會同時有很多連接出現(xiàn)這個問題。
- Selector 沒有銷毀重建,一直用的都是一個。
- 程序剛啟動的時候必會出現(xiàn)一些,之后會間歇性出現(xiàn)。
分析問題
正常 TCP 建連接三次握手過程,分為如下三個步驟:
- Client 發(fā)送 Syn 到 Server 發(fā)起握手。
- Server 收到 Syn 后回復(fù) Syn + Ack 給 Client。
- Client 收到 Syn + Ack后,回復(fù) Server 一個 Ack 表示收到了 Server 的 Syn + Ack(此時 Client 的 56911 端口的連接已經(jīng)是 Established)。
從問題的描述來看,有點(diǎn)像 TCP 建連接的時候全連接隊(duì)列(Accept 隊(duì)列,后面具體講)滿了。
尤其是癥狀 2、4 為了證明是這個原因,馬上通過 netstat -s | egrep "listen" 去看隊(duì)列的溢出統(tǒng)計(jì)數(shù)據(jù):
反復(fù)看了幾次之后發(fā)現(xiàn)這個 overflowed 一直在增加,可以明確的是 Server 上全連接隊(duì)列一定溢出了。
接著查看溢出后,OS 怎么處理:
tcp_abort_on_overflow 為 0 表示如果三次握手第三步的時候全連接隊(duì)列滿了那么 Server 扔掉 Client 發(fā)過來的 Ack(在 Server 端認(rèn)為連接還沒建立起來)。
為了證明客戶端應(yīng)用代碼的異常跟全連接隊(duì)列滿有關(guān)系,我先把 tcp_abort_on_overflow 修改成 1。
1 表示第三步的時候如果全連接隊(duì)列滿了,Server 發(fā)送一個 Reset 包給 Client,表示廢掉這個握手過程和這個連接(本來在 Server 端這個連接就還沒建立起來)。
接著測試,這時在客戶端異常中可以看到很多 connection reset by peer 的錯誤,到此證明客戶端錯誤是這個原因?qū)е碌模ㄟ壿媷?yán)謹(jǐn)、快速證明問題的關(guān)鍵點(diǎn)所在)。
于是開發(fā)同學(xué)翻看 Java 源代碼發(fā)現(xiàn) Socket 默認(rèn)的 backlog(這個值控制全連接隊(duì)列的大小,后面再詳述)是 50。
于是改大重新跑,經(jīng)過 12 個小時以上的壓測,這個錯誤一次都沒出現(xiàn)了,同時觀察到 overflowed 也不再增加了。
到此問題解決,簡單來說 TCP 三次握手后有個 Accept 隊(duì)列,進(jìn)到這個隊(duì)列才能從 Listen 變成 Accept,默認(rèn) backlog 值是 50,很容易就滿了。
滿了之后握手第三步的時候 Server 就忽略了 Client 發(fā)過來的 Ack 包(隔一段時間 Server 重發(fā)握手第二步的 Syn + Ack 包給 Client),如果這個連接一直排不上隊(duì)就異常了。
但是不能只是滿足問題的解決,而是要去復(fù)盤解決過程,中間涉及到了哪些知識點(diǎn)是我所缺失或者理解不到位的。
這個問題除了上面的異常信息表現(xiàn)出來之外,還有沒有更明確地指征來查看和確認(rèn)這個問題。
深入理解 TCP 握手過程中建連接的流程和隊(duì)列
如上圖所示,這里有兩個隊(duì)列:Syns Queue(半連接隊(duì)列);Accept Queue(全連接隊(duì)列)。
三次握手中,在第一步 Server 收到 Client 的 Syn 后,把這個連接信息放到半連接隊(duì)列中,同時回復(fù) Syn + Ack 給 Client(第二步):
第三步的時候 Server 收到 Client 的 Ack,如果這時全連接隊(duì)列沒滿,那么從半連接隊(duì)列拿出這個連接的信息放入到全連接隊(duì)列中,否則按 tcp_abort_on_overflow 指示的執(zhí)行。
這時如果全連接隊(duì)列滿了并且 tcp_abort_on_overflow 是 0 的話,Server 過一段時間再次發(fā)送 Syn + Ack 給 Client(也就是重新走握手的第二步),如果 Client 超時等待比較短,Client 就很容易異常了。
在我們的 OS 中 Retry 第二步的默認(rèn)次數(shù)是 2(Centos 默認(rèn)是 5 次):
如果 TCP 連接隊(duì)列溢出,有哪些指標(biāo)可以看呢?
上述解決過程有點(diǎn)繞,聽起來懵,那么下次再出現(xiàn)類似問題有什么更快更明確的手段來確認(rèn)這個問題呢?(通過具體的、感性的東西來強(qiáng)化我們對知識點(diǎn)的理解和吸收。)
netstat -s
比如上面看到的 667399 times ,表示全連接隊(duì)列溢出的次數(shù),隔幾秒鐘執(zhí)行下,如果這個數(shù)字一直在增加的話肯定全連接隊(duì)列偶爾滿了。
ss 命令
上面看到的第二列 Send-Q 值是 50,表示第三列的 Listen 端口上的全連接隊(duì)列最大為 50,第一列 Recv-Q 為全連接隊(duì)列當(dāng)前使用了多少。
全連接隊(duì)列的大小取決于:min(backlog,somaxconn)。backlog 是在 Socket 創(chuàng)建的時候傳入的,Somaxconn 是一個 OS 級別的系統(tǒng)參數(shù)。
這個時候可以跟我們的代碼建立聯(lián)系了,比如 Java 創(chuàng)建 ServerSocket 的時候會讓你傳入 backlog 的值:
半連接隊(duì)列的大小取決于:max(64,/proc/sys/net/ipv4/tcp_max_syn_backlog),不同版本的 OS 會有些差異。
我們寫代碼的時候從來沒有想過這個 backlog 或者說大多時候就沒給它值(那么默認(rèn)就是 50),直接忽視了它。
首先這是一個知識點(diǎn)的盲點(diǎn);其次也許哪天你在哪篇文章中看到了這個參數(shù),當(dāng)時有點(diǎn)印象,但是過一陣子就忘了,這是知識之間沒有建立連接,不是體系化的。
但是如果你跟我一樣首先經(jīng)歷了這個問題的痛苦,然后在壓力和痛苦的驅(qū)動下自己去找為什么。
同時能夠把為什么從代碼層推理理解到 OS 層,那么這個知識點(diǎn)你才算是比較好地掌握了,也會成為你的知識體系在 TCP 或者性能方面成長自我生長的一個有力抓手。
netstat 命令
netstat 跟 ss 命令一樣也能看到 Send-Q、Recv-Q 這些狀態(tài)信息,不過如果這個連接不是 Listen 狀態(tài)的話,Recv-Q 就是指收到的數(shù)據(jù)還在緩存中,還沒被進(jìn)程讀取,這個值就是還沒被進(jìn)程讀取的 bytes。
而 Send 則是發(fā)送隊(duì)列中沒有被遠(yuǎn)程主機(jī)確認(rèn)的 bytes 數(shù),如下圖:
netstat -tn 看到的 Recv-Q 跟全連接半連接沒有關(guān)系,這里特意拿出來說一下是因?yàn)槿菀赘?ss -lnt 的 Recv-Q 搞混淆,順便建立知識體系,鞏固相關(guān)知識點(diǎn) 。
比如如下 netstat -t 看到的 Recv-Q 有大量數(shù)據(jù)堆積,那么一般是 CPU 處理不過來導(dǎo)致的:
上面是通過一些具體的工具、指標(biāo)來認(rèn)識全連接隊(duì)列(工程效率的手段)。
實(shí)踐驗(yàn)證一下上面的理解
把 Java 中 backlog 改成 10(越小越容易溢出),繼續(xù)跑壓力,這個時候 Client 又開始報(bào)異常了,然后在 Server 上通過 ss 命令觀察到:
按照前面的理解,這個時候我們能看到 3306 這個端口上的服務(wù)全連接隊(duì)列最大是 10。
但是現(xiàn)在有 11 個在隊(duì)列中和等待進(jìn)隊(duì)列的,肯定有一個連接進(jìn)不去隊(duì)列要 overflow 掉,同時也確實(shí)能看到 overflow 的值在不斷地增大。
Tomcat 和 Nginx 中的 Accept 隊(duì)列參數(shù)
Tomcat 默認(rèn)短連接,backlog(Tomcat 里面的術(shù)語是 Accept count)Ali-tomcat 默認(rèn)是 200,Apache Tomcat 默認(rèn) 100。
Nginx 默認(rèn)是 511,如下圖:
因?yàn)?Nginx 是多進(jìn)程模式,所以看到了多個 8085,也就是多個進(jìn)程都監(jiān)聽同一個端口以盡量避免上下文切換來提升性能。
總結(jié)
全連接隊(duì)列、半連接隊(duì)列溢出這種問題很容易被忽視,但是又很關(guān)鍵,特別是對于一些短連接應(yīng)用(比如 Nginx、PHP,當(dāng)然它們也是支持長連接的)更容易爆發(fā)。
一旦溢出,從 CPU、線程狀態(tài)看起來都比較正常,但是壓力上不去,在 Client 看來 RT 也比較高(RT = 網(wǎng)絡(luò) + 排隊(duì) + 真正服務(wù)時間),但是從 Server 日志記錄的真正服務(wù)時間來看 rt 又很短。
JDK、Netty 等一些框架默認(rèn) backlog 比較小,可能有些情況下導(dǎo)致性能上不去。
希望通過本文能夠幫大家理解 TCP 連接過程中的半連接隊(duì)列和全連接隊(duì)列的概念、原理和作用,更關(guān)鍵的是有哪些指標(biāo)可以明確看到這些問題(工程效率幫助強(qiáng)化對理論的理解)。
另外每個具體問題都是最好學(xué)習(xí)的機(jī)會,光看書理解肯定是不夠深刻的,請珍惜每個具體問題,碰到后能夠把來龍去脈弄清楚,每個問題都是你對具體知識點(diǎn)通關(guān)的好機(jī)會。
最后提出相關(guān)問題給大家思考:
- 全連接隊(duì)列滿了會影響半連接隊(duì)列嗎?
- netstat -s 看到的 overflowed 和 ignored 的數(shù)值有什么聯(lián)系嗎?
- 如果 Client 走完了 TCP 握手的第三步,在 Client 看來連接已經(jīng)建立好了,但是 Server 上的對應(yīng)連接實(shí)際沒有準(zhǔn)備好,這個時候如果 Client 發(fā)數(shù)據(jù)給 Server,Server 會怎么處理呢?(有同學(xué)說會 Reset,你覺得呢?)
提出這些問題,希望以這個知識點(diǎn)為抓手,讓你的知識體系開始自我生長。
參考文章:
- http://veithen.github.io/2014/01/01/how-tcp-backlog-works-in-linux.html
- http://www.cnblogs.com/zengkefu/p/5606696.html
- http://www.cnxct.com/something-about-phpfpm-s-backlog/
- http://jaseywang.me/2014/07/20/tcp-queue-%E7%9A%84%E4%B8%80%E4%BA%9B%E9%97%AE%E9%A2%98/
- http://jin-yang.github.io/blog/network-synack-queue.html#
- http://blog.chinaunix.net/uid-20662820-id-4154399.html