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