隨手記之TCP Keepalive筆記
零。前言
TCP是無感知的虛擬連接,中間斷開兩端不會(huì)立刻得到通知。一般在使用長連接的環(huán)境下,需要心跳保活機(jī)制可以勉強(qiáng)感知其存活。業(yè)務(wù)層面有心跳機(jī)制,TCP協(xié)議也提供了心跳保活機(jī)制。
一。TCP Keepalive解讀
長連接的環(huán)境下,人們一般使用業(yè)務(wù)層面或上層應(yīng)用層協(xié)議(諸如MQTT,SOCKET.IO等)里面定義和使用。一旦有熱數(shù)據(jù)需要傳遞,若此時(shí)連接已經(jīng)被中介設(shè)備斷開,應(yīng)用程序沒有及時(shí)感知的話,那么就會(huì)導(dǎo)致在一個(gè)無效的數(shù)據(jù)鏈路層面發(fā)送業(yè)務(wù)數(shù)據(jù),結(jié)果就是發(fā)送失敗。
無論是因?yàn)榭蛻舳艘馔鈹嚯姟⑺罊C(jī)、崩潰、重啟,還是中間路由網(wǎng)絡(luò)無故斷開、NAT超時(shí)等,服務(wù)器端要做到快速感知失敗,減少無效鏈接操作。
1. 交互過程
2. 協(xié)議解讀
下面協(xié)議解讀,基于RFC1122#TCP Keep-Alives。
- TCP Keepalive雖不是標(biāo)準(zhǔn)規(guī)范,但操作系統(tǒng)一旦實(shí)現(xiàn),默認(rèn)情況下須為關(guān)閉,可以被上層應(yīng)用開啟和關(guān)閉。
- TCP Keepalive必須在沒有任何數(shù)據(jù)(包括ACK包)接收之后的周期內(nèi)才會(huì)被發(fā)送,允許配置,默認(rèn)值不能夠小于2個(gè)小時(shí)
- 不包含數(shù)據(jù)的ACK段在被TCP發(fā)送時(shí)沒有可靠性保證,意即一旦發(fā)送,不確保一定發(fā)送成功。系統(tǒng)實(shí)現(xiàn)不能對任何特定探針包作死連接對待
- 規(guī)范建議keepalive保活包不應(yīng)該包含數(shù)據(jù),但也可以包含1個(gè)無意義的字節(jié),比如0x0。
- SEG.SEQ = SND.NXT-1,即TCP保活探測報(bào)文序列號將前一個(gè)TCP報(bào)文序列號減1。SND.NXT = RCV.NXT,即下一次發(fā)送正常報(bào)文序號等于ACK序列號;總之保活報(bào)文不在窗口控制范圍內(nèi) 有一張圖,可以很容易說明,但請仔細(xì)觀察Tcp Keepalive部分:
- 不太好的TCP堆棧實(shí)現(xiàn),可能會(huì)要求保活報(bào)文必須攜帶有1個(gè)字節(jié)的數(shù)據(jù)負(fù)載
- TCP Keepalive應(yīng)該在服務(wù)器端啟用,客戶端不做任何改動(dòng);若單獨(dú)在客戶端啟用,若客戶端異常崩潰或出現(xiàn)連接故障,存在服務(wù)器無限期的為已打開的但已失效的文件描述符消耗資源的嚴(yán)重問題。但在特殊的NFS文件系統(tǒng)環(huán)境下,需要客戶端和服務(wù)器端都要啟用Tcp Keepalive機(jī)制。
- TCP Keepalive不是TCP規(guī)范的一部分,有三點(diǎn)需要注意:
- 在短暫的故障期間,它們可能引起一個(gè)良好連接(good connection)被釋放(dropped)
- 它們消費(fèi)了不必要的寬帶
- 在以數(shù)據(jù)包計(jì)費(fèi)的互聯(lián)網(wǎng)消費(fèi)(額外)花費(fèi)金錢
二。Tcp keepalive 如何使用
以下環(huán)境是在Linux服務(wù)器上進(jìn)行。應(yīng)用程序若想使用,需要設(shè)置SO_KEEPALIVE套接口選項(xiàng)才能夠生效。
1. 系統(tǒng)內(nèi)核參數(shù)配置
- tcp_keepalive_time,在TCP保活打開的情況下,最后一次數(shù)據(jù)交換到TCP發(fā)送第一個(gè)保活探測包的間隔,即允許的持續(xù)空閑時(shí)長,或者說每次正常發(fā)送心跳的周期,默認(rèn)值為7200s(2h)。
- tcp_keepalive_probes 在tcp_keepalive_time之后,沒有接收到對方確認(rèn),繼續(xù)發(fā)送保活探測包次數(shù),默認(rèn)值為9(次)
- tcp_keepalive_intvl,在tcp_keepalive_time之后,沒有接收到對方確認(rèn),繼續(xù)發(fā)送保活探測包的發(fā)送頻率,默認(rèn)值為75s。
發(fā)送頻率tcp_keepalive_intvl乘以發(fā)送次數(shù)tcp_keepalive_probes,就得到了從開始探測到放棄探測確定連接斷開的時(shí)間
若設(shè)置,服務(wù)器在客戶端連接空閑的時(shí)候,每90秒發(fā)送一次保活探測包到客戶端,若沒有及時(shí)收到客戶端的TCP Keepalive ACK確認(rèn),將繼續(xù)等待15秒*2=30秒。總之可以在90s+30s=120秒(兩分鐘)時(shí)間內(nèi)可檢測到連接失效與否。
以下改動(dòng),需要寫入到/etc/sysctl.conf文件:
net.ipv4.tcp_keepalive_time=90
net.ipv4.tcp_keepalive_intvl=15
net.ipv4.tcp_keepalive_probes=2
保存退出,然后執(zhí)行sysctl -p
生效。可通過 sysctl -a | grep keepalive
命令檢測一下是否已經(jīng)生效。
針對已經(jīng)設(shè)置SO_KEEPALIVE的套接字,應(yīng)用程序不用重啟,內(nèi)核直接生效。
2. Java/netty服務(wù)器如何使用
只需要在服務(wù)器端一方設(shè)置即可,客戶端完全不用設(shè)置,比如基于netty 4服務(wù)器程序:
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new EchoServerHandler());
}
});
// Start the server.
ChannelFuture f = b.bind(port).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
Java程序只能做到設(shè)置SO_KEEPALIVE選項(xiàng),至于TCP_KEEPCNT,TCP_KEEPIDLE,TCP_KEEPINTVL等參數(shù)配置,只能依賴于sysctl配置,系統(tǒng)進(jìn)行讀取。
3. C語言如何設(shè)置
下面代碼摘取自libkeepalive源碼,C語言可以設(shè)置更為詳細(xì)的TCP內(nèi)核參數(shù)。
int socket(int domain, int type, int protocol)
{
int (*libc_socket)(int, int, int);
int s, optval;
char *env;
*(void **)(&libc_socket) = dlsym(RTLD_NEXT, "socket");
if(dlerror()) {
errno = EACCES;
return -1;
}
if((s = (*libc_socket)(domain, type, protocol)) != -1) {
if((domain == PF_INET) && (type == SOCK_STREAM)) {
if(!(env = getenv("KEEPALIVE")) || strcasecmp(env, "off")) {
optval = 1;
} else {
optval = 0;
}
if(!(env = getenv("KEEPALIVE")) || strcasecmp(env, "skip")) {
setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));
}
#ifdef TCP_KEEPCNT
if((env = getenv("KEEPCNT")) && ((optval = atoi(env)) >= 0)) {
setsockopt(s, SOL_TCP, TCP_KEEPCNT, &optval, sizeof(optval));
}
#endif
#ifdef TCP_KEEPIDLE
if((env = getenv("KEEPIDLE")) && ((optval = atoi(env)) >= 0)) {
setsockopt(s, SOL_TCP, TCP_KEEPIDLE, &optval, sizeof(optval));
}
#endif
#ifdef TCP_KEEPINTVL
if((env = getenv("KEEPINTVL")) && ((optval = atoi(env)) >= 0)) {
setsockopt(s, SOL_TCP, TCP_KEEPINTVL, &optval, sizeof(optval));
}
#endif
}
}
return s;
}
4. 針對已有程序沒有硬編碼KTTCP EEPALIVE實(shí)現(xiàn)
完全可以借助于第三方工具libkeepalive,通過LD_PRELOAD方式實(shí)現(xiàn)。比如
LD_PRELOAD=/the/path/libkeepalive.so java -jar /your/path/yourapp.jar &
這個(gè)工具還有一個(gè)比較方便的地方,可以直接在程序運(yùn)行前指定TCP保活詳細(xì)參數(shù),可以省去配置sysctl.conf的麻煩:
LD_PRELOAD=/the/path/libkeepalive.so \
> KEEPCNT=20 \
> KEEPIDLE=180 \
> KEEPINTVL=60 \
> java -jar /your/path/yourapp.jar &
針對較老很久不更新的程序,可以嘗試一下嘛。
三。Linux內(nèi)核層面對keepalive處理
參數(shù)和定義
#define MAX_TCP_KEEPIDLE 32767
#define MAX_TCP_KEEPINTVL 32767
#define MAX_TCP_KEEPCNT 127
#define MAX_TCP_SYNCNT 127
#define TCP_KEEPIDLE 4 /* Start keeplives after this period */
#define TCP_KEEPINTVL 5 /* Interval between keepalives */
#define TCP_KEEPCNT 6 /* Number of keepalives before death */
net/ipv4/Tcp.c,可以找到對應(yīng)關(guān)系:
case TCP_KEEPIDLE:
val = (tp->keepalive_time ? : sysctl_tcp_keepalive_time) / HZ;
break;
case TCP_KEEPINTVL:
val = (tp->keepalive_intvl ? : sysctl_tcp_keepalive_intvl) / HZ;
break;
case TCP_KEEPCNT:
val = tp->keepalive_probes ? : sysctl_tcp_keepalive_probes;
break;
初始化:
case TCP_KEEPIDLE:
if (val < 1 || val > MAX_TCP_KEEPIDLE)
err = -EINVAL;
else {
tp->keepalive_time = val * HZ;
if (sock_flag(sk, SOCK_KEEPOPEN) &&
!((1 << sk->sk_state) &
(TCPF_CLOSE | TCPF_LISTEN))) {
__u32 elapsed = tcp_time_stamp - tp->rcv_tstamp;
if (tp->keepalive_time > elapsed)
elapsed = tp->keepalive_time - elapsed;
else
elapsed = 0;
inet_csk_reset_keepalive_timer(sk, elapsed);
}
}
break;
case TCP_KEEPINTVL:
if (val < 1 || val > MAX_TCP_KEEPINTVL)
err = -EINVAL;
else
tp->keepalive_intvl = val * HZ;
break;
case TCP_KEEPCNT:
if (val < 1 || val > MAX_TCP_KEEPCNT)
err = -EINVAL;
else
tp->keepalive_probes = val;
break;
這里可以找到大部分處理邏輯,net/ipv4/Tcp_timer.c:
static void tcp_keepalive_timer (unsigned long data)
{
struct sock *sk = (struct sock *) data;
struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
__u32 elapsed;
/* Only process if socket is not in use. */
bh_lock_sock(sk);
if (sock_owned_by_user(sk)) {
/* Try again later. */
inet_csk_reset_keepalive_timer (sk, HZ/20);
goto out;
}
if (sk->sk_state == TCP_LISTEN) {
tcp_synack_timer(sk);
goto out;
}
// 關(guān)閉狀態(tài)的處理
if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {
if (tp->linger2 >= 0) {
const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;
if (tmo > 0) {
tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
goto out;
}
}
tcp_send_active_reset(sk, GFP_ATOMIC);
goto death;
}
if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)
goto out;
elapsed = keepalive_time_when(tp);
/* It is alive without keepalive 8) */
if (tp->packets_out || sk->sk_send_head)
goto resched;
elapsed = tcp_time_stamp - tp->rcv_tstamp;
if (elapsed >= keepalive_time_when(tp)) {
if ((!tp->keepalive_probes && icsk->icsk_probes_out >= sysctl_tcp_keepalive_probes) ||
(tp->keepalive_probes && icsk->icsk_probes_out >= tp->keepalive_probes)) {
tcp_send_active_reset(sk, GFP_ATOMIC);
tcp_write_err(sk); // 向上層應(yīng)用匯報(bào)連接異常
goto out;
}
if (tcp_write_wakeup(sk) <= 0) {
icsk->icsk_probes_out++; // 這里僅僅是計(jì)數(shù),并沒有再次發(fā)送保活探測包
elapsed = keepalive_intvl_when(tp);
} else {
/* If keepalive was lost due to local congestion,
* try harder.
*/
elapsed = TCP_RESOURCE_PROBE_INTERVAL;
}
} else {
/* It is tp->rcv_tstamp + keepalive_time_when(tp) */
elapsed = keepalive_time_when(tp) - elapsed;
}
TCP_CHECK_TIMER(sk);
sk_stream_mem_reclaim(sk);
resched:
inet_csk_reset_keepalive_timer (sk, elapsed);
goto out;
death:
tcp_done(sk);
out:
bh_unlock_sock(sk);
sock_put(sk);
}
keepalive_intvl_when 函數(shù)定義:
static inline int keepalive_intvl_when(const struct tcp_sock *tp)
{
return tp->keepalive_intvl ? : sysctl_tcp_keepalive_intvl;
}
四。TCP Keepalive 引發(fā)的錯(cuò)誤
啟用TCP Keepalive的應(yīng)用程序,一般可以捕獲到下面幾種類型錯(cuò)誤
- ETIMEOUT 超時(shí)錯(cuò)誤,在發(fā)送一個(gè)探測保護(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
- EHOSTUNREACH host unreachable(主機(jī)不可達(dá))錯(cuò)誤,這個(gè)應(yīng)該是ICMP匯報(bào)給上層應(yīng)用的。
java.io.IOException: No route to host
- 鏈接被重置,終端可能崩潰死機(jī)重啟之后,接收到來自服務(wù)器的報(bào)文,然物是人非,前朝往事,只能報(bào)以無奈重置宣告之。
java.io.IOException: Connection reset by peer
五。常見的使用模式
- 默認(rèn)情況下使用keepalive周期為2個(gè)小時(shí),如不選擇更改,屬于誤用范疇,造成資源浪費(fèi):內(nèi)核會(huì)為每一個(gè)連接都打開一個(gè)保活計(jì)時(shí)器,N個(gè)連接會(huì)打開N個(gè)保活計(jì)時(shí)器。 優(yōu)勢很明顯:
- TCP協(xié)議層面保活探測機(jī)制,系統(tǒng)內(nèi)核完全替上層應(yīng)用自動(dòng)給做好了
- 內(nèi)核層面計(jì)時(shí)器相比上層應(yīng)用,更為高效
- 上層應(yīng)用只需要處理數(shù)據(jù)收發(fā)、連接異常通知即可
- 數(shù)據(jù)包將更為緊湊
- 關(guān)閉TCP的keepalive,完全使用業(yè)務(wù)層面心跳保活機(jī)制 完全應(yīng)用掌管心跳,靈活和可控,比如每一個(gè)連接心跳周期的可根據(jù)需要減少或延長
- 業(yè)務(wù)心跳 + TCP keepalive一起使用,互相作為補(bǔ)充,但TCP保活探測周期和應(yīng)用的心跳周期要協(xié)調(diào),以互補(bǔ)方可,不能夠差距過大,否則將達(dá)不到設(shè)想的效果。朋友的公司所做IM平臺業(yè)務(wù)心跳2-5分鐘智能調(diào)整 + tcp keepalive 300秒,組合協(xié)作,據(jù)說效果也不錯(cuò)。
雖然說沒有固定的模式可遵循,那么有以下原則可以參考:
- 不想折騰,那就棄用TCP Keepalive吧,完全依賴應(yīng)用層心跳機(jī)制,靈活可控性強(qiáng)
- 除非可以很好把控TCP Keepalive機(jī)制,那就可以根據(jù)需要自由使用吧
六。注意和 HTTP的Keep-Alive區(qū)別
- HTTP協(xié)議的Keep-Alive意圖在于連接復(fù)用,同一個(gè)連接上串行方式傳遞請求-響應(yīng)數(shù)據(jù)
- TCP的keepalive機(jī)制意圖在于保活、心跳,檢測連接錯(cuò)誤。