您好,登錄后才能下訂單哦!
這篇文章給大家介紹如何理解TCP中keepalive和time_wait,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。
TCP是一個(gè)有狀態(tài)通訊協(xié)議,所謂的有狀態(tài)是指通信過(guò)程中通信的雙方各自維護(hù)連接的狀態(tài)。
先簡(jiǎn)單回顧一下TCP連接建立和斷開的整個(gè)過(guò)程。(這里主要考慮主流程,關(guān)于丟包、擁塞、窗口、失敗重試等情況后面詳細(xì)討論。)
首先是客戶端發(fā)送syn(Synchronize Sequence Numbers:同步序列編號(hào))包給服務(wù)端,告訴服務(wù)端我要連接你,syn包里面主要攜帶了客戶端的seq序列號(hào);服務(wù)端回發(fā)一個(gè)syn+ack,其中syn包和客戶端原理類似,只不過(guò)攜帶的是服務(wù)端的seq序列號(hào),ack包則是確認(rèn)客戶端允許連接;最后客戶端再次發(fā)送一個(gè)ack確認(rèn)接收到服務(wù)端的syn包。這樣客戶端和服務(wù)端就可以建立連接了。整個(gè)流程稱為三次握手。
建立連接后,客戶端或者服務(wù)端便可以通過(guò)已建立的socket連接發(fā)送數(shù)據(jù),對(duì)端接收數(shù)據(jù)后,便可以通過(guò)ack確認(rèn)已經(jīng)收到數(shù)據(jù)。
數(shù)據(jù)交換完畢后,通常是客戶端便可以發(fā)送FIN包,告訴另一端我要斷開了;另一端先通過(guò)ack確認(rèn)收到FIN包,然后發(fā)送FIN包告訴客戶端我也關(guān)閉了;最后客戶端回應(yīng)ack確認(rèn)連接終止。整個(gè)流程成為四次揮手。
TCP的性能經(jīng)常為大家所詬病,除了TCP+IP額外的header以外,它建立連接需要三次握手,關(guān)閉連接需要四次揮手。如果只是發(fā)送很少的數(shù)據(jù),那么傳輸?shù)挠行?shù)據(jù)是非常少的。
是不是建立一次連接后續(xù)可以繼續(xù)復(fù)用呢?的確可以這樣做,但這又帶來(lái)另一個(gè)問(wèn)題,如果連接一直不釋放,端口被占滿了咋辦。為此引入了今天討論的第一個(gè)話題TCP keepalive。所謂的TCP keepalive是指TCP連接建立后會(huì)通過(guò)keepalive的方式一直保持,不會(huì)在數(shù)據(jù)傳輸完成后立刻中斷,而是通過(guò)keepalive機(jī)制檢測(cè)連接狀態(tài)。
Linux控制keepalive有三個(gè)參數(shù):?;顣r(shí)間net.ipv4.tcp_keepalive_time、?;顣r(shí)間間隔net.ipv4.tcp_keepalive_intvl、?;钐綔y(cè)次數(shù)net.ipv4.tcp_keepalive_probes,默認(rèn)值分別是 7200 秒(2 小時(shí))、75 秒和 9 次探測(cè)。如果使用 TCP 自身的 keep-Alive 機(jī)制,在 Linux 系統(tǒng)中,最少需要經(jīng)過(guò) 2 小時(shí) + 9*75 秒后斷開。譬如我們SSH登錄一臺(tái)服務(wù)器后可以看到這個(gè)TCP的keepalive時(shí)間是2個(gè)小時(shí),并且會(huì)在2個(gè)小時(shí)后發(fā)送探測(cè)包,確認(rèn)對(duì)端是否處于連接狀態(tài)。
之所以會(huì)討論TCP的keepalive,是因?yàn)榘l(fā)現(xiàn)服器上有泄露的TCP連接:
# ll /proc/11516/fd/10 lrwx------ 1 root root 64 Jan 3 19:04 /proc/11516/fd/10 -> socket:[1241854730] # date Sun Jan 5 17:39:51 CST 2020
已經(jīng)建立連接兩天,但是對(duì)方已經(jīng)斷開了(非正常斷開)。由于使用了比較老的go(1.9之前版本有問(wèn)題)導(dǎo)致連接沒(méi)有釋放。
解決這類問(wèn)題,可以借助TCP的keepalive機(jī)制。新版go語(yǔ)言支持在建立連接的時(shí)候設(shè)置keepalive時(shí)間。首先查看網(wǎng)絡(luò)包中建立TCP連接的DialContext方法中
if tc, ok := c.(*TCPConn); ok && d.KeepAlive >= 0 { setKeepAlive(tc.fd, true) ka := d.KeepAlive if d.KeepAlive == 0 { ka = defaultTCPKeepAlive } setKeepAlivePeriod(tc.fd, ka) testHookSetKeepAlive(ka) }
其中defaultTCPKeepAlive是15s。如果是HTTP連接,使用默認(rèn)client,那么它會(huì)將keepalive時(shí)間設(shè)置成30s。
var DefaultTransport RoundTripper = &Transport{ Proxy: ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, }
下面通過(guò)一個(gè)簡(jiǎn)單的demo測(cè)試一下,代碼如下:
func main() { wg := &sync.WaitGroup{} c := http.DefaultClient for i := 0; i < 2; i++ { wg.Add(1) go func() { defer wg.Done() for { r, err := c.Get("http://10.143.135.95:8080") if err != nil { fmt.Println(err) return } _, err = ioutil.ReadAll(r.Body) r.Body.Close() if err != nil { fmt.Println(err) return } time.Sleep(30 * time.Millisecond) } }() } wg.Wait() }
執(zhí)行程序后,可以查看連接。初始設(shè)置keepalive為30s。
然后不斷遞減,至0后,又會(huì)重新獲取30s。
整個(gè)過(guò)程可以通過(guò)tcpdump抓包獲取。
# tcpdump -i bond0 port 35832 -nvv -A
其實(shí)很多應(yīng)用并非是通過(guò)TCP的keepalive機(jī)制探活的,因?yàn)槟J(rèn)的兩個(gè)多小時(shí)檢查時(shí)間對(duì)于很多實(shí)時(shí)系統(tǒng)是完全沒(méi)法滿足的,通常的做法是通過(guò)應(yīng)用層的定時(shí)監(jiān)測(cè),如PING-PONG機(jī)制(就像打乒乓球,一來(lái)一回),應(yīng)用層每隔一段時(shí)間發(fā)送心跳包,如websocket的ping-pong。
第二個(gè)希望和大家分享的話題是TCP的Time_wait狀態(tài)。、
為啥需要time_wait狀態(tài)呢?為啥不直接進(jìn)入closed狀態(tài)呢?直接進(jìn)入closed狀態(tài)能更快地釋放資源給新的連接使用了,而不是還需要等待2MSL(Linux默認(rèn))時(shí)間。
有兩個(gè)原因:
一是為了防止“迷路的數(shù)據(jù)包”,如下圖所示,如果在第一個(gè)連接里第三個(gè)數(shù)據(jù)包由于底層網(wǎng)絡(luò)故障延遲送達(dá)。等待新的連接建立后,這個(gè)遲到的數(shù)據(jù)包才到達(dá),那么將會(huì)導(dǎo)致接收數(shù)據(jù)紊亂。
第二個(gè)原因則更加簡(jiǎn)單,如果因?yàn)樽詈笠粋€(gè)ack丟失,那么對(duì)方將一直處于last ack狀態(tài),如果此時(shí)重新發(fā)起新的連接,對(duì)方將返回RST包拒絕請(qǐng)求,將會(huì)導(dǎo)致無(wú)法建立新連接。
為此設(shè)計(jì)了time_wait狀態(tài)。在高并發(fā)情況下,如果能將time_wait的TCP復(fù)用, time_wait復(fù)用是指可以將處于time_wait狀態(tài)的連接重復(fù)利用起來(lái)。從time_wait轉(zhuǎn)化為established,繼續(xù)復(fù)用。Linux內(nèi)核通過(guò)net.ipv4.tcp_tw_reuse參數(shù)控制是否開啟time_wait狀態(tài)復(fù)用。
讀者可能很好奇,之前不是說(shuō)time_wait設(shè)計(jì)之初是為了解決上面兩個(gè)問(wèn)題的嗎?如果直接復(fù)用不是反而會(huì)導(dǎo)致上面兩個(gè)問(wèn)題出現(xiàn)嗎?這里先介紹Linux默認(rèn)開啟的一個(gè)TCP時(shí)間戳策略net.ipv4.tcp_timestamps = 1。
時(shí)間戳開啟后,針對(duì)第一個(gè)迷路數(shù)據(jù)包的問(wèn)題,由于晚到數(shù)據(jù)包的時(shí)間戳過(guò)早會(huì)被直接丟棄,不會(huì)導(dǎo)致新連接數(shù)據(jù)包紊亂;針對(duì)第二個(gè)問(wèn)題,開啟reuse后,當(dāng)對(duì)方處于last-ack狀態(tài)時(shí),發(fā)送syn包會(huì)返回FIN,ACK包,然后客戶端發(fā)送RST讓服務(wù)端關(guān)閉請(qǐng)求,從而客戶端可以再次發(fā)送syn建立新的連接。
最后還需要提醒讀者的是,Linux 4.1內(nèi)核版本之前除了tcp_tw_reuse以外,還有一個(gè)參數(shù)tcp_tw_recycle,這個(gè)參數(shù)就是強(qiáng)制回收time_wait狀態(tài)的連接,它會(huì)導(dǎo)致NAT環(huán)境丟包,所以不建議開啟。
關(guān)于如何理解TCP中keepalive和time_wait就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。