回顧-上期的遺留問題
- 疑惑1:按道理全連接隊列滿了,但是客戶端的連接請求是已經(jīng)接收到SYN+ACK了,所以對于客戶端來說該連接已經(jīng)建立了,為啥會報connect timeout ? 應(yīng)該是read timeout或者connect reset 。
- 疑惑2:全連接隊列滿了,但客戶端的請求認為連接ESTABLISH狀態(tài),可以繼續(xù)發(fā)送數(shù)據(jù)請求。這時候服務(wù)端如何處理?
TCP三次握手
- client發(fā)送syn給server端。
- server接收到client的syn,這個時候則會將相關(guān)信息放到半鏈接隊列(syn queue),并且發(fā)送syn+ack發(fā)送給client。
-
client接受到server的syn+ack之后,發(fā)送一個ack給server告訴server我接收到了。這個時候server就會將相關(guān)信息放到全連接隊列(accept queue)中。
image.png
回顧-TCP三次握手-半連接隊列和全連接隊列
- 當 client 通過 connect 向 server 發(fā)出 SYN 包時,client 會維護一個 socket 等待隊列,而 server 會維護一個 SYN 隊列
- 此時進入半鏈接的狀態(tài),如果 socket 等待隊列滿了,server 則會丟棄,而 client 也會由此返回 connection time out;只要是 client 沒有收到 SYN+ACK,3s 之后,client 會再次發(fā)送,如果依然沒有收到,9s 之后會繼續(xù)發(fā)送
- 半連接 syn 隊列的長度為 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog) 決定
- 當 server 收到 client 的 SYN 包后,會返回 SYN, ACK 的包加以確認,client 的 TCP 協(xié)議棧會喚醒 socket 等待隊列,發(fā)出 connect 調(diào)用
- client 返回 ACK 的包后,server 會進入一個新的叫 accept 的隊列,該隊列的長度為 min(backlog, somaxconn),默認情況下,somaxconn 的值為 128,表示最多有 129 的 ESTAB 的連接等待 accept(),而 backlog 的值則由 int listen(int sockfd, int backlog) 中的第二個參數(shù)指定,listen 里面的 backlog 的含義請看這里。
- 當accept 隊列滿了之后,即使 client 繼續(xù)向 server 發(fā)送 ACK 的包,也會不被相應(yīng),此時,server 通過 /proc/sys/net/ipv4/tcp_abort_on_overflow 來決定如何返回,0 表示直接丟丟棄該 ACK,1 表示發(fā)送 RST 通知 client;相應(yīng)的,client 則會分別返回 read timeout 或者 connection reset by peer。
(在自己的測試驗證過程中,實際情況還會產(chǎn)生:服務(wù)器會隨機的忽略收到的 SYN,建立起來的連接數(shù)可以無限的增加,只不過客戶端會遇到延時以及超時的情況。)
驗證疑惑1:全連接隊列滿了情況怎么樣?
- 啟動一個TCP服務(wù)端Socket,只對端口做listen監(jiān)聽,不進行accept操作。(port = 9999, backlog = 2)
- 分別調(diào)用4次該端口(采用nc命令: nc ip port的方式)
-
服務(wù)器的TCP參數(shù) cat /proc/sys/net/ipv4/tcp_abort_on_overflow = 0
PS: 注意以下的代碼,servserSocket.accpet()是注釋掉的,代表不會從全連接隊列中獲取請求信息。
image.png
第1次Socket連接:
13:55:31.379052 IP 10.127.4.74.56720 > 10.16.30.142.9999: Flags [S], seq 1290846985, win 8192, options [mss 1360,nop,wscale 8,nop,nop,sackOK], length 0
13:55:31.379065 IP 10.16.30.142.9999 > 10.127.4.74.56720: Flags [S.], seq 3765322754, ack 1290846986, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:31.406038 IP 10.127.4.74.56720 > 10.16.30.142.9999: Flags [.], ack 1, win 69, length 0
第2次Socket連接:
13:55:33.188479 IP 10.127.4.74.56728 > 10.16.30.142.9999: Flags [S], seq 3220053736, win 8192, options [mss 1360,nop,wscale 8,nop,nop,sackOK], length 0
13:55:33.188494 IP 10.16.30.142.9999 > 10.127.4.74.56728: Flags [S.], seq 3458261601, ack 3220053737, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:33.208279 IP 10.127.4.74.56728 > 10.16.30.142.9999: Flags [.], ack 1, win 69, length 0
第3次Socket連接:
13:55:34.974518 IP 10.127.4.74.56735 > 10.16.30.142.9999: Flags [S], seq 1314208155, win 8192, options [mss 1360,nop,wscale 8,nop,nop,sackOK], length 0
13:55:34.974539 IP 10.16.30.142.9999 > 10.127.4.74.56735: Flags [S.], seq 3817678685, ack 1314208156, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:34.999211 IP 10.127.4.74.56735 > 10.16.30.142.9999: Flags [.], ack 1, win 69, length 0
第4次Socket連接
backlog配置的2,全連接隊列個數(shù)為2+1=3,
第4次開始溢出,/proc/sys/net/ipv4/tcp_abort_on_overflow 配置的為0,則系統(tǒng)會默認重試5次SYN+ACK(重試次數(shù)見: /proc/sys/net/ipv4/tcp_synack_retries)
13:55:36.990148 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [S], seq 713717214, win 8192, options [mss 1360,nop,wscale 8,nop,nop,sackOK], length 0
13:55:36.990170 IP 10.16.30.142.9999 > 10.127.4.74.56739: Flags [S.], seq 2481376603, ack 713717215, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:37.011510 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [.], ack 1, win 69, length 0
13:55:38.389731 IP 10.16.30.142.9999 > 10.127.4.74.56739: Flags [S.], seq 2481376603, ack 713717215, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:38.401817 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [.], ack 1, win 69, options [nop,nop,sack 1 {0:1}], length 0
13:55:40.589773 IP 10.16.30.142.9999 > 10.127.4.74.56739: Flags [S.], seq 2481376603, ack 713717215, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:40.607068 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [.], ack 1, win 69, options [nop,nop,sack 1 {0:1}], length 0
13:55:44.789766 IP 10.16.30.142.9999 > 10.127.4.74.56739: Flags [S.], seq 2481376603, ack 713717215, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:44.804919 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [.], ack 1, win 69, options [nop,nop,sack 1 {0:1}], length 0
13:55:52.789749 IP 10.16.30.142.9999 > 10.127.4.74.56739: Flags [S.], seq 2481376603, ack 713717215, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:52.804957 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [.], ack 1, win 69, options [nop,nop,sack 1 {0:1}], length 0
13:56:08.789786 IP 10.16.30.142.9999 > 10.127.4.74.56739: Flags [S.], seq 2481376603, ack 713717215, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:56:08.847814 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [.], ack 1, win 69, options [nop,nop,sack 1 {0:1}], length 0
驗證結(jié)果:抓包的TCP DUMP 顯示,在第4次的時候全連接隊列發(fā)生溢出:在收到客戶端的ack確認時候(時間13:55:37.011510),由于backlog已滿,所以服務(wù)端直接丟棄該ACK確認,再次重試響應(yīng)SYN+ACK(時間13:55:38.389731) 。反復(fù)5次以后,客戶端出現(xiàn)read-timeout。 這個就證實了開章首提的疑惑1確實如我們所想
驗證疑惑1:為啥會出現(xiàn)connect-timeout?
啟動一個http服務(wù),配置tomcat-acceptCount為1,并發(fā)10個線程請求進行,發(fā)現(xiàn)出現(xiàn)了一次connect timeout
#正常的三次握手
18:15:14.975879 IP 10.16.80.136.52633 > 10.16.30.142.19966: Flags [S], seq 17196608, win 8192, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
18:15:14.975907 IP 10.16.30.142.19966 > 10.16.80.136.52633: Flags [S.], seq 3788688748, ack 17196609, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
18:15:14.981048 IP 10.16.80.136.52633 > 10.16.30.142.19966: Flags [.], ack 1, win 256, length 0
#由于全隊列滿隨機忽略不響應(yīng)SYN+ACK:
18:15:14.981065 IP 10.16.80.136.52637 > 10.16.30.142.19966: Flags [S], seq 1960972950, win 8192, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
為啥會出現(xiàn)這種情況,查找了相關(guān)Linux資料,如下:
man listen中提到每次收到新SYN包,內(nèi)核往SYN隊列追加一個新連接(除非該隊列已滿)。但事實并非如此,net/ipv4/tcp_ipv4.c中tcp_v4_conn_request函數(shù)負責處理SYN包,請看以下代碼:
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
goto drop;
- sk_acceptq_is_full()函數(shù)很好理解,根據(jù)字面意思就可以看出,該函數(shù)是檢查連接隊列是否已滿
- inet_csk_reqsk_queue_young()函數(shù)返回半連接隊列中未重傳過SYN+ACK段的連接請求塊數(shù)量。 如果連接隊列已滿并且半連接隊列中的連接請求塊中未重傳的數(shù)量大于1,則會跳轉(zhuǎn)到drop處,丟棄SYN包。如果半連接隊列中未重傳的請求塊數(shù)量大于1,則表示未來可能有2個完成的連接,這些新完成的連接要放到連接隊列中,但此時連接隊列已滿。如果在接收到三次握手中最后的ACK后連接隊列中沒有空閑的位置,會忽略接收到的ACK包,連接建立會推遲,所以此時最好丟掉部分新的連接請求,空出資源以完成正在進行的連接建立過程。還要注意,這個判斷并沒有考慮半連接隊列是否已滿的問題。從這里可以看出,即使開啟了SYN cookies機制并不意味著一定可以完成連接的建立。
- 參考資料:https://blog.csdn.net/justlinux2010/article/details/12619761
自己按照理解畫了一個簡圖,如下:
image.png
結(jié)論:所以正是由于inet_csk_reqsk_queue_young()函數(shù)機制的存在,所以在極端情況下,全連接隊列滿了,也會引起客戶端出現(xiàn)connect-timeout的情況
驗證疑惑2:全連接隊列滿了,客戶端繼續(xù)發(fā)送數(shù)據(jù)請求。這時候服務(wù)端如何處理?
這個其實就是經(jīng)典的client fooling問題
由于全隊列滿的情況對于客戶端來說是透明的,因為請求方已經(jīng)收到了服務(wù)端的SYN+ACK,狀態(tài)已經(jīng)變?yōu)镋STABLISH,客戶端認為連接是建立的,所以繼續(xù)發(fā)消息,那么此種情況下(全隊列滿,服務(wù)端沒有對應(yīng)連接,客戶端認為連接建立成功),客戶端仍發(fā)消息會產(chǎn)生什情況?,試驗如下:
結(jié)論:可以發(fā)現(xiàn)客戶端發(fā)送的數(shù)據(jù)包如果沒有收到服務(wù)端的ack, 客戶端會自動重試幾次,如果仍沒收到ack,則客戶端發(fā)送RST指令斷開已服務(wù)端的連接。
小結(jié)
TCP握手的全連接隊列、半連接隊列溢出這種問題很容易被大家忽視,但是又很關(guān)鍵。 一旦溢出,從各項指標比如CPU, GC情況, 線程狀態(tài)看都很正常,但是壓力上不去,從服務(wù)端上看其他的請求的響應(yīng)時間又很快(見上篇的pinpoint監(jiān)控圖)。
希望本文能夠幫助大家理解TCP 全連接、半連接隊列的概念,以及出現(xiàn)問題如何排查。
如文章有理解不足之處,也請大伙幫忙指出,感謝