一、背景
涉及到網(wǎng)絡(luò)層面的問(wèn)題一般都比較復(fù)雜闺魏,場(chǎng)景多未状,定位難,成為了大多數(shù)開(kāi)發(fā)的噩夢(mèng)析桥,應(yīng)該是最復(fù)雜的了司草。
這里會(huì)舉一些例子艰垂,并從tcp層、應(yīng)用層以及工具的使用等方面進(jìn)行闡述埋虹。
二猜憎、現(xiàn)象
1.超時(shí)
超時(shí)錯(cuò)誤大部分處在應(yīng)用層面,所以這塊著重理解概念搔课。
超時(shí)大體可以分為連接超時(shí)和讀寫超時(shí)拉宗,某些使用連接池的客戶端框架還會(huì)存在獲取連接超時(shí)和空閑連接清理超時(shí)。
讀寫超時(shí)辣辫。readTimeout/writeTimeout旦事,有些框架叫做so_timeout或者socketTimeout,均指的是數(shù)據(jù)讀寫超時(shí)急灭。注意這邊的超時(shí)大部分是指邏輯上的超時(shí)姐浮。soa的超時(shí)指的也是讀超時(shí)。讀寫超時(shí)一般都只針對(duì)客戶端設(shè)置葬馋。
連接超時(shí)卖鲤。connectionTimeout,客戶端通常指與服務(wù)端建立連接的最大時(shí)間畴嘶。服務(wù)端這邊connectionTimeout就有些五花八門了蛋逾,jetty中表示空閑連接清理時(shí)間,tomcat則表示連接維持的最大時(shí)間窗悯。
其他区匣,包括連接獲取超時(shí)connectionAcquireTimeout和空閑連接清理超時(shí)idleConnectionTimeout。多用于使用連接池或隊(duì)列的客戶端或服務(wù)端框架蒋院。
我們?cè)谠O(shè)置各種超時(shí)時(shí)間中亏钩,需要確認(rèn)的是盡量保持客戶端的超時(shí)小于服務(wù)端的超時(shí),以保證連接正常結(jié)束欺旧。
在實(shí)際開(kāi)發(fā)中姑丑,我們關(guān)心最多的應(yīng)該是接口的讀寫超時(shí)了。
如何設(shè)置合理的接口超時(shí)是一個(gè)問(wèn)題辞友。如果接口超時(shí)設(shè)置的過(guò)長(zhǎng)栅哀,那么有可能會(huì)過(guò)多地占用服務(wù)端的tcp連接。而如果接口設(shè)置的過(guò)短称龙,那么接口超時(shí)就會(huì)非常頻繁留拾。
服務(wù)端接口明明rt降低,但客戶端仍然一直超時(shí)又是另一個(gè)問(wèn)題茵瀑。
這個(gè)問(wèn)題其實(shí)很簡(jiǎn)單间驮,客戶端到服務(wù)端的鏈路包括網(wǎng)絡(luò)傳輸躬厌、排隊(duì)以及服務(wù)處理等马昨,每一個(gè)環(huán)節(jié)都可能是耗時(shí)的原因竞帽。
2.TCP隊(duì)列溢出
tcp隊(duì)列溢出是個(gè)相對(duì)底層的錯(cuò)誤,它可能會(huì)造成超時(shí)鸿捧、rst等更表層的錯(cuò)誤屹篓。
因此錯(cuò)誤也更隱蔽,所以我們單獨(dú)說(shuō)一說(shuō)匙奴。
如上圖所示堆巧,這里有兩個(gè)隊(duì)列:syns queue(半連接隊(duì)列)、accept queue(全連接隊(duì)列)泼菌。
三次握手谍肤,在server收到client的syn后,把消息放到syns queue哗伯,回復(fù)syn+ack給client荒揣;
server收到client的ack,如果這時(shí)accept queue沒(méi)滿焊刹,那就從syns queue拿出暫存的信息放入accept queue中系任,否則按tcp_abort_on_overflow指示的執(zhí)行:
tcp_abort_on_overflow 0表示如果三次握手第三步的時(shí)候accept queue滿了那么server扔掉client發(fā)過(guò)來(lái)的ack。
tcp_abort_on_overflow 1則表示第三步的時(shí)候如果全連接隊(duì)列滿了虐块,server發(fā)送一個(gè)rst包給client俩滥,表示廢掉這個(gè)握手過(guò)程和這個(gè)連接,意味著日志里可能會(huì)有很多connection reset / connection reset by peer贺奠。
那么在實(shí)際開(kāi)發(fā)中霜旧,我們?cè)趺茨芸焖俣ㄎ坏絫cp隊(duì)列溢出呢?
netstat命令儡率,執(zhí)行netstat -s | egrep "listen|LISTEN"
如上圖所示:
overflowed表示全連接隊(duì)列溢出的次數(shù)
sockets dropped表示半連接隊(duì)列溢出的次數(shù)颁糟。
ss命令,執(zhí)行ss -lnt
上面看到Send-Q 表示第三列的listen端口上的全連接隊(duì)列最大為32768喉悴,第一列Recv-Q為全連接隊(duì)列當(dāng)前使用了多少棱貌。
接著我們看看怎么設(shè)置全連接、半連接隊(duì)列大谢唷:
全連接隊(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)障贸。
在日常開(kāi)發(fā)中,我們往往使用servlet容器作為服務(wù)端吟宦,所以我們有時(shí)候也需要關(guān)注容器的連接隊(duì)列大小篮洁。在tomcat中backlog叫做acceptCount,在jetty里面則是acceptQueueSize殃姓。
3.RST異常
TCP有6種表示位: SYN(建立聯(lián)機(jī))?ACK(確認(rèn))?PSH(傳送)?FIN(結(jié)束)?RST(重置)?URG(緊急)
RST包表示連接重置袁波,用于關(guān)閉一些無(wú)用的連接瓦阐,通常表示異常關(guān)閉,區(qū)別于四次揮手篷牌。
在實(shí)際開(kāi)發(fā)中睡蟋,我們往往會(huì)看到connection reset / connection reset by peer錯(cuò)誤,這種情況就是RST包導(dǎo)致的枷颊。
那么有哪些原因可能導(dǎo)致RST呢戳杀?
目標(biāo)端口不存在
如果像對(duì)不存在的端口發(fā)出建立連接SYN請(qǐng)求,那么服務(wù)端發(fā)現(xiàn)自己并沒(méi)有這個(gè)端口則會(huì)直接返回一個(gè)RST報(bào)文夭苗,用于中斷連接信卡。
主動(dòng)用RST代替FIN終止連接
一般來(lái)說(shuō),正常的連接關(guān)閉都是需要通過(guò)FIN報(bào)文實(shí)現(xiàn)题造,然而我們也可以用RST報(bào)文來(lái)代替FIN坐求,表示直接終止連接。
實(shí)際開(kāi)發(fā)中晌梨,可設(shè)置SO_LINGER數(shù)值來(lái)控制桥嗤,這種往往是故意的,來(lái)跳過(guò)TIMED_WAIT仔蝌,提供交互效率泛领,不閑就慎用。
客戶端或服務(wù)端有一邊發(fā)生了異常敛惊,該方向?qū)Χ税l(fā)送RST以告知關(guān)閉連接渊鞋。
我們上面講的tcp隊(duì)列溢出發(fā)送RST包其實(shí)也是屬于這一種。這種往往是由于某些原因瞧挤,一方無(wú)法再能正常處理請(qǐng)求連接了(比如程序崩了锡宋,隊(duì)列滿了),從而告知另一方關(guān)閉連接特恬。
接收到的TCP報(bào)文不在已知的TCP連接內(nèi)
比如执俩,一方機(jī)器由于網(wǎng)絡(luò)實(shí)在太差TCP報(bào)文失蹤了,另一方關(guān)閉了該連接癌刽,然后過(guò)了許久收到了之前失蹤的TCP報(bào)文役首,但由于對(duì)應(yīng)的TCP連接已不存在,那么會(huì)直接發(fā)一個(gè)RST包以便開(kāi)啟新的連接显拜。
一方長(zhǎng)期未收到另一方的確認(rèn)報(bào)文衡奥,在一定時(shí)間或重傳次數(shù)后發(fā)出RST報(bào)文
這種大多也和網(wǎng)絡(luò)環(huán)境相關(guān)了,網(wǎng)絡(luò)環(huán)境差可能會(huì)導(dǎo)致更多的RST報(bào)文远荠。
RST報(bào)文多會(huì)導(dǎo)致程序報(bào)錯(cuò)矮固,在一個(gè)已關(guān)閉的連接上讀操作會(huì)報(bào)connection reset,而在一個(gè)已關(guān)閉的連接上寫操作則會(huì)報(bào)connection reset by peer譬淳。
通常我們可能還會(huì)看到broken pipe錯(cuò)誤档址,這是管道層面的錯(cuò)誤盹兢,表示對(duì)已關(guān)閉的管道進(jìn)行讀寫,往往是在收到RST辰晕,報(bào)出connection reset錯(cuò)后繼續(xù)讀寫數(shù)據(jù)報(bào)的錯(cuò),這個(gè)在glibc源碼注釋中也有介紹确虱。
我們?cè)谂挪楣收蠒r(shí)候怎么確定有RST包的存在呢含友?
當(dāng)然是使用tcpdump命令進(jìn)行抓包,并使用wireshark進(jìn)行簡(jiǎn)單分析了校辩。
tcpdump? ?-i en0? ?tcp? -w? xxx.cap窘问,en0表示監(jiān)聽(tīng)的網(wǎng)卡。
接下來(lái)我們通過(guò)wireshark打開(kāi)抓到的包宜咒,可能就能看到如下圖所示惠赫,紅色的就表示RST包了。
TIME_WAIT和CLOSE_WAIT
TIME_WAIT和CLOSE_WAIT是啥意思相信大家都知道故黑。
在線上時(shí)儿咱,我們可以直接用命令netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'來(lái)查看time-wait和close_wait的數(shù)量,用ss命令會(huì)更快ss -ant | awk '{++S[$1]} END {for(a in S) print a, S[a]}'
# netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
# ss -ant | awk '{++S[$1]} END {for(a in S) print a, S[a]}'
命令"netstat -na"查看到的相關(guān)TCP狀態(tài)解釋:
LISTEN:? ? ? ?監(jiān)聽(tīng)來(lái)自遠(yuǎn)方的TCP端口的連接請(qǐng)求;
SYN-SENT:???? 在發(fā)送連接請(qǐng)求后等待匹配的連接請(qǐng)求;
SYN-RECEIVED: 在收到和發(fā)送一個(gè)連接請(qǐng)求后等待對(duì)方對(duì)連接請(qǐng)求的確認(rèn);
ESTABLISHED:? 代表一個(gè)打開(kāi)的連接;
FIN-WAIT-1:?? 等待遠(yuǎn)程TCP連接中斷請(qǐng)求, 或先前的連接中斷請(qǐng)求的確認(rèn);
FIN-WAIT-2:?? 從遠(yuǎn)程TCP等待連接中斷請(qǐng)求;
CLOSE-WAIT:?? 等待從本地用戶發(fā)來(lái)的連接中斷請(qǐng)求;
LAST-ACK:???? 等待原來(lái)的發(fā)向遠(yuǎn)程TCP的連接中斷請(qǐng)求的確認(rèn);
TIME-WAIT:??? 等待足夠的時(shí)間以確保遠(yuǎn)程TCP接收到連接中斷請(qǐng)求的確認(rèn);
CLOSED:??????? 沒(méi)有任何連接狀態(tài);
下面簡(jiǎn)單解釋下什么是TIME-WAIT和CLOSE-WAIT ?
通常來(lái)說(shuō)要想解決問(wèn)題场晶,就要先理解問(wèn)題混埠。
有時(shí)遇到問(wèn)題,上網(wǎng)百度個(gè)解決方案,臨時(shí)修復(fù)了問(wèn)題,就以為問(wèn)題已經(jīng)不在了, 其實(shí)問(wèn)題不是真的不存在了,而是可能隱藏在更深的地方诗轻,只是我們沒(méi)有發(fā)現(xiàn)钳宪,或者以現(xiàn)有自己的的知識(shí)水平無(wú)法發(fā)現(xiàn)而已。
眾所周知扳炬,由于socket是全雙工的工作模式吏颖,一個(gè)socket的關(guān)閉,是需要四次握手來(lái)完成的:
1)?主動(dòng)關(guān)閉連接的一方恨樟,調(diào)用close()半醉;協(xié)議層發(fā)送FIN包,進(jìn)入入FIN_WAIT_1狀態(tài)?;
2)?被動(dòng)關(guān)閉的一方收到FIN包后劝术,協(xié)議層回復(fù)ACK奉呛;然后被動(dòng)關(guān)閉的一方,進(jìn)入CLOSE_WAIT狀態(tài)夯尽,主動(dòng)關(guān)閉的一方等待對(duì)方關(guān)閉瞧壮,則進(jìn)入FIN_WAIT_2狀態(tài);此時(shí)匙握,主動(dòng)關(guān)閉的一方等待被動(dòng)關(guān)閉一方的應(yīng)用程序調(diào)用close操作 ;
3)?被動(dòng)關(guān)閉的一方在完成所有數(shù)據(jù)發(fā)送后咆槽,調(diào)用close()操作;此時(shí)圈纺,協(xié)議層發(fā)送FIN包給主動(dòng)關(guān)閉的一方秦忿,等待對(duì)方的ACK麦射,被動(dòng)關(guān)閉的一方進(jìn)入LAST_ACK狀態(tài);
4)?主動(dòng)關(guān)閉的一方收到FIN包灯谣,協(xié)議層回復(fù)ACK潜秋;此時(shí),主動(dòng)關(guān)閉連接的一方胎许,進(jìn)入TIME_WAIT狀態(tài)峻呛;而被動(dòng)關(guān)閉的一方,進(jìn)入CLOSED狀態(tài) ;
5)?等待2MSL時(shí)間辜窑,主動(dòng)關(guān)閉的一方钩述,結(jié)束TIME_WAIT,進(jìn)入CLOSED狀態(tài) ;
通過(guò)上面的一次socket關(guān)閉操作穆碎,可以得出以下幾點(diǎn):
1)?主動(dòng)關(guān)閉連接的一方 – 也就是主動(dòng)調(diào)用socket的close操作的一方牙勘,最終會(huì)進(jìn)入TIME_WAIT狀態(tài) ;
2)?被動(dòng)關(guān)閉連接的一方,有一個(gè)中間狀態(tài)所禀,即CLOSE_WAIT方面,因?yàn)閰f(xié)議層在等待上層的應(yīng)用程序,主動(dòng)調(diào)用close操作后才主動(dòng)關(guān)閉這條連接 ;
3)?TIME_WAIT會(huì)默認(rèn)等待2MSL時(shí)間后色徘,才最終進(jìn)入CLOSED狀態(tài)葡幸;
4)?在一個(gè)連接沒(méi)有進(jìn)入CLOSED狀態(tài)之前,這個(gè)連接是不能被重用的贺氓!
所以說(shuō)這里憑直覺(jué)看蔚叨,TIME_WAIT并不可怕,CLOSE_WAIT才可怕辙培,因?yàn)镃LOSE_WAIT很多蔑水,表示說(shuō)要么是你的應(yīng)用程序?qū)懙挠袉?wèn)題,沒(méi)有合適的關(guān)閉socket扬蕊;
要么是說(shuō)搀别,你的服務(wù)器CPU處理不過(guò)來(lái)(CPU太忙)或者你的應(yīng)用程序一直睡眠到其它地方(鎖,或者文件I/O等等)尾抑,你的應(yīng)用程序獲得不到合適的調(diào)度時(shí)間歇父,造成你的程序沒(méi)法真正的執(zhí)行close操作。
那么這里又出現(xiàn)兩個(gè)問(wèn)題:
1)?上面提到的連接重用再愈,那連接到底是個(gè)什么概念榜苫?
2)?協(xié)議層為什么要設(shè)計(jì)一個(gè)TIME_WAIT狀態(tài)?這個(gè)狀態(tài)為什么默認(rèn)等待2MSL時(shí)間才會(huì)進(jìn)入CLOSED翎冲?
4分鐘就是2個(gè)MSL垂睬,每個(gè)MSL是2分鐘。MSL就是maximium segment lifetime——最長(zhǎng)報(bào)文壽命。
這個(gè)時(shí)間是由官方RFC協(xié)議規(guī)定的驹饺。至于為什么是2個(gè)MSL而不是1個(gè)MSL钳枕,我還沒(méi)有看到一個(gè)非常滿意的解釋。
有讀者提醒赏壹,RFC里建議的MSL其實(shí)是2分鐘鱼炒,但是很多實(shí)現(xiàn)都是30秒。
TIME_WAIT
time_wait的存在一是為了避免丟失的數(shù)據(jù)包被后面連接復(fù)用蝌借,二是為了在2MSL的時(shí)間范圍內(nèi)正常關(guān)閉連接昔瞧。
它的存在其實(shí)會(huì)大大減少RST包的出現(xiàn)。
過(guò)多的time_wait在短連接頻繁的場(chǎng)景比較容易出現(xiàn)骨望。這種情況可以在服務(wù)端做一些內(nèi)核參數(shù)調(diào)優(yōu):
#表示開(kāi)啟重用硬爆。允許將TIME-WAIT sockets重新用于新的TCP連接欣舵,默認(rèn)為0擎鸠,表示關(guān)閉
net.ipv4.tcp_tw_reuse = 1
#表示開(kāi)啟TCP連接中TIME-WAIT sockets的快速回收,默認(rèn)為0缘圈,表示關(guān)閉
net.ipv4.tcp_tw_recycle = 1
當(dāng)然我們不要忘記在NAT環(huán)境下因?yàn)闀r(shí)間戳錯(cuò)亂導(dǎo)致數(shù)據(jù)包被拒絕的坑了劣光,另外的辦法就是改小tcp_max_tw_buckets,超過(guò)這個(gè)數(shù)的time_wait都會(huì)被干掉糟把,不過(guò)這也會(huì)導(dǎo)致報(bào)time wait bucket table overflow的錯(cuò)绢涡。
CLOSE_WAIT
close_wait往往都是因?yàn)?b>應(yīng)用程序?qū)?/b>的有問(wèn)題,沒(méi)有在ACK后再次發(fā)起FIN報(bào)文遣疯。
close_wait出現(xiàn)的概率甚至比time_wait要更高雄可,后果也更嚴(yán)重。
往往是由于某個(gè)地方阻塞住了缠犀,沒(méi)有正常關(guān)閉連接数苫,從而漸漸地消耗完所有的線程。
想要定位這類問(wèn)題辨液,最好是通過(guò)jstack來(lái)分析線程堆棧來(lái)排查問(wèn)題虐急,具體可參考上述章節(jié)。
開(kāi)發(fā)同學(xué)說(shuō)應(yīng)用上線后CLOSE_WAIT就一直增多滔迈,直到掛掉為止止吁,jstack后找到比較可疑的堆棧是大部分線程都卡在了countdownlatch.await方法,找開(kāi)發(fā)同學(xué)了解后得知使用了多線程但是卻沒(méi)有catch異常燎悍,修改后發(fā)現(xiàn)異常僅僅是最簡(jiǎn)單的升級(jí)sdk后常出現(xiàn)的class not found敬惦。
三、參考
TCP的6種標(biāo)志位
https://blog.csdn.net/wuanwujie/article/details/71439551
JAVA線上故障排查全套路
https://fredal.xin/java-error-check
跟著動(dòng)畫(huà)來(lái)學(xué)習(xí)TCP三次握手和四次揮手
https://juejin.cn/post/6844903625513238541
TCP協(xié)議:三次握手過(guò)程詳解
https://baijiahao.baidu.com/s?id=1618114723935605183&wfr=spider&for=pc
計(jì)算機(jī)網(wǎng)絡(luò)基礎(chǔ)(三次握手|TCP/IP協(xié)議|五層協(xié)議棧|網(wǎng)絡(luò)安全)
https://blog.csdn.net/weixin_36474809/article/details/97539457
TCP 半連接隊(duì)列和全連接隊(duì)列滿了會(huì)發(fā)生什么谈山?又該如何應(yīng)對(duì)仁热?
https://blog.csdn.net/kexuanxiu1163/article/details/107148025
TCP連接的TIME_WAIT和CLOSE_WAIT 狀態(tài)解說(shuō)
https://www.cnblogs.com/kevingrace/p/9988354.html
記一次 TCP 全隊(duì)列溢出問(wèn)題排查過(guò)程
https://blog.csdn.net/weixin_43970890/article/details/111866688
TCP連接狀態(tài)與2MSL等待時(shí)間
https://www.cnblogs.com/mddblog/p/4565562.html
TCP協(xié)議詳解
https://www.cnblogs.com/qdhxhz/p/10267932.html
https://www.cnblogs.com/qdhxhz/p/8470997.html
再談 TCP 的 CLOSE_WAIT
https://www.easyice.cn/archives/320