最近碰到一個問題,Client 端鏈接服務器老是拋異常。在反覆定位分析、並查閱各類資料搞懂後,我發現並無文章能把這兩個隊列以及怎麼觀察他們的指標說清楚。html
場景:Java 的 Client 和 Server,使用 Socket 通訊。Server 使用 NIO。java
問題:nginx
正常 TCP 建鏈接三次握手過程,分爲以下三個步驟:centos
從問題的描述來看,有點像 TCP 建鏈接的時候全鏈接隊列(Accept 隊列,後面具體講)滿了。api
尤爲是症狀 二、4 爲了證實是這個緣由,立刻經過 netstat -s | egrep "listen"
去看隊列的溢出統計數據:緩存
667399 times the listen queue of a socket overflowed
反覆看了幾回以後發現這個overflowed 一直在增長,那麼能夠明確的是server上全鏈接隊列必定溢出了。tomcat
接着查看溢出後,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),若是這個鏈接一直排不上隊就異常了。
可是不能只是知足問題的解決,而是要去覆盤解決過程,中間涉及到了哪些知識點是我所缺失或者理解不到位的。
這個問題除了上面的異常信息表現出來以外,還有沒有更明確地指徵來查看和確認這個問題。
如上圖所示,這裏有兩個隊列: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
上述解決過程有點繞,聽起來懵,那麼下次再出現相似問題有什麼更快更明確的手段來確認這個問題呢?(經過具體的、感性的東西來強化咱們對知識點的理解和吸取。)
[root@server ~] # netstat -s | egrep "listen|LISTEN" 667399 times the listen queue of a socket overflowed 667399 SYNs to LISTEN sockets ignored
好比上面看到的 667399 times ,表示全鏈接隊列溢出的次數,隔幾秒鐘執行下,若是這個數字一直在增長的話確定全鏈接隊列偶爾滿了。
[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 跟 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默認短鏈接,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鏈接過程當中的半鏈接隊列和全鏈接隊列的概念、原理和做用,更關鍵的是有哪些指標能夠明確看到這些問題(工程效率幫助強化對理論的理解)。
另外每一個具體問題都是最好學習的機會,光看書理解確定是不夠深入的,請珍惜每一個具體問題,碰到後可以把前因後果弄清楚,每一個問題都是你對具體知識點通關的好機會。
最後提出相關問題給你們思考
來源:阿里技術微信公衆號