1 前言
可能很多 Java 程序員對(duì) TCP 的理解只有一個(gè)三次握手,四次揮手的認(rèn)識(shí),我覺(jué)得這樣的原因主要在于 TCP 協(xié)議本身稍微有點(diǎn)抽象(相比較于應(yīng)用層的 HTTP 協(xié)議);其次,非框架開(kāi)發(fā)者不太需要接觸到 TCP 的一些細(xì)節(jié)。其實(shí)我個(gè)人對(duì) TCP 的很多細(xì)節(jié)也并沒(méi)有完全理解,這篇文章主要針對(duì)微信交流群里有人提出的長(zhǎng)連接,心跳的問(wèn)題,做一個(gè)統(tǒng)一的整理。
在 Java 中,使用 TCP 通信,大概率會(huì)涉及到 Socket、Netty,本文會(huì)借用它們的一些 API 和設(shè)置參數(shù)來(lái)輔助介紹。
2 長(zhǎng)連接與短連接
TCP 本身并沒(méi)有長(zhǎng)短連接的區(qū)別,長(zhǎng)短與否,完全取決于我們?cè)趺从盟?/p>
短連接:每次通信時(shí),創(chuàng)建 Socket;一次通信結(jié)束,調(diào)用 socket.close()。這就是一般意義上的短連接,短連接的好處是管理起來(lái)比較簡(jiǎn)單,存在的連接都是可用的連接,不需要額外的控制手段。
長(zhǎng)連接:每次通信完畢后,不會(huì)關(guān)閉連接,這樣就可以做到連接的復(fù)用。長(zhǎng)連接的好處便是省去了創(chuàng)建連接的耗時(shí)。
短連接和長(zhǎng)連接的優(yōu)勢(shì),分別是對(duì)方的劣勢(shì)。想要圖簡(jiǎn)單,不追求高性能,使用短連接合適,這樣我們就不需要操心連接狀態(tài)的管理;想要追求性能,使用長(zhǎng)連接,我們就需要擔(dān)心各種問(wèn)題:比如端對(duì)端連接的維護(hù),連接的保活。
長(zhǎng)連接還常常被用來(lái)做數(shù)據(jù)的推送,我們大多數(shù)時(shí)候?qū)νㄐ诺恼J(rèn)知還是 request/response 模型,但 TCP 雙工通信的性質(zhì)決定了它還可以被用來(lái)做雙向通信。在長(zhǎng)連接之下,可以很方便的實(shí)現(xiàn) push 模型。
短連接沒(méi)有太多東西可以講,所以下文我們將目光聚焦在長(zhǎng)連接的一些問(wèn)題上。純講理論未免有些過(guò)于單調(diào),所以下文我借助 Dubbo 這個(gè) RPC 框架的一些實(shí)踐來(lái)展開(kāi) TCP 的相關(guān)討論。
3 服務(wù)治理框架中的長(zhǎng)連接
前面已經(jīng)提到過(guò),追求性能的時(shí)候,必然會(huì)選擇使用長(zhǎng)連接,所以借助 Dubbo 可以很好的來(lái)理解 TCP。我們開(kāi)啟兩個(gè) Dubbo 應(yīng)用,一個(gè) server 負(fù)責(zé)監(jiān)聽(tīng)本地 20880(眾所周知,這是 Dubbo 協(xié)議默認(rèn)的端口),一個(gè) client 負(fù)責(zé)循環(huán)發(fā)送請(qǐng)求。執(zhí)行 lsof-i:20880 命令可以查看端口的相關(guān)使用情況:
*:20880(LISTEN) 說(shuō)明了 Dubbo 正在監(jiān)聽(tīng)本地的 20880 端口,處理發(fā)送到本地 20880 端口的請(qǐng)求。
后兩條信息說(shuō)明請(qǐng)求的發(fā)送情況,驗(yàn)證了 TCP 是一個(gè)雙向的通信過(guò)程,由于我是在同一個(gè)機(jī)器開(kāi)啟了兩個(gè) Dubbo 應(yīng)用,所以你能夠看到是本地的 53078 端口與 20880 端口在通信。我們并沒(méi)有手動(dòng)設(shè)置 53078 這個(gè)客戶(hù)端端口,他是隨機(jī)的,但也闡釋了一個(gè)道理:即使是發(fā)送請(qǐng)求的一方,也需要占用一個(gè)端口。
稍微說(shuō)一下 FD 這個(gè)參數(shù),他代表了文件句柄,每新增一條連接都會(huì)占用新的文件句柄,如果你在使用 TCP 通信的過(guò)程中出現(xiàn)了 open too many files 的異常,那就應(yīng)該檢查一下,你是不是創(chuàng)建了太多的連接,而沒(méi)有關(guān)閉。細(xì)心的讀者也會(huì)聯(lián)想到長(zhǎng)連接的另一個(gè)好處,那就是會(huì)占用較少的文件句柄。
4 長(zhǎng)連接的維護(hù)
因?yàn)榭蛻?hù)端請(qǐng)求的服務(wù)可能分布在多個(gè)服務(wù)器上,客戶(hù)端端自然需要跟對(duì)端創(chuàng)建多條長(zhǎng)連接,使用長(zhǎng)連接,我們遇到的第一個(gè)問(wèn)題就是要如何維護(hù)長(zhǎng)連接。
//客戶(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》
}
在 Dubbo 中,客戶(hù)端和服務(wù)端都使用 ip:port 維護(hù)了端對(duì)端的長(zhǎng)連接,Channel 便是對(duì)連接的抽象。我們主要關(guān)注 NettyHandler 中的長(zhǎng)連接,服務(wù)端同時(shí)維護(hù)一個(gè)長(zhǎng)連接的集合是 Dubbo 的設(shè)計(jì),我們將在后面提到。
5 連接的?;?/strong>
這個(gè)話題就有的聊了,會(huì)牽扯到比較多的知識(shí)點(diǎn)。首先需要明確一點(diǎn),為什么需要連接的報(bào)活?當(dāng)雙方已經(jīng)建立了連接,但因?yàn)?a href="http://ttokpm.com/v/tag/1722/" target="_blank">網(wǎng)絡(luò)問(wèn)題,鏈路不通,這樣長(zhǎng)連接就不能使用了。需要明確的一點(diǎn)是,通過(guò) netstat,lsof 等指令查看到連接的狀態(tài)處于 ESTABLISHED 狀態(tài)并不是一件非??孔V的事,因?yàn)檫B接可能已死,但沒(méi)有被系統(tǒng)感知到,更不用提假死這種疑難雜癥了。如果保證長(zhǎng)連接可用是一件技術(shù)活。
6 連接的保活:KeepAlive
首先想到的是 TCP 中的 KeepAlive 機(jī)制。KeepAlive 并不是 TCP 協(xié)議的一部分,但是大多數(shù)操作系統(tǒng)都實(shí)現(xiàn)了這個(gè)機(jī)制。KeepAlive 機(jī)制開(kāi)啟后,在一定時(shí)間內(nèi)(一般時(shí)間為 7200s,參數(shù) tcp_keepalive_time)在鏈路上沒(méi)有數(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 中開(kāi)啟 KeepAlive:
bootstrap.option(ChannelOption.TCP_NODELAY,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 的開(kāi)關(guān)是在應(yīng)用層開(kāi)啟的,但是具體參數(shù)(如重試測(cè)試,重試間隔時(shí)間)的設(shè)置卻是操作系統(tǒng)級(jí)別的,位于操作系統(tǒng)的 /etc/sysctl.conf 配置中,這對(duì)于應(yīng)用來(lái)說(shuō)不夠靈活。
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)長(zhǎng)的過(guò)程。
KeepAlive 本身是面向網(wǎng)絡(luò)的,并不是面向于應(yīng)用的,當(dāng)連接不可用時(shí),可能是由于應(yīng)用本身 GC 問(wèn)題,系統(tǒng) load 高等情況,但網(wǎng)絡(luò)仍然是通的,此時(shí),應(yīng)用已經(jīng)失去了活性,所以連接自然應(yīng)該認(rèn)為是不可用的。
看來(lái),應(yīng)用層面的連接?;钸€是必須要做的。
7 連接的保活:應(yīng)用層心跳
終于點(diǎn)題了,文題中提到的心跳便是一個(gè)本文想要重點(diǎn)強(qiáng)調(diào)的另一個(gè) TCP 相關(guān)的知識(shí)點(diǎn)。上一節(jié)我們已經(jīng)解釋過(guò)了,網(wǎng)絡(luò)層面的 KeepAlive 不足以支撐應(yīng)用級(jí)別的連接可用性,本節(jié)就來(lái)聊聊應(yīng)用層的心跳機(jī)制是實(shí)現(xiàn)連接保活的。
如何理解應(yīng)用層的心跳?簡(jiǎn)單來(lái)說(shuō),就是客戶(hù)端會(huì)開(kāi)啟一個(gè)定時(shí)任務(wù),定時(shí)對(duì)已經(jīng)建立連接的對(duì)端應(yīng)用發(fā)送請(qǐng)求(這里的請(qǐng)求是特殊的心跳請(qǐng)求),服務(wù)端則需要特殊處理該請(qǐng)求,返回響應(yīng)。如果心跳持續(xù)多次沒(méi)有收到響應(yīng),客戶(hù)端會(huì)認(rèn)為連接不可用,主動(dòng)斷開(kāi)連接。不同的服務(wù)治理框架對(duì)心跳,建連,斷連,拉黑的機(jī)制有不同的策略,但大多數(shù)的服務(wù)治理框架都會(huì)在應(yīng)用層做心跳,Dubbo 也不例外。
8 應(yīng)用層心跳的設(shè)計(jì)細(xì)節(jié)
以 Dubbo 為例,支持應(yīng)用層的心跳,客戶(hù)端和服務(wù)端都會(huì)開(kāi)啟一個(gè) HeartBeatTask,客戶(hù)端在 HeaderExchangeClient 中開(kāi)啟,服務(wù)端將在 HeaderExchangeServer 開(kāi)啟。文章開(kāi)頭埋了一個(gè)坑:Dubbo 為什么在服務(wù)端同時(shí)維護(hù) Map
// HeartBeatTask if (channel instanceof Client) {
((Client) channel).reconnect();
} else { channel.close();
}
熟悉其他 RPC 框架的同學(xué)會(huì)發(fā)現(xiàn),不同框架的心跳機(jī)制真的是差距非常大。心跳設(shè)計(jì)還跟連接創(chuàng)建,重連機(jī)制,黑名單連接相關(guān),還需要具體框架具體分析。
除了定時(shí)任務(wù)的設(shè)計(jì),還需要在協(xié)議層面支持心跳。最簡(jiǎn)單的例子可以參考 nginx 的健康檢查,而針對(duì) Dubbo 協(xié)議,自然也需要做心跳的支持,如果將心跳請(qǐng)求識(shí)別為正常流量,會(huì)造成服務(wù)端的壓力問(wèn)題,干擾限流等諸多問(wèn)題。
其中 Flag 代表了 Dubbo 協(xié)議的標(biāo)志位,一共 8 個(gè)地址位。低四位用來(lái)表示消息體數(shù)據(jù)用的序列化工具的類(lèi)型(默認(rèn) hessian),高四位中,第一位為1表示是 request 請(qǐng)求,第二位為 1 表示雙向傳輸(即有返回response),第三位為 1 表示是心跳事件。
心跳請(qǐng)求應(yīng)當(dāng)和普通請(qǐng)求區(qū)別對(duì)待。
9 注意和 HTTP 的 KeepAlive 區(qū)別對(duì)待
HTTP 協(xié)議的 KeepAlive 意圖在于連接復(fù)用,同一個(gè)連接上串行方式傳遞請(qǐng)求-響應(yīng)數(shù)據(jù)
TCP 的 KeepAlive 機(jī)制意圖在于?;睢⑿奶?,檢測(cè)連接錯(cuò)誤。
這壓根是兩個(gè)概念。
10 KeepAlive 常見(jiàn)異常
啟用 TCP KeepAlive 的應(yīng)用程序,一般可以捕獲到下面幾種類(lèi)型錯(cuò)誤
ETIMEOUT 超時(shí)錯(cuò)誤,在發(fā)送一個(gè)探測(cè)保護(hù)包經(jīng)過(guò) (tcpkeepalivetime + tcpkeepaliveintvl * tcpkeepaliveprobes)時(shí)間后仍然沒(méi)有接收到 ACK 確認(rèn)情況下觸發(fā)的異常,套接字被關(guān)閉 java java.io.IOException:Connectiontimedout
EHOSTUNREACH host unreachable(主機(jī)不可達(dá))錯(cuò)誤,這個(gè)應(yīng)該是 ICMP 匯報(bào)給上層應(yīng)用的。 java java.io.IOException:Noroute to host
鏈接被重置,終端可能崩潰死機(jī)重啟之后,接收到來(lái)自服務(wù)器的報(bào)文,然物是人非,前朝往事,只能報(bào)以無(wú)奈重置宣告之。 java java.io.IOException:Connectionresetbypeer
11 總結(jié)
有三種使用 KeepAlive 的實(shí)踐方案:
1.默認(rèn)情況下使用 KeepAlive 周期為 2 個(gè)小時(shí),如不選擇更改,屬于誤用范疇,造成資源浪費(fèi):內(nèi)核會(huì)為每一個(gè)連接都打開(kāi)一個(gè)?;钣?jì)時(shí)器,N 個(gè)連接會(huì)打開(kāi) N 個(gè)?;钣?jì)時(shí)器。 優(yōu)勢(shì)很明顯:
TCP 協(xié)議層面?;钐綔y(cè)機(jī)制,系統(tǒng)內(nèi)核完全替上層應(yīng)用自動(dòng)給做好了
內(nèi)核層面計(jì)時(shí)器相比上層應(yīng)用,更為高效
上層應(yīng)用只需要處理數(shù)據(jù)收發(fā)、連接異常通知即可
數(shù)據(jù)包將更為緊湊
2.關(guān)閉 TCP 的 KeepAlive,完全使用應(yīng)用層心跳?;顧C(jī)制。由應(yīng)用掌管心跳,更靈活可控,比如可以在應(yīng)用級(jí)別設(shè)置心跳周期,適配私有協(xié)議。
3.業(yè)務(wù)心跳 + TCP KeepAlive 一起使用,互相作為補(bǔ)充,但 TCP 保活探測(cè)周期和應(yīng)用的心跳周期要協(xié)調(diào),以互補(bǔ)方可,不能夠差距過(guò)大,否則將達(dá)不到設(shè)想的效果。
各個(gè)框架的設(shè)計(jì)都有所不同,例如 Dubbo 使用的是方案三,但阿里內(nèi)部的 HSF 框架則沒(méi)有設(shè)置 TCP 的 KeepAlive,僅僅由應(yīng)用心跳?;?。和心跳策略一樣,這和框架整體的設(shè)計(jì)相關(guān)。
-
JAVA
+關(guān)注
關(guān)注
19文章
2952瀏覽量
104479 -
API
+關(guān)注
關(guān)注
2文章
1472瀏覽量
61749 -
TCP
+關(guān)注
關(guān)注
8文章
1347瀏覽量
78933
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論