問題描述
JAVA的client和server厘擂,使用socket通信滴劲。server使用NIO。
1.間歇性的出現(xiàn)client向server建立連接三次握手已經(jīng)完成,但server的selector沒有響應到這連接餐抢。
2.出問題的時間點现使,會同時有很多連接出現(xiàn)這個問題。
3.selector沒有銷毀重建旷痕,一直用的都是一個碳锈。
4.程序剛啟動的時候必會出現(xiàn)一些,之后會間歇性出現(xiàn)欺抗。
第一步:client 發(fā)送 syn 到server 發(fā)起握手售碳;
第二步:server 收到 syn后回復syn+ack給client;
第三步:client 收到syn+ack后绞呈,回復server一個ack表示收到了server的syn+ack(此時client的56911端口的連接已經(jīng)是established)
從問題的描述來看贸人,有點像TCP建連接的時候全連接隊列(accept隊列)滿了,尤其是癥狀2佃声、4. 為了證明是這個原因艺智,馬上通過 ss -s 去看隊列的溢出統(tǒng)計數(shù)據(jù):
667399 times the listen queue of a socket overflowed
反復看了幾次之后發(fā)現(xiàn)這個overflowed 一直在增加,那么可以明確的是server上全連接隊列一定溢出了
接著查看溢出后圾亏,OS怎么處理:
# cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0
tcp_abort_on_overflow 為0表示如果三次握手第三步的時候全連接隊列滿了那么server扔掉client 發(fā)過來的ack(在server端認為連接還沒建立起來)
為了證明客戶端應用代碼的異常跟全連接隊列滿有關系力惯,我先把tcp_abort_on_overflow修改成 1,1表示第三步的時候如果全連接隊列滿了召嘶,server發(fā)送一個reset包給client,表示廢掉這個握手過程和這個連接(本來在server端這個連接就還沒建立起來)哮缺。
接著測試然后在客戶端異常中可以看到很多connection reset by peer的錯誤弄跌,到此證明客戶端錯誤是這個原因導致的。
于是開發(fā)同學翻看java 源代碼發(fā)現(xiàn)socket 默認的backlog(這個值控制全連接隊列的大小尝苇,后面再詳述)是50铛只,于是改大重新跑,經(jīng)過12個小時以上的壓測糠溜,這個錯誤一次都沒出現(xiàn)過淳玩,同時 overflowed 也不再增加了。
到此問題解決非竿,簡單來說TCP三次握手后有個accept隊列蜕着,進到這個隊列才能從Listen變成accept,默認backlog 值是50红柱,很容易就滿了承匣。滿了之后握手第三步的時候server就忽略了client發(fā)過來的ack包(隔一段時間server重發(fā)握手第二步的syn+ack包給client),如果這個連接一直排不上隊就異常了锤悄。
(圖片來源:http://www.cnxct.com/something-about-phpfpm-s-backlog/)
如上圖所示韧骗,這里有兩個隊列:syns queue(半連接隊列);accept queue(全連接隊列)
三次握手中零聚,在第一步server收到client的syn后袍暴,把相關信息放到半連接隊列中些侍,同時回復syn+ack給client(第二步);
比如syn floods 攻擊就是針對半連接隊列的政模,攻擊方不停地建連接岗宣,但是建連接的時候只做第一步,第二步中攻擊方收到server的syn+ack后故意扔掉什么也不做览徒,導致server上這個隊列滿其它正常請求無法進來
第三步的時候server收到client的ack狈定,如果這時全連接隊列沒滿,那么從半連接隊列拿出相關信息放入到全連接隊列中习蓬,否則按tcp_abort_on_overflow指示的執(zhí)行纽什。
這時如果全連接隊列滿了并且tcp_abort_on_overflow是0的話,server過一段時間再次發(fā)送syn+ack給client(也就是重新走握手的第二步)躲叼,如果client超時等待比較短芦缰,就很容易異常了。
在我們的os中retry 第二步的默認次數(shù)是2(centos默認是5次):
net.ipv4.tcp_synack_retries = 2
上述解決過程有點繞,那么下次再出現(xiàn)類似問題有什么更快更明確的手段來確認這個問題呢或听?
[root@server ~]#? netstat -s | egrep "listen|LISTEN"
667399 times the listen queue of a socket overflowed
667399 SYNs to LISTEN sockets ignored
比如上面看到的 667399 times 探孝,表示全連接隊列溢出的次數(shù),隔幾秒鐘執(zhí)行下誉裆,如果這個數(shù)字一直在增加的話肯定全連接隊列偶爾滿了顿颅。
[root@server ~]# ss -lnt
Recv-Q Send-Q Local Address:Port? Peer Address:Port
0? ? ? ? 50? ? ? ? ? ? ? *:3306? ? ? ? ? ? *:*
上面看到的第二列Send-Q 表示第三列的listen端口上的全連接隊列最大為50,第一列Recv-Q為全連接隊列當前使用了多少
全連接隊列的大小取決于:min(backlog, somaxconn) . backlog是在socket創(chuàng)建的時候傳入的足丢,somaxconn是一個os級別的系統(tǒng)參數(shù)
半連接隊列的大小取決于:max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)粱腻。 不同版本的os會有些差異
把java中backlog改成10(越小越容易溢出),繼續(xù)跑壓力斩跌,這個時候client又開始報異常了绍些,然后在server上通過 ss 命令觀察到:
Fri May? 5 13:50:23 CST 2017
Recv-Q Send-QLocal Address:Port? Peer Address:Port
11? ? ? ? 10? ? ? ? *:3306? ? ? ? ? ? ? *:*
按照前面的理解,這個時候我們能看到3306這個端口上的服務全連接隊列最大是10耀鸦,但是現(xiàn)在有11個在隊列中和等待進隊列的柬批,肯定有一個連接進不去隊列要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:PortPeer Address:Port
LISTEN? ? 0? ? 511? ? ? ? ? ? ? *:8085? ? ? ? ? *:*
LISTEN? ? 0? ? 511? ? ? ? ? ? ? *:8085? ? ? ? ? *:*
因為Nginx是多進程模式袖订,也就是多個進程都監(jiān)聽同一個端口以盡量避免上下文切換來提升性能
如果client走完第三步在client看來連接已經(jīng)建立好了萝快,但是server上的對應連接實際沒有準備好,這個時候如果client發(fā)數(shù)據(jù)給server著角,server會怎么處理呢揪漩?(有同學說會reset,還是實踐看看)
先來看一個例子:
(圖片來自:http://blog.chinaunix.net/uid-20662820-id-4154399.html)
如上圖吏口,150166號包是三次握手中的第三步client發(fā)送ack給server奄容,然后150167號包中client發(fā)送了一個長度為816的包給server冰更,因為在這個時候client認為連接建立成功,但是server上這個連接實際沒有ready昂勒,所以server沒有回復蜀细,一段時間后client認為丟包了然后重傳這816個字節(jié)的包,一直到超時戈盈,client主動發(fā)fin包斷開該連接奠衔。
這個問題也叫client fooling,可以看這里:https://github.com/torvalds/linux/commit/5ea8ea2cb7f1d0db15762c9b0bb9e7330425a071(感謝淺奕的提示)
從上面的實際抓包來看不是reset塘娶,而是server忽略這些包归斤,然后client重傳,一定次數(shù)后client認為異常刁岸,然后斷開連接脏里。
[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表示全連接隊列溢出次數(shù)迫横,socket ignored表示半連接隊列溢出次數(shù),沒這么巧吧酝碳。
翻看內(nèi)核源代碼(http://elixir.free-electrons.com/linux/v3.18/source/net/ipv4/tcp_ipv4.c):
可以看到overflow的時候一定會drop++(socket ignored)矾踱,也就是drop一定大于等于overflow。
同時我也查看了另外幾臺server的這兩個值來證明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
來看三次握手第一步的源代碼(http://elixir.free-electrons.com/linux/v2.6.33/source/net/ipv4/tcp_ipv4.c#L1249):
TCP三次握手第一步的時候如果全連接隊列滿了會影響第一步drop 半連接的發(fā)生介返。大概流程的如下:
tcp_v4_do_rcv->tcp_rcv_state_process->tcp_v4_conn_request
//如果accept backlog隊列已滿,且未超時的request socket的數(shù)量大于1沃斤,則丟棄當前請求
if(sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_yong(sk)>1)
goto drop;
全連接隊列、半連接隊列溢出這種問題很容易被忽視刃宵,但是又很關鍵衡瓶,特別是對于一些短連接應用(比如Nginx、PHP牲证,當然他們也是支持長連接的)更容易爆發(fā)哮针。 一旦溢出,從cpu坦袍、線程狀態(tài)看起來都比較正常十厢,但是壓力上不去,在client看來rt也比較高(rt=網(wǎng)絡+排隊+真正服務時間)捂齐,但是從server日志記錄的真正服務時間來看rt又很短蛮放。