記一次TCP TIME_WAIT引發(fā)的血案
前言
記錄線上一次故障,狀態(tài)延遲,狀態(tài)使用短連接,長(zhǎng)輪訓(xùn)的方式獲取,在每天的固定時(shí)間點(diǎn),出現(xiàn)狀態(tài)延遲,持續(xù)幾分鐘,然后又莫名其妙的恢復(fù)了,很是怪異,下面就來復(fù)盤下,這次問題的定位和思考。

冰山一角
我們可以掌握的線索有 1.固定的時(shí)間點(diǎn),發(fā)生。 2.通過監(jiān)控可以看到,流量并不高,但是TCP TIMEWAIT一瞬間瘋漲 3.出問題的時(shí)間點(diǎn),遠(yuǎn)程客戶的電腦(全內(nèi)網(wǎng)),ping網(wǎng)關(guān)和服務(wù)器,發(fā)現(xiàn)有大量延時(shí) 4.有同事通過jstat看,發(fā)現(xiàn)GC的次數(shù)很多,認(rèn)為GC導(dǎo)致了接口延時(shí)。 5.接口超時(shí)的時(shí)間點(diǎn),CPU不高,內(nèi)存不高,I/O不高,系統(tǒng)負(fù)載不高,也就是未達(dá)到機(jī)器的性能瓶頸。機(jī)器配置16核64g
軟件版本:
?操作系統(tǒng) centos7.9?JDK 1.6?nginx7.7

由于,這個(gè)問題牽涉的大客戶,很多技術(shù)人員,投入進(jìn)來一起攻破,每個(gè)人的想法不一致,導(dǎo)致問題更難以統(tǒng)一突破。
一部分人,認(rèn)為是網(wǎng)絡(luò)問題,和某為扯了半天,沒有個(gè)所以然 一部分人,認(rèn)為GC太頻繁,full GC次數(shù)太多了,所以需要GC調(diào)優(yōu) 一部分人,認(rèn)為需要在測(cè)試環(huán)境,壓測(cè),測(cè)試出系統(tǒng)瓶頸
實(shí)施的方案
拋開以上的言論,我們回到線索上 1、每天固定時(shí)間出現(xiàn),詢問客戶,這個(gè)時(shí)間段是否是業(yè)務(wù)高峰期。 回答:是 2、從監(jiān)控來看,TCP-TIMEWAIT高,那我們就要先定位到系統(tǒng)部署方案。
系統(tǒng)部署方案如下 1.nginx反向代理java服務(wù)。 nginx監(jiān)聽443端口 java服務(wù)監(jiān)聽8080端口
每一次輪訓(xùn),發(fā)起http請(qǐng)求,通過nginx,nginx請(qǐng)求java服務(wù)
查看8080端口的tcp端口timewait個(gè)數(shù),發(fā)現(xiàn)全是timewait,現(xiàn)在可以確定的是nginx和tcp連接出現(xiàn)了大量的timewait,導(dǎo)致接口延時(shí)。
翻了運(yùn)維的調(diào)優(yōu)記錄如下:
?根據(jù)監(jiān)控看,丟棄的連接數(shù)比較多
修改后| 當(dāng)前情況| 說明| |--|--|--| | net.core.netdev_max_backlog = 16384 |net.core.netdev_max_backlog = 1000 |系統(tǒng)建立全鏈接隊(duì)列長(zhǎng)度(TCP三次握手成功后的連接放入隊(duì)列) | | net.ipv4.tcp_max_syn_backlog = 8192|net.ipv4.tcp_max_syn_backlog = 512 |系統(tǒng)建立半鏈接隊(duì)列長(zhǎng)度(TCP三次握手過程中的連接放入隊(duì)列) | | net.core.somaxconn = 4096 |net.core.somaxconn = 128 |系統(tǒng)建立全鏈接隊(duì)列(TCP三次握手成功后的連接放入隊(duì)列 系統(tǒng)級(jí)別的 |
TCP 建立連接時(shí)要經(jīng)過 3 次握手,在客戶端向服務(wù)器發(fā)起連接時(shí),對(duì)于服務(wù)器而言,一個(gè)完整的連接建立過程,服務(wù)器會(huì)經(jīng)歷 2 種 TCP 狀態(tài):SYN_REVD, ESTABELLISHED
對(duì)應(yīng)也會(huì)維護(hù)兩個(gè)隊(duì)列:
一個(gè)存放 SYN 的隊(duì)列(半連接隊(duì)列) 一個(gè)存放已經(jīng)完成連接的隊(duì)列(全連接隊(duì)列)
?ESTABLISHED列長(zhǎng)度如何計(jì)算?
如果 backlog 大于內(nèi)核參數(shù) net.core.somaxconn,則以 net.core.somaxconn 為準(zhǔn),
即全連接隊(duì)列長(zhǎng)度 = min(backlog, 內(nèi)核參數(shù) net.core.somaxconn),net.core.somaxconn 默認(rèn)為 128。
這個(gè)很好理解,net.core.somaxconn 定義了系統(tǒng)級(jí)別的全連接隊(duì)列最大長(zhǎng)度,
backlog 只是應(yīng)用層傳入的參數(shù),不可能超過內(nèi)核參數(shù),所以 backlog 必須小于等于 net.core.somaxconn。
?SYN_RECV隊(duì)列長(zhǎng)度如何計(jì)算?
半連接隊(duì)列長(zhǎng)度由內(nèi)核參數(shù) tcp_max_syn_backlog 決定,
當(dāng)使用 SYN Cookie 時(shí)(就是內(nèi)核參數(shù) net.ipv4.tcp_syncookies = 1),這個(gè)參數(shù)無效,
半連接隊(duì)列的最大長(zhǎng)度為 backlog、內(nèi)核參數(shù) net.core.somaxconn、內(nèi)核參數(shù) tcp_max_syn_backlog 的最小值。
即半連接隊(duì)列長(zhǎng)度 = min(backlog, 內(nèi)核參數(shù) net.core.somaxconn,內(nèi)核參數(shù) tcp_max_syn_backlog)。
這個(gè)公式實(shí)際上規(guī)定半連接隊(duì)列長(zhǎng)度不能超過全連接隊(duì)列長(zhǎng)度,但是tcp_syncooking默認(rèn)是啟用的,如果按上文的理解,那這個(gè)參數(shù)設(shè)置沒有多大意義
其實(shí),對(duì)于 Nginx/Tomcat 等這種 Web 服務(wù)器,都提供了 backlog 參數(shù)設(shè)置入口,當(dāng)然它們都會(huì)有默認(rèn)值,通常這個(gè)默認(rèn)值都不會(huì)太大(包括內(nèi)核默認(rèn)的半連接隊(duì)列和全連接隊(duì)列長(zhǎng)度)。如果應(yīng)用并發(fā)訪問非常高,只增大應(yīng)用層 backlog 是沒有意義的,因?yàn)榭赡軆?nèi)核參數(shù)關(guān)于連接隊(duì)列設(shè)置的都很小,一定要綜合應(yīng)用層 backlog 和內(nèi)核參數(shù)一起看,通過公式很容易調(diào)整出正確的設(shè)置
?nginx調(diào)優(yōu) nginx到j(luò)ava建立的連接Timewait比較大,優(yōu)化nginx配置,降低握手次數(shù)。
?從以上記錄看,似乎都和TCP連接數(shù)有關(guān)。 解決思路如下:
1.長(zhǎng)輪訓(xùn) 短連接改為websocket方案 評(píng)估:不確定因素太多,客戶要限時(shí)解決問題,不敢上線 2.降低tcp 連接數(shù),根據(jù)業(yè)務(wù)拆分,降低請(qǐng)求量。 3.控制timewait數(shù)量,保證業(yè)務(wù)高峰期不會(huì)發(fā)生太大的抖動(dòng) 4.詢問業(yè)務(wù)人員,請(qǐng)求會(huì)分配到一個(gè)線程的處理,并且沒有限制線程數(shù)量,所以如果請(qǐng)求量很大,應(yīng)該會(huì)有很多線程,并且cpu應(yīng)該有很高的使用量,而現(xiàn)在cpu的使用量很低,不太正常。使用jstack打印線程情況
實(shí)施
?
找到運(yùn)維溝通,tcp的連接數(shù)設(shè)置高點(diǎn)。然而被告知最多只能創(chuàng)建65535個(gè)。這個(gè)問題我們稍后在細(xì)聊下。
?
控制TCP TIMEWAIT的數(shù)量,設(shè)置參數(shù)
?jstack打印線程池,發(fā)現(xiàn)有很多線程在等待同一個(gè)鎖。 鎖是使用synchronized關(guān)鍵字,鎖的是一個(gè)數(shù)組,鎖的代碼邏輯,處理比較長(zhǎng)。 優(yōu)化:拆分鎖的邏輯,不需要一致性的數(shù)據(jù)踢出去。
后記
一臺(tái)機(jī)器最多能創(chuàng)建多少個(gè)TCP連接?
上面的截圖中,tcp timewait的個(gè)數(shù)太多,到了65535限制,導(dǎo)致連接被重置,那為什么有這個(gè)結(jié)論呢 注意看上面的nginx配置
這里指定了去連接127.0.0.1的8080端口,那么nginx去連接java服務(wù)的時(shí)候如下: | 源ip| 源端口 |目標(biāo)ip |目標(biāo)端口 | |--|--|--|--| | 127.0.0.1|20000 |127.0.0.1 |8080 | | 127.0.0.1|20001 |127.0.0.1 |8080 | | 127.0.0.1|20002 |127.0.0.1 |8080 |
所以為什么會(huì)說一個(gè)機(jī)器上最多65535個(gè)端口數(shù)。 這個(gè)端口號(hào)16位的,可以有0~65535端口數(shù)是針對(duì)單個(gè)ip來描述的,那我們?cè)跈C(jī)器上可不是只有一個(gè)ip,所以理論上端口數(shù)是無上限的,配置nginx的時(shí)候這里需要注意上。
?端口號(hào)的上限不一定是65535 如果你有幸見過cannot assign requested address?那么你一定知道 Linux對(duì)可使用的范圍端口有具體限制,使用以下命令查看
?文件描述符 Linux 下一切皆文件 我們突破tcp端口限制的時(shí)候,很可能會(huì)遇到以下錯(cuò)誤too many open files
每建立一個(gè)TCP連接,會(huì)分配一個(gè)文件描述符,linux 對(duì)可打開的文件描述符的數(shù)量分別作了三個(gè)方面的限制。
?C10K 每創(chuàng)建一個(gè)TCP連接,操作系統(tǒng)都需要消耗一個(gè)線程,當(dāng)我們TCP連接數(shù)過多,會(huì)導(dǎo)致線程不停的上下文切換,導(dǎo)致cpu處理時(shí)間越來越長(zhǎng)。 C10K就是早期單機(jī)性能的瓶頸代名詞。所以后續(xù)有了 I/O多路復(fù)用模型。
何時(shí)進(jìn)行JVM調(diào)優(yōu)?
還記得上面說過,通過jstat看到GC很頻繁,full GC次數(shù)很多,所以建議GC調(diào)優(yōu)嗎?

我們看關(guān)鍵指標(biāo)
我們可以看到GC的次數(shù)很多,但是GC的平均時(shí)間并不高。
但是這并不是萬能的
有一種情況比如Full GC發(fā)生了10次,平均值不高,但是某幾次的Full GC時(shí)間為5~6秒,所以為了更確定這個(gè)問題,我們可以使用啟動(dòng)參數(shù),查看每次GC的時(shí)間。

查看當(dāng)天GC日志,F(xiàn)ull GC時(shí)間并不高,可以排除,GC引起的問題。
所有的優(yōu)化,GC調(diào)優(yōu)應(yīng)該是最后的手段,更多的是優(yōu)化我們的代碼。