Tcp長連接和keepalive

長連接與短連接

TCP 本身并沒有長短連接的區(qū)別 ,長短與否,完全取決于我們?cè)趺从盟?/p>

  • 短連接:每次通信時(shí),創(chuàng)建 Socket;一次通信結(jié)束,調(diào)用 socket.close()。這就是一般意義上的短連接,短連接的好處是管理起來比較簡單,存在的連接都是可用的連接,不需要額外的控制手段。
  • 長連接:每次通信完畢后,不會(huì)關(guān)閉連接,這樣可以做到連接的復(fù)用。 長連接的好處是省去了創(chuàng)建連接的耗時(shí)。

短連接和長連接的優(yōu)勢(shì),分別是對(duì)方的劣勢(shì)。想要圖簡單,不追求高性能,使用短連接合適,這樣我們就不需要操心連接狀態(tài)的管理;想要追求性能,使用長連接,我們就需要擔(dān)心各種問題:比如 端對(duì)端連接的維護(hù),連接的?;?。

長連接還常常被用來做數(shù)據(jù)的推送,我們大多數(shù)時(shí)候?qū)νㄐ诺恼J(rèn)知還是 request/response 模型,但 TCP 雙工通信的性質(zhì)決定了它還可以被用來做雙向通信。在長連接之下,可以很方便的實(shí)現(xiàn) push 模型。

服務(wù)治理框架中的長連接

追求性能時(shí),必然會(huì)選擇使用長連接,所以借助 Dubbo 可以很好的來理解 TCP。我們開啟兩個(gè) Dubbo 應(yīng)用,一個(gè) server 負(fù)責(zé)監(jiān)聽本地 20880 端口,一個(gè) client 負(fù)責(zé)循環(huán)發(fā)送請(qǐng)求。執(zhí)行 lsof -i:20880 命令可以查看端口的相關(guān)使用情況:


image.png
  • *:20880 (LISTEN) 說明了 Dubbo 正在監(jiān)聽本地的 20880 端口,處理發(fā)送到本地 20880 端口的請(qǐng)求
  • 后兩條信息說明請(qǐng)求的發(fā)送情況,驗(yàn)證了 TCP 是一個(gè)雙向的通信過程,由于我是在同一個(gè)機(jī)器開啟了兩個(gè) Dubbo 應(yīng)用,所以你能夠看到是本地的 53078 端口與 20880 端口在通信。我們并沒有手動(dòng)設(shè)置 53078 這個(gè)客戶端端口,它是隨機(jī)的。通過這兩條信息,闡釋了一個(gè)事實(shí): 即使是發(fā)送請(qǐng)求的一方,也需要占用一個(gè)端口 。
  • 稍微說一下 FD 這個(gè)參數(shù),他代表了 文件句柄 ,每新增一條連接都會(huì)占用新的文件句柄,如果你在使用 TCP 通信的過程中出現(xiàn)了 open too many files 的異常,那就應(yīng)該檢查一下,你是不是創(chuàng)建了太多連接,而沒有關(guān)閉。
長連接的維護(hù)

因?yàn)榭蛻舳苏?qǐng)求的服務(wù)可能分布在多個(gè)服務(wù)器上,客戶端自然需要跟對(duì)端創(chuàng)建多條長連接,我們遇到的第一個(gè)問題就是如何維護(hù)長連接。

// 客戶端
public class NettyHandler extends SimpleChannelHandler {

    private final Map<String, Channel> channels = new ConcurrentHashMap<String, Channel>(); // <ip:port, channel>
}
// 服務(wù)端
public class NettyServer extends AbstractServer implements Server {
    private Map<String, Channel> channels; // <ip:port, channel>
}

客戶端和服務(wù)端都使用 ip:port 維護(hù)了端對(duì)端的長連接,Channel 便是對(duì)連接的抽象.

解釋下為什么我認(rèn)為客戶端的連接集合要重要一點(diǎn)。TCP 是一個(gè)雙向通信的協(xié)議,任一方都可以是發(fā)送者,接受者,那為什么還抽象了 Client 和 Server 呢?因?yàn)?建立連接這件事就跟談念愛一樣,必須要有主動(dòng)的一方,你主動(dòng)我們就會(huì)有故事 。Client 可以理解為主動(dòng)建立連接的一方,實(shí)際上兩端的地位可以理解為是對(duì)等的。

連接的?;?/code>

為什么需要連接的?;??當(dāng)雙方已經(jīng)建立了連接,但因?yàn)榫W(wǎng)絡(luò)問題,鏈路不通,這樣長連接就不能使用了。需要明確的一點(diǎn)是,通過 netstat,lsof 等指令查看到連接的狀態(tài)處于 ESTABLISHED 狀態(tài)并不是一件非常靠譜的事,因?yàn)檫B接可能已死,但沒有被系統(tǒng)感知到,更不用提假死這種疑難雜癥了

連接的保活:KeepAlive

首先想到的是 TCP 中的 KeepAlive 機(jī)制。KeepAlive 并不是 TCP 協(xié)議的一部分,但是大多數(shù)操作系統(tǒng)都實(shí)現(xiàn)了這個(gè)機(jī)制(所以需要在操作系統(tǒng)層面設(shè)置 KeepAlive 的相關(guān)參數(shù))。KeepAlive 機(jī)制開啟后,在一定時(shí)間內(nèi)(一般時(shí)間為 7200s,參數(shù) tcp_keepalive_time)在鏈路上沒有數(shù)據(jù)傳送的情況下,TCP 層將發(fā)送相應(yīng)的 KeepAlive 探針以確定連接可用性,探測(cè)失敗后重試 10(參數(shù) tcp_keepalive_probes)次,每次間隔時(shí)間 75s(參數(shù) tcp_keepalive_intvl),所有探測(cè)失敗后,才認(rèn)為當(dāng)前連接已經(jīng)不可用。

在 Netty 中開啟 KeepAlive:

bootstrap.option(ChannelOption.SO_KEEPALIVE, true)

Linux 操作系統(tǒng)中設(shè)置 KeepAlive 相關(guān)參數(shù),修改 /etc/sysctl.conf 文件:

net.ipv4.tcp_keepalive_time=90
net.ipv4.tcp_keepalive_intvl=15
net.ipv4.tcp_keepalive_probes=2

KeepAlive 機(jī)制是在網(wǎng)絡(luò)層面保證了連接的可用性 ,但站在應(yīng)用框架層面我們認(rèn)為這還不夠。主要體現(xiàn)在三個(gè)方面:

  • KeepAlive 的開關(guān)是在應(yīng)用層開啟的,但是具體參數(shù)(如重試測(cè)試,重試間隔時(shí)間)的設(shè)置卻是操作系統(tǒng)級(jí)別的,位于操作系統(tǒng)的 /etc/sysctl.conf 配置中,這對(duì)于應(yīng)用來說不夠靈活。
  • KeepAlive 的?;顧C(jī)制只在鏈路空閑的情況下才會(huì)起到作用,假如此時(shí)有數(shù)據(jù)發(fā)送,且物理鏈路已經(jīng)不通,操作系統(tǒng)這邊的鏈路狀態(tài)還是 ESTABLISHED,這時(shí)會(huì)發(fā)生什么?自然會(huì)走 TCP 重傳機(jī)制,要知道默認(rèn)的 TCP 超時(shí)重傳,指數(shù)退避算法也是一個(gè)相當(dāng)長的過程。
  • KeepAlive 本身是面向網(wǎng)絡(luò)的,并不面向于應(yīng)用,當(dāng)連接不可用,可能是由于應(yīng)用本身的 GC 頻繁,系統(tǒng) load 高等情況,但網(wǎng)絡(luò)仍然是通的,此時(shí),應(yīng)用已經(jīng)失去了活性,連接應(yīng)該被認(rèn)為是不可用的。

我們已經(jīng)為應(yīng)用層面的連接?;钭隽俗銐虻匿亯|,下面就來一起看看,怎么在應(yīng)用層做連接?;?。
連接的?;睿簯?yīng)用層心跳

網(wǎng)絡(luò)層面的 KeepAlive 不足以支撐應(yīng)用級(jí)別的連接可用性,現(xiàn)在就來聊聊應(yīng)用層的心跳機(jī)制是實(shí)現(xiàn)連接?;畹?。

如何理解應(yīng)用層的心跳?簡單來說,就是客戶端會(huì)開啟一個(gè)定時(shí)任務(wù),定時(shí)對(duì)已經(jīng)建立連接的對(duì)端應(yīng)用發(fā)送請(qǐng)求(這里的請(qǐng)求是特殊的心跳請(qǐng)求),服務(wù)端則需要特殊處理該請(qǐng)求,返回響應(yīng)。如果心跳持續(xù)多次沒有收到響應(yīng),客戶端會(huì)認(rèn)為連接不可用,主動(dòng)斷開連接。不同的服務(wù)治理框架對(duì)心跳,建連,斷連,拉黑的機(jī)制有不同的策略,但大多數(shù)的服務(wù)治理框架都會(huì)在應(yīng)用層做心跳,Dubbo/HSF 也不例外。

應(yīng)用層心跳的設(shè)計(jì)細(xì)節(jié)

以 Dubbo 為例,支持應(yīng)用層的心跳,客戶端和服務(wù)端都會(huì)開啟一個(gè) HeartBeatTask,客戶端在 HeaderExchangeClient 中開啟,服務(wù)端將在 HeaderExchangeServer 開啟.
文章開頭埋了一個(gè)坑:Dubbo 為什么在服務(wù)端同時(shí)維護(hù) Map<String,Channel> 呢?主要就是為了給心跳做貢獻(xiàn),心跳定時(shí)任務(wù)在發(fā)現(xiàn)連接不可用時(shí),會(huì)根據(jù)當(dāng)前是客戶端還是服務(wù)端走不同的分支,客戶端發(fā)現(xiàn)不可用,是重連;服務(wù)端發(fā)現(xiàn)不可用,是直接 close。

// HeartBeatTask
if (channel instanceof Client) {
    ((Client) channel).reconnect();
} else {
    channel.close();
}

注意和 HTTP 的 KeepAlive 區(qū)別對(duì)待

HTTP 協(xié)議的 KeepAlive 意圖在于連接復(fù)用,同一個(gè)連接上串行方式傳遞請(qǐng)求 - 響應(yīng)數(shù)據(jù)
TCP 的 KeepAlive 機(jī)制意圖在于?;睢⑿奶?,檢測(cè)連接錯(cuò)誤。
這壓根是兩個(gè)概念。

KeepAlive 常見錯(cuò)誤
啟用 TCP KeepAlive 的應(yīng)用程序,一般可以捕獲到下面幾種類型錯(cuò)誤

  1. ETIMEOUT 超時(shí)錯(cuò)誤,在發(fā)送一個(gè)探測(cè)保護(hù)包經(jīng)過 (tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes) 時(shí)間后仍然沒有接收到 ACK 確認(rèn)情況下觸發(fā)的異常,套接字被關(guān)閉
java.io.IOException: Connection timed out

  1. 鏈接被重置,終端可能崩潰死機(jī)重啟之后,接收到來自服務(wù)器的報(bào)文,然物是人非,前朝往事,只能報(bào)以無奈重置宣告之。
java.io.IOException: Connection reset by peer

有三種使用 KeepAlive 的實(shí)踐方案:

  • 默認(rèn)情況下使用 KeepAlive 周期為 2 個(gè)小時(shí),如不選擇更改,屬于誤用范疇,造成資源浪費(fèi):內(nèi)核會(huì)為每一個(gè)連接都打開一個(gè)?;钣?jì)時(shí)器,N 個(gè)連接會(huì)打開 N 個(gè)保活計(jì)時(shí)器,優(yōu)勢(shì)很明顯:
  1. TCP 協(xié)議層面?;钐綔y(cè)機(jī)制,系統(tǒng)內(nèi)核完全替上層應(yīng)用自動(dòng)給做好了
  2. 內(nèi)核層面計(jì)時(shí)器相比上層應(yīng)用,更為高效
  3. 上層應(yīng)用只需要處理數(shù)據(jù)收發(fā)、連接異常通知即可
  4. 數(shù)據(jù)包將更為緊湊
  • 關(guān)閉 TCP 的 KeepAlive,完全使用應(yīng)用層心跳?;顧C(jī)制。由應(yīng)用掌管心跳,更靈活可控,比如可以在應(yīng)用級(jí)別設(shè)置心跳周期,適配私有協(xié)議。
  • 業(yè)務(wù)心跳 + TCP KeepAlive 一起使用,互相作為補(bǔ)充,但 TCP ?;钐綔y(cè)周期和應(yīng)用的心跳周期要協(xié)調(diào),以互補(bǔ)方可,不能夠差距過大,否則將達(dá)不到設(shè)想的效果。
    各個(gè)框架的設(shè)計(jì)都有所不同,例如 Dubbo 使用的是方案三.

http長連接

一次http請(qǐng)求,誰會(huì)先斷開TCP連接?

我們有2臺(tái)內(nèi)部http服務(wù)(nginx)

201:這臺(tái)服務(wù)器部署的服務(wù)是account.api.91160.com,這個(gè)服務(wù)是供前端頁面調(diào)用;
202:這臺(tái)服務(wù)器部署的服務(wù)是hdbs.api.91160.com, 這個(gè)服務(wù)是供前端頁面調(diào)用;

近期發(fā)現(xiàn),這2臺(tái)服務(wù)器的網(wǎng)絡(luò)連接中,TIME_WAIT 數(shù)量差別很大,201的TIME_WAIT大概20000+,202的TIME_WAIT大概1000 ,差距20倍;2臺(tái)的請(qǐng)求量差不多,都是以上內(nèi)部調(diào)用的連接,且服務(wù)模式也沒有什么差異,為什么連接數(shù)會(huì)差這么大?

是因?yàn)檫@2個(gè)模塊的調(diào)用程序由不同團(tuán)隊(duì)寫的,調(diào)用方式不一樣,導(dǎo)致一個(gè)是調(diào)用方(客戶端,PHP程序)主動(dòng)斷開連接,一個(gè)是被調(diào)用方(服務(wù)端 201、202)主動(dòng)斷開連接;因TIME_WAIT 產(chǎn)生在主動(dòng)斷開連接的一方,因此導(dǎo)致一臺(tái)服務(wù)器TIME_WAIT 數(shù)高,一臺(tái)TIME_WAIT 數(shù)低;

這就有個(gè)細(xì)節(jié),一次http請(qǐng)求,誰會(huì)先斷開TCP連接?什么情況下客戶端先斷,什么情況下服務(wù)端先斷?

主要有http1.0和http1.1之間保持連接的差異以及http頭中connection、content-length、Transfer-encoding等參數(shù)有關(guān);在nginx中,對(duì)于http1.0與http1.1也是支持長連接的。什么是長連接呢?我們知道,http請(qǐng)求是基于TCP協(xié)議之上的,那么,當(dāng)客戶端在發(fā)起請(qǐng)求前,需要先與服務(wù)端建立TCP連接,而每一次的TCP連接是需要三次握手來確定的,如果客戶端與服務(wù)端之間網(wǎng)絡(luò)差一點(diǎn),這三次交互消費(fèi)的時(shí)間會(huì)比較多,而且三次交互也會(huì)帶來網(wǎng)絡(luò)流量。當(dāng)然,當(dāng)連接斷開后,也會(huì)有四次的交互,當(dāng)然對(duì)用戶體驗(yàn)來說就不重要了。

而http請(qǐng)求是請(qǐng)求應(yīng)答式的,如果我們能知道每個(gè)請(qǐng)求頭與響應(yīng)體的長度,那么我們是可以在一個(gè)連接上面執(zhí)行多個(gè)請(qǐng)求的,這就是所謂的長連接,但前提條件是我們先得確定請(qǐng)求頭與響應(yīng)體的長度。對(duì)于請(qǐng)求來說,如果當(dāng)前請(qǐng)求需要有body,如POST請(qǐng)求,那么nginx就需要客戶端在請(qǐng)求頭中指定content-length來表明body的大小,否則返回400錯(cuò)誤。也就是說,請(qǐng)求體的長度是確定的,那么響應(yīng)體的長度呢?先來看看http協(xié)議中關(guān)于響應(yīng)body長度的確定

  1. 對(duì)于http1.0協(xié)議來說,如果響應(yīng)頭中有content-length頭,則以content-length的長度就可以知道body的長度了,客戶端在接收body時(shí),就可以依照這個(gè)長度來接收數(shù)據(jù),接收完后,就表示這個(gè)請(qǐng)求完成了。而如果沒有content-length頭,則客戶端會(huì)一直接收數(shù)據(jù),直到服務(wù)端主動(dòng)斷開連接,才表示body接收完了。

2.而對(duì)于http1.1協(xié)議來說,如果響應(yīng)頭中的Transfer-encoding為chunked傳輸,則表示body是流式輸出,body會(huì)被分成多個(gè)塊,每塊的開始會(huì)標(biāo)識(shí)出當(dāng)前塊的長度,此時(shí),body不需要通過長度來指定。如果是非chunked傳輸,而且有content-length,則按照content-length來接收數(shù)據(jù)。否則,如果是非chunked,并且沒有content-length,則客戶端接收數(shù)據(jù),直到服務(wù)端主動(dòng)斷開連接。

從上面,我們可以看到,除了http1.0不帶content-length以及http1.1非chunked不帶content-length外,body的長度是可知的。此時(shí),當(dāng)服務(wù)端在輸出完body之后,會(huì)可以考慮使用長連接。能否使用長連接,也是有條件限制的。如果客戶端的請(qǐng)求頭中的connection為close,則表示客戶端需要關(guān)掉長連接,如果為keep-alive,則客戶端需要打開長連接,如果客戶端的請(qǐng)求中沒有connection這個(gè)頭,那么根據(jù)協(xié)議,如果是http1.0,則默認(rèn)為close,如果是http1.1,則默認(rèn)為keep-alive。如果結(jié)果為keepalive,那么,nginx在輸出完響應(yīng)體后,會(huì)設(shè)置當(dāng)前連接的keepalive屬性,然后等待客戶端下一次請(qǐng)求。當(dāng)然,nginx不可能一直等待下去,如果客戶端一直不發(fā)數(shù)據(jù)過來,豈不是一直占用這個(gè)連接?所以當(dāng)nginx設(shè)置了keepalive等待下一次的請(qǐng)求時(shí),同時(shí)也會(huì)設(shè)置一個(gè)最大等待時(shí)間,這個(gè)時(shí)間是通過選項(xiàng)keepalive_timeout來配置的,如果配置為0,則表示關(guān)掉keepalive,此時(shí),http版本無論是1.1還是1.0,客戶端的connection不管是close還是keepalive,都會(huì)強(qiáng)制為close。

如果服務(wù)端最后的決定是keepalive打開,那么在響應(yīng)的http頭里面,也會(huì)包含有connection頭域,其值是”Keep-Alive”,否則就是”Close”。如果connection值為close,那么在nginx響應(yīng)完數(shù)據(jù)后,會(huì)主動(dòng)關(guān)掉連接。所以,對(duì)于請(qǐng)求量比較大的nginx來說,關(guān)掉keepalive最后會(huì)產(chǎn)生比較多的time-wait狀態(tài)的socket。一般來說,當(dāng)客戶端的一次訪問,需要多次訪問同一個(gè)server時(shí),打開keepalive的優(yōu)勢(shì)非常大,比如圖片服務(wù)器,通常一個(gè)網(wǎng)頁會(huì)包含很多個(gè)圖片。打開keepalive也會(huì)大量減少time-wait的數(shù)量。

http1.0
帶content-length,body長度可知,客戶端在接收body時(shí),就可以依據(jù)這個(gè)長度來接受數(shù)據(jù)。接受完畢后,就表示這個(gè)請(qǐng)求完畢了??蛻舳酥鲃?dòng)調(diào)用close進(jìn)入四次揮手。

不帶content-length ,body長度不可知,客戶端一直接受數(shù)據(jù),直到服務(wù)端主動(dòng)斷開

http1.1
帶content-length body長度可知 客戶端主動(dòng)斷開

帶Transfer-encoding:chunked body會(huì)被分成多個(gè)塊,每塊的開始會(huì)標(biāo)識(shí)出當(dāng)前塊的長度,body就不需要通過content-length來指定了。但依然可以知道body的長度 客戶端主動(dòng)斷開

不帶Transfer-encoding:chunked且不帶content-length 客戶端接收數(shù)據(jù),直到服務(wù)端主動(dòng)斷開連接。

如果能夠有辦法知道服務(wù)器傳來的長度,都是客戶端首先斷開。如果不知道就一直接收數(shù)據(jù)。直到服務(wù)端斷開

最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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