最近碰到一個問題,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"
去看隊列的溢出統計數據:
667399 times the listen queue of a socket overflowed
反復看了幾次之后發現這個overflowed 一直在增加,那么可以明確的是server上全連接隊列一定溢出了。
接着查看溢出后,OS怎么處理:
# cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0
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(第二步);
題外話,比如
syn floods
攻擊就是針對半連接隊列的,攻擊方不停地建連接,但是建連接的時候只做第一步,第二步中攻擊方收到server的syn+ack后故意扔掉什么也不做,導致server上這個隊列滿其他正常請求無法進來。
第三步的時候server收到client的ack,如果這時全連接隊列沒滿,那么從半連接隊列拿出這個連接的信息放入到全連接隊列中,否則按tcp_abort_on_overflow
指示的執行。
這時如果全連接隊列滿了並且tcp_abort_on_overflow
是0的話,server過一段時間再次發送syn+ack給client(也就是重新走握手的第二步),如果client超時等待比較短,client就很容易異常了。
在我們的os中retry 第二步的默認次數是2(centos默認是5次):
net.ipv4.tcp_synack_retries =2
如果TCP連接隊列溢出,有哪些指標可以看呢?
上述解決過程有點繞,聽起來懵,那么下次再出現類似問題有什么更快更明確的手段來確認這個問題呢?(通過具體的、感性的東西來強化我們對知識點的理解和吸收。)
netstat -s
[root@server ~] # netstat -s | egrep "listen|LISTEN"
667399 times the listen queue of a socket overflowed
667399 SYNs to LISTEN sockets ignored
比如上面看到的 667399 times ,表示全連接隊列溢出的次數,隔幾秒鍾執行下,如果這個數字一直在增加的話肯定全連接隊列偶爾滿了。
ss 命令
[root@server ~]#ss -lnt
Recv -Q Send -Q Loacl Address:Port Peer Address:Port
0 50 *:3306 *:*
上面看到的第二列Send-Q 值是50,表示第三列的listen端口上的全連接隊列最大為50,第一列Recv-Q為全連接隊列當前使用了多少。
全連接隊列的大小取決於:min(backlog, somaxconn) 。backlog是在socket創建的時候傳入的,somaxconn是一個os級別的系統參數。
這個時候可以跟我們的代碼建立聯系了,比如Java創建ServerSocket的時候會讓你傳入backlog的值:
ServerSocket()
Creates an unbound server socket.
ServerSocket(int port)
Creates a server socket,bound to the specified port.
ServerSocket(int port, int backlog)
Creates a server socket and binds it to the specified local port number, with the specified backlog.
ServerSocket(int port, int backlog, InetAddress bindAddr)
Creates a server with the specified port, listen backlog, and local IP address to bind to.
半連接隊列的大小取決於: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。
$netstat -tn
Active Internet connections(w/o servers)
Proto Recv -Q Send -Q Local Address Foreign Address State
tcp0 0 server:8182 client-1:15260 SYN_RECV
tcp0 28 server:22 client-1:51708 ESTABLISHED
tcp0 0 server:2376 client-1:60269 ESTABLISHED
netstat -tn 看到的 Recv-Q 跟全連接半連接沒有關系,這里特意拿出來說一下是因為容易跟 ss -lnt 的 Recv-Q 搞混淆,順便建立知識體系,鞏固相關知識點 。
比如如下netstat -t 看到的Recv-Q有大量數據堆積,那么一般是CPU處理不過來導致的:
上面是通過一些具體的工具、指標來認識全連接隊列(工程效率的手段)。
實踐驗證一下上面的理解
把java中backlog改成10(越小越容易溢出),繼續跑壓力,這個時候client又開始報異常了,然后在server上通過 ss 命令觀察到:
Fri May 5 13:50:23 CST 2017
Recv -Q Send -Q Local Address:port Peer Address:Port
11 10 *:3306 *:*
按照前面的理解,這個時候我們能看到3306這個端口上的服務全連接隊列最大是10,但是現在有11個在隊列中和等待進隊列的,肯定有一個連接進不去隊列要overflow掉,同時也確實能看到overflow的值在不斷地增大。
Tomcat和Nginx中的Accept隊列參數
Tomcat默認短連接,backlog(Tomcat里面的術語是Accept count)Ali-tomcat默認是200, Apache Tomcat默認100。
#ss -lnt
Recv -Q Send -Q Local Address:port Peer Address:Port
0 100 *: 8080 *:*
Nginx默認是511。
#sudo ss -lnt
State Recv -Q Send -Q Local Address:Port Peer Address:Port
LISTEN 0 511 *: 8085 *:*
LISTEN 0 511 *: 8085 *:*
因為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,你覺得呢?)
來源:阿里技術微信公眾號