關(guān)于TCP 半連接隊(duì)列和全連接隊(duì)列

關(guān)于TCP 半連接隊(duì)列和全連接隊(duì)列

最近碰到一個(gè)client端連接異常問(wèn)題,然后定位分析并查閱各種資料文章,對(duì)TCP連接隊(duì)列有個(gè)深入的理解

查資料過(guò)程中發(fā)現(xiàn)沒(méi)有文章把這兩個(gè)隊(duì)列以及怎么觀察他們的指標(biāo)說(shuō)清楚,希望通過(guò)這篇文章能把他們說(shuō)清楚一點(diǎn)

問(wèn)題描述

JAVA的client和server,使用socket通信。server使用NIO。
1.間歇性的出現(xiàn)client向server建立連接三次握手已經(jīng)完成,但server的selector沒(méi)有響應(yīng)到這連接。
2.出問(wèn)題的時(shí)間點(diǎn),會(huì)同時(shí)有很多連接出現(xiàn)這個(gè)問(wèn)題。
3.selector沒(méi)有銷毀重建,一直用的都是一個(gè)。
4.程序剛啟動(dòng)的時(shí)候必會(huì)出現(xiàn)一些,之后會(huì)間歇性出現(xiàn)。

分析問(wèn)題

正常TCP建連接三次握手過(guò)程:

image.png
  • 第一步:client 發(fā)送 syn 到server 發(fā)起握手;
  • 第二步:server 收到 syn后回復(fù)syn+ack給client;
  • 第三步:client 收到syn+ack后,回復(fù)server一個(gè)ack表示收到了server的syn+ack(此時(shí)client的56911端口的連接已經(jīng)是established)

從問(wèn)題的描述來(lái)看,有點(diǎn)像TCP建連接的時(shí)候全連接隊(duì)列(accept隊(duì)列)滿了,尤其是癥狀2、4. 為了證明是這個(gè)原因,馬上通過(guò) ss -s 去看隊(duì)列的溢出統(tǒng)計(jì)數(shù)據(jù):

667399 times the listen queue of a socket overflowed

反復(fù)看了幾次之后發(fā)現(xiàn)這個(gè)overflowed 一直在增加,那么可以明確的是server上全連接隊(duì)列一定溢出了

接著查看溢出后,OS怎么處理:

# cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0

tcp_abort_on_overflow 為0表示如果三次握手第三步的時(shí)候全連接隊(duì)列滿了那么server扔掉client 發(fā)過(guò)來(lái)的ack(在server端認(rèn)為連接還沒(méi)建立起來(lái))

為了證明客戶端應(yīng)用代碼的異常跟全連接隊(duì)列滿有關(guān)系,我先把tcp_abort_on_overflow修改成 1,1表示第三步的時(shí)候如果全連接隊(duì)列滿了,server發(fā)送一個(gè)reset包給client,表示廢掉這個(gè)握手過(guò)程和這個(gè)連接(本來(lái)在server端這個(gè)連接就還沒(méi)建立起來(lái))。

接著測(cè)試然后在客戶端異常中可以看到很多connection reset by peer的錯(cuò)誤,到此證明客戶端錯(cuò)誤是這個(gè)原因?qū)е碌摹?/p>

于是開發(fā)同學(xué)翻看java 源代碼發(fā)現(xiàn)socket 默認(rèn)的backlog(這個(gè)值控制全連接隊(duì)列的大小,后面再詳述)是50,于是改大重新跑,經(jīng)過(guò)12個(gè)小時(shí)以上的壓測(cè),這個(gè)錯(cuò)誤一次都沒(méi)出現(xiàn)過(guò),同時(shí) overflowed 也不再增加了。

到此問(wèn)題解決,簡(jiǎn)單來(lái)說(shuō)TCP三次握手后有個(gè)accept隊(duì)列,進(jìn)到這個(gè)隊(duì)列才能從Listen變成accept,默認(rèn)backlog 值是50,很容易就滿了。滿了之后握手第三步的時(shí)候server就忽略了client發(fā)過(guò)來(lái)的ack包(隔一段時(shí)間server重發(fā)握手第二步的syn+ack包給client),如果這個(gè)連接一直排不上隊(duì)就異常了。

深入理解TCP握手過(guò)程中建連接的流程和隊(duì)列


(圖片來(lái)源:http://www.cnxct.com/something-about-phpfpm-s-backlog/

如上圖所示,這里有兩個(gè)隊(duì)列:syns queue(半連接隊(duì)列);accept queue(全連接隊(duì)列)

三次握手中,在第一步server收到client的syn后,把相關(guān)信息放到半連接隊(duì)列中,同時(shí)回復(fù)syn+ack給client(第二步);

比如syn floods 攻擊就是針對(duì)半連接隊(duì)列的,攻擊方不停地建連接,但是建連接的時(shí)候只做第一步,第二步中攻擊方收到server的syn+ack后故意扔掉什么也不做,導(dǎo)致server上這個(gè)隊(duì)列滿其它正常請(qǐng)求無(wú)法進(jìn)來(lái)

第三步的時(shí)候server收到client的ack,如果這時(shí)全連接隊(duì)列沒(méi)滿,那么從半連接隊(duì)列拿出相關(guān)信息放入到全連接隊(duì)列中,否則按tcp_abort_on_overflow指示的執(zhí)行。

這時(shí)如果全連接隊(duì)列滿了并且tcp_abort_on_overflow是0的話,server過(guò)一段時(shí)間再次發(fā)送syn+ack給client(也就是重新走握手的第二步),如果client超時(shí)等待比較短,就很容易異常了。

在我們的os中retry 第二步的默認(rèn)次數(shù)是2(centos默認(rèn)是5次):

net.ipv4.tcp_synack_retries = 2

如果TCP連接隊(duì)列溢出,有哪些指標(biāo)可以看呢?

上述解決過(guò)程有點(diǎn)繞,那么下次再出現(xiàn)類似問(wèn)題有什么更快更明確的手段來(lái)確認(rèn)這個(gè)問(wèn)題呢?

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 ,表示全連接隊(duì)列溢出的次數(shù),隔幾秒鐘執(zhí)行下,如果這個(gè)數(shù)字一直在增加的話肯定全連接隊(duì)列偶爾滿了。

ss 命令

[root@server ~]# ss -lnt
Recv-Q Send-Q Local Address:Port  Peer Address:Port 
0        50               *:3306             *:* 

上面看到的第二列Send-Q 表示第三列的listen端口上的全連接隊(duì)列最大為50,第一列Recv-Q為全連接隊(duì)列當(dāng)前使用了多少

全連接隊(duì)列的大小取決于:min(backlog, somaxconn) . backlog是在socket創(chuàng)建的時(shí)候傳入的,somaxconn是一個(gè)os級(jí)別的系統(tǒng)參數(shù)

半連接隊(duì)列的大小取決于:max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。 不同版本的os會(huì)有些差異

實(shí)踐驗(yàn)證下上面的理解

把java中backlog改成10(越小越容易溢出),繼續(xù)跑壓力,這個(gè)時(shí)候client又開始報(bào)異常了,然后在server上通過(guò) ss 命令觀察到:

Fri May  5 13:50:23 CST 2017
Recv-Q Send-QLocal Address:Port  Peer Address:Port
11         10         *:3306               *:*

按照前面的理解,這個(gè)時(shí)候我們能看到3306這個(gè)端口上的服務(wù)全連接隊(duì)列最大是10,但是現(xiàn)在有11個(gè)在隊(duì)列中和等待進(jìn)隊(duì)列的,肯定有一個(gè)連接進(jìn)不去隊(duì)列要overflow掉

容器中的Accept隊(duì)列參數(shù)

Tomcat默認(rèn)短連接,backlog(Tomcat里面的術(shù)語(yǔ)是Accept count)Ali-tomcat默認(rèn)是200, Apache Tomcat默認(rèn)100.

#ss -lnt
Recv-Q Send-Q   Local Address:Port Peer Address:Port
0       100                 *:8080            *:*

Nginx默認(rèn)是511

$sudo ss -lnt
State  Recv-Q Send-Q Local Address:PortPeer Address:Port
LISTEN    0     511              *:8085           *:*
LISTEN    0     511              *:8085           *:*

因?yàn)镹ginx是多進(jìn)程模式,也就是多個(gè)進(jìn)程都監(jiān)聽同一個(gè)端口以盡量避免上下文切換來(lái)提升性能

進(jìn)一步思考

如果client走完第三步在client看來(lái)連接已經(jīng)建立好了,但是server上的對(duì)應(yīng)連接實(shí)際沒(méi)有準(zhǔn)備好,這個(gè)時(shí)候如果client發(fā)數(shù)據(jù)給server,server會(huì)怎么處理呢?(有同學(xué)說(shuō)會(huì)reset,還是實(shí)踐看看)

先來(lái)看一個(gè)例子:

image.png

(圖片來(lái)自:http://blog.chinaunix.net/uid-20662820-id-4154399.html

如上圖,150166號(hào)包是三次握手中的第三步client發(fā)送ack給server,然后150167號(hào)包中client發(fā)送了一個(gè)長(zhǎng)度為816的包給server,因?yàn)樵谶@個(gè)時(shí)候client認(rèn)為連接建立成功,但是server上這個(gè)連接實(shí)際沒(méi)有ready,所以server沒(méi)有回復(fù),一段時(shí)間后client認(rèn)為丟包了然后重傳這816個(gè)字節(jié)的包,一直到超時(shí),client主動(dòng)發(fā)fin包斷開該連接。

這個(gè)問(wèn)題也叫client fooling,可以看這里:https://github.com/torvalds/linux/commit/5ea8ea2cb7f1d0db15762c9b0bb9e7330425a071 (感謝 @劉歡(淺奕(16:00后答疑) 的提示)

**從上面的實(shí)際抓包來(lái)看不是reset,而是server忽略這些包,然后client重傳,一定次數(shù)后client認(rèn)為異常,然后斷開連接。
**

過(guò)程中發(fā)現(xiàn)的一個(gè)奇怪問(wèn)題

[root@server ~]# date; netstat -s | egrep "listen|LISTEN" 
Fri May  5 15:39:58 CST 2017
1641685 times the listen queue of a socket overflowed
1641685 SYNs to LISTEN sockets ignored

[root@server ~]# date; netstat -s | egrep "listen|LISTEN" 
Fri May  5 15:39:59 CST 2017
1641906 times the listen queue of a socket overflowed
1641906 SYNs to LISTEN sockets ignored

如上所示:
overflowed和ignored居然總是一樣多,并且都是同步增加,overflowed表示全連接隊(duì)列溢出次數(shù),socket ignored表示半連接隊(duì)列溢出次數(shù),沒(méi)這么巧吧。

翻看內(nèi)核源代碼(http://elixir.free-electrons.com/linux/v3.18/source/net/ipv4/tcp_ipv4.c):

image.png

可以看到overflow的時(shí)候一定會(huì)drop++(socket ignored),也就是drop一定大于等于overflow。

同時(shí)我也查看了另外幾臺(tái)server的這兩個(gè)值來(lái)證明drop一定大于等于overflow:

server1
150 SYNs to LISTEN sockets dropped

server2
193 SYNs to LISTEN sockets dropped

server3
16329 times the listen queue of a socket overflowed
16422 SYNs to LISTEN sockets dropped

server4
20 times the listen queue of a socket overflowed
51 SYNs to LISTEN sockets dropped

server5
984932 times the listen queue of a socket overflowed
988003 SYNs to LISTEN sockets dropped

那么全連接隊(duì)列滿了會(huì)影響半連接隊(duì)列嗎?

來(lái)看三次握手第一步的源代碼(http://elixir.free-electrons.com/linux/v2.6.33/source/net/ipv4/tcp_ipv4.c#L1249):

image.png

TCP三次握手第一步的時(shí)候如果全連接隊(duì)列滿了會(huì)影響第一步drop 半連接的發(fā)生。大概流程的如下:

tcp_v4_do_rcv->tcp_rcv_state_process->tcp_v4_conn_request
//如果accept backlog隊(duì)列已滿,且未超時(shí)的request socket的數(shù)量大于1,則丟棄當(dāng)前請(qǐng)求  
  if(sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_yong(sk)>1)
      goto drop;

總結(jié)

全連接隊(duì)列、半連接隊(duì)列溢出這種問(wèn)題很容易被忽視,但是又很關(guān)鍵,特別是對(duì)于一些短連接應(yīng)用(比如Nginx、PHP,當(dāng)然他們也是支持長(zhǎng)連接的)更容易爆發(fā)。 一旦溢出,從cpu、線程狀態(tài)看起來(lái)都比較正常,但是壓力上不去,在client看來(lái)rt也比較高(rt=網(wǎng)絡(luò)+排隊(duì)+真正服務(wù)時(shí)間),但是從server日志記錄的真正服務(wù)時(shí)間來(lái)看rt又很短。

另外就是jdk、netty等一些框架默認(rèn)backlog比較小,可能有些情況下導(dǎo)致性能上不去,比如 @畢玄 碰到的這個(gè) 《netty新建連接并發(fā)數(shù)很小的case》
都是類似原因

希望通過(guò)本文能夠幫大家理解TCP連接過(guò)程中的半連接隊(duì)列和全連接隊(duì)列的概念、原理和作用,更關(guān)鍵的是有哪些指標(biāo)可以明確看到這些問(wèn)題。

另外每個(gè)具體問(wèn)題都是最好學(xué)習(xí)的機(jī)會(huì),光看書理解肯定是不夠深刻的,請(qǐng)珍惜每個(gè)具體問(wèn)題,碰到后能夠把來(lái)龍去脈弄清楚。

我的其他幾篇跟網(wǎng)絡(luò)問(wèn)題相關(guān)的文章,也很有趣,借著案例來(lái)理解好概念和原理,希望對(duì)大家也有點(diǎn)幫助

https://www.atatech.org/articles/60633

https://www.atatech.org/articles/73174

https://www.atatech.org/articles/73289

https://www.atatech.org/articles/76138

最后感謝 @夢(mèng)實(shí) 在這個(gè)過(guò)程中提供的幫助


參考文章:

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

https://www.atatech.org/articles/12919

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容