TCP 已經(jīng)實(shí)現(xiàn) KeepAlive, 為什么應(yīng)用層還要實(shí)現(xiàn)一遍?
TCP 心跳
TCP Keepalive 是一種用于檢測(cè) TCP 連接是否活躍的機(jī)制,通過定期發(fā)送探測(cè)數(shù)據(jù)包來確定連接的狀態(tài),主要用于檢測(cè)空閑 (僵尸) 連接、保持 NAT 映射 (NAT 設(shè)備、防火墻設(shè)備) 等。
1.原理簡(jiǎn)述
- 要啟用 TCP Keepalive 自動(dòng)檢測(cè)機(jī)制,需要通信雙方都開啟 Keepalive 選項(xiàng)
- 如果在一定時(shí)間(默認(rèn) 2 小時(shí))內(nèi)沒有數(shù)據(jù)傳輸,TCP 會(huì)發(fā)送一個(gè) Keepalive 探測(cè)數(shù)據(jù)包
- 如果通信的對(duì)方仍然活躍,就會(huì)對(duì)該探測(cè)數(shù)據(jù)包進(jìn)行響應(yīng),如果對(duì)方?jīng)]有響應(yīng),TCP 將重試發(fā)送探測(cè)數(shù)據(jù)包
- 在達(dá)到最大重試次數(shù)(默認(rèn) 10 次)后,如果仍然未收到響應(yīng),TCP 將認(rèn)為連接已斷開,關(guān)閉連接
下面是根據(jù) TCP Keepalive 工作原理,轉(zhuǎn)換后的邏輯偽代碼 (針對(duì)單個(gè) TCP 連接)。
# TCP Keepalive 控制參數(shù)
KEEPALIVE_INTERVAL = 7200 # 默認(rèn) 2 小時(shí)
KEEPALIVE_PROBES = 10 # 默認(rèn) 10 次
KEEPALIVE_TIMEOUT = 75 # 默認(rèn) 75 秒
# 開啟 TCP Keepalive 機(jī)制
# 初始化各項(xiàng)控制參數(shù)
def enable_keepalive(socket):
socket.setsockopt(..., socket.SO_KEEPALIVE, 1)
socket.setsockopt(..., KEEPALIVE_INTERVAL)
socket.setsockopt(..., KEEPALIVE_PROBES)
socket.setsockopt(... KEEPALIVE_TIMEOUT)
# 如果連接在 Keepalive 間隔時(shí)間內(nèi)處于空閑狀態(tài)
# 發(fā)送 Keepalive 探測(cè)包并啟動(dòng)探測(cè)計(jì)時(shí)器
def send_keepalive_probe(socket):
if is_idle(socket, KEEPALIVE_INTERVAL):
send_probe_packet(socket)
start_probe_timer(socket)
# 處理 Keepalive 響應(yīng)
# 如果收到探測(cè)包的 ACK 確認(rèn),則重置空閑計(jì)時(shí)器
# 否則增加探測(cè)次數(shù)
# 如果超過最大探測(cè)次數(shù),則關(guān)閉連接
# Function to handle keepalive response
def handle_keepalive_response(socket):
if received_probe_ack(socket):
reset_idle_timer(socket)
else:
increment_probe_count(socket)
if probe_count(socket) > KEEPALIVE_PROBES:
close_connection(socket)
# 檢查Keepalive超時(shí)
# 如果探測(cè)計(jì)時(shí)器過期,則處理 Keepalive 響應(yīng)
def check_keepalive_timeout(socket):
if probe_timer_expired(socket):
handle_keepalive_response(socket)
# 核心主循環(huán)管理 Keepalive
# 1. 啟用 Keepalive 選項(xiàng)
# 2. 在連接打開時(shí)定期發(fā)送探測(cè)包和檢查超時(shí)
# Main loop to manage keepalive
def manage_keepalive(socket):
enable_keepalive(socket)
while is_open(socket):
send_keepalive_probe(socket)
check_keepalive_timeout(socket)
time.sleep(KEEPALIVE_TIMEOUT)
...
...
2.相關(guān)參數(shù)
Linux 內(nèi)核中和 TCP Keepalive 機(jī)制相關(guān)的幾個(gè)參數(shù)如下:
- tcp_keepalive_time:首次探測(cè)之前的空閑時(shí)間(默認(rèn) 2 小時(shí))
- tcp_keepalive_intvl:重試探測(cè)的時(shí)間間隔(默認(rèn) 75 秒)
- tcp_keepalive_probes:最大重試次數(shù)(默認(rèn) 10 次)
當(dāng)然,這些參數(shù)都可以通過修改系統(tǒng)配置文件進(jìn)行修改,尤其在優(yōu)化高并發(fā)場(chǎng)景和移動(dòng)場(chǎng)景為主的后端服務(wù)器時(shí),這幾個(gè)參數(shù)需要著重優(yōu)化一下:
# 設(shè)置首次探測(cè)之前的空閑時(shí)間為 10 分鐘
echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time
# 設(shè)置重試探測(cè)的時(shí)間間隔為 15 秒
echo 15 > /proc/sys/net/ipv4/tcp_keepalive_intvl
# 設(shè)置最大重試次數(shù)為 3 次
echo 3 > /proc/sys/net/ipv4/tcp_keepalive_probes
運(yùn)行 sysctl -p 命令生效,重啟之后仍然有效。
3.局限性
TCP Keepalive 機(jī)制由內(nèi)核 (操作系統(tǒng)) 負(fù)責(zé)執(zhí)行,當(dāng)進(jìn)程退出后,內(nèi)核會(huì)針對(duì)進(jìn)程中未關(guān)閉的連接逐個(gè)進(jìn)行關(guān)閉 (向連接的通信對(duì)方發(fā)送 FIN 報(bào)文),這樣就保證了每個(gè)連接的通信雙方都可以知道通信的狀態(tài),并根據(jù)狀態(tài)來完成不同的具體業(yè)務(wù)邏輯。
表面上看,不論進(jìn)程是運(yùn)行還是退出,TCP Keepalive 機(jī)制都可以通過內(nèi)核很好地完成,但是在一些極端場(chǎng)景中,內(nèi)核無法保證 TCP 協(xié)議棧正常工作,例如:
- 操作系統(tǒng)異常導(dǎo)致重啟,TCP 協(xié)議棧沒有機(jī)會(huì)發(fā)送 FIN 報(bào)文
- 服務(wù)器硬件故障、基礎(chǔ)設(shè)置故障 (如斷電、斷網(wǎng)、地理不可抗力因素),TCP 協(xié)議棧同樣沒有機(jī)會(huì)發(fā)送 FIN 報(bào)文
- 海量并發(fā)連接數(shù),操作系統(tǒng)或進(jìn)程重啟時(shí),TCP 協(xié)議??赡軣o法斷開所有連接,也就是 FIN 報(bào)文出現(xiàn)丟包后,沒有更多的時(shí)間進(jìn)行重試
- 網(wǎng)絡(luò)鏈路故障,只能等到 TCP Keepalive 檢測(cè)超時(shí),通信雙方才能確認(rèn)這種情況,此時(shí)距離發(fā)生故障可能已經(jīng)過去了一段時(shí)間
應(yīng)用層心跳
1.必要性
前文中講到了 TCP Keepalive 機(jī)制 (內(nèi)核實(shí)現(xiàn)) 的局限性,除此之外,結(jié)合到應(yīng)用層一起來看的話,TCP Keepalive 機(jī)制無法確認(rèn)應(yīng)用層的心跳檢測(cè)目標(biāo):應(yīng)用程序還在正常工作。具體來說,TCP Keepalive 檢測(cè)結(jié)果正常,只能說明兩件事情:
- 應(yīng)用程序 (進(jìn)程) 還存在
- 網(wǎng)絡(luò)鏈路正常
但是 當(dāng)應(yīng)用程序進(jìn)程運(yùn)行中發(fā)生異常時(shí),例如死鎖、Bug 導(dǎo)致的無限循環(huán)、無限阻塞 等,雖然此時(shí)操作系統(tǒng)依然可以正常執(zhí)行 TCP Keepalive 機(jī)制,但是對(duì)于應(yīng)用程序的異常情況,通信對(duì)方是無法得知的。
此外,應(yīng)用層心跳檢測(cè)具有更好的靈活性,例如可以控制檢測(cè)時(shí)間、間隔、異常處理機(jī)制、附加額外數(shù)據(jù)等。
綜上所述,應(yīng)用層心跳檢測(cè)是必須實(shí)現(xiàn)的。
2.實(shí)現(xiàn)方式
常見的應(yīng)用層心跳實(shí)現(xiàn)方式有:
- HTTP: 訪問指定 URL, 根據(jù)響應(yīng)碼或者響應(yīng)數(shù)據(jù)來判定應(yīng)用是否正常
- Exec: 執(zhí)行指定 (Shell) 命令 (例如文件檢查、網(wǎng)絡(luò)檢查),并檢查命令的退出狀態(tài)碼,如果狀態(tài)碼為 0,說明應(yīng)用正常運(yùn)行
- WebSocket: 和 HTTP 檢測(cè)方式類似
- 其他自定義檢測(cè)方式
其中業(yè)界主流的檢測(cè)方式是 HTTP (長(zhǎng)連接方式), 主要是因?yàn)?
- HTTP 實(shí)現(xiàn)簡(jiǎn)單,基于長(zhǎng)連接的方式避免了連接的建立和釋放帶來的開銷
- HTTP 對(duì)于 (異構(gòu)) 環(huán)境的要求很低,而且大多數(shù)應(yīng)用中都使用 HTTP 作為 API 主要通信協(xié)議,心跳檢測(cè)并不會(huì)帶來多少額外的工作量
3.實(shí)現(xiàn)細(xì)節(jié)
(1) 不要單獨(dú)實(shí)現(xiàn) “心跳線程”
使用單獨(dú)的線程來實(shí)現(xiàn) “心跳檢測(cè)”,雖然可以將心跳檢測(cè)應(yīng)用代碼和具體的業(yè)務(wù)邏輯代碼隔離,但是當(dāng) “業(yè)務(wù)線程” 發(fā)生死鎖或者 Bug 崩潰時(shí),心跳線程檢測(cè)不到。
所以應(yīng)該將心跳檢測(cè)直接實(shí)現(xiàn)在 “業(yè)務(wù)線程” 中。
(2) 不要單獨(dú)實(shí)現(xiàn) “心跳連接”
對(duì)于網(wǎng)絡(luò) (例如 TCP) 編程的場(chǎng)景,心跳檢測(cè)應(yīng)該在 “業(yè)務(wù)連接” 直接實(shí)現(xiàn),而不是使用單獨(dú)的連接,這樣當(dāng)業(yè)務(wù)連接出現(xiàn)異常時(shí),通信對(duì)方可以第一時(shí)間感知到 (沒有及時(shí)收到心跳響應(yīng))。
此外,大多數(shù)網(wǎng)絡(luò)防火墻會(huì)定時(shí)監(jiān)測(cè)空閑 (僵尸) 連接并清除,如果心跳檢測(cè)使用額外的連接,那么當(dāng) “業(yè)務(wù)連接” 長(zhǎng)時(shí)間沒有要發(fā)送的數(shù)據(jù)時(shí),就已經(jīng)被防火墻斷開了,但是此時(shí)心跳檢測(cè)連接還在正常工作,這會(huì)影響通信對(duì)方的判斷,以為 “業(yè)務(wù)連接” 還在正常工作。
所以應(yīng)該將心跳檢測(cè)直接實(shí)現(xiàn)在 “業(yè)務(wù)連接” 中。