隨手記之TCP Keepalive筆記
零。前言
TCP是無感知的虛擬連接,中間斷開兩端不會立刻得到通知。一般在使用長連接的環境下,需要心跳保活機制可以勉強感知其存活。業務層面有心跳機制,TCP協議也提供了心跳保活機制。
一。TCP Keepalive解讀
長連接的環境下,人們一般使用業務層面或上層應用層協議(諸如MQTT,SOCKET.IO等)里面定義和使用。一旦有熱數據需要傳遞,若此時連接已經被中介設備斷開,應用程序沒有及時感知的話,那么就會導致在一個無效的數據鏈路層面發送業務數據,結果就是發送失敗。
無論是因為客戶端意外斷電、死機、崩潰、重啟,還是中間路由網絡無故斷開、NAT超時等,服務器端要做到快速感知失敗,減少無效鏈接操作。
1. 交互過程
2. 協議解讀
下面協議解讀,基于RFC1122#TCP Keep-Alives。
- TCP Keepalive雖不是標準規范,但操作系統一旦實現,默認情況下須為關閉,可以被上層應用開啟和關閉。
- TCP Keepalive必須在沒有任何數據(包括ACK包)接收之后的周期內才會被發送,允許配置,默認值不能夠小于2個小時
- 不包含數據的ACK段在被TCP發送時沒有可靠性保證,意即一旦發送,不確保一定發送成功。系統實現不能對任何特定探針包作死連接對待
- 規范建議keepalive保活包不應該包含數據,但也可以包含1個無意義的字節,比如0x0。
- SEG.SEQ = SND.NXT-1,即TCP保活探測報文序列號將前一個TCP報文序列號減1。SND.NXT = RCV.NXT,即下一次發送正常報文序號等于ACK序列號;總之保活報文不在窗口控制范圍內 有一張圖,可以很容易說明,但請仔細觀察Tcp Keepalive部分:
- 不太好的TCP堆棧實現,可能會要求保活報文必須攜帶有1個字節的數據負載
- TCP Keepalive應該在服務器端啟用,客戶端不做任何改動;若單獨在客戶端啟用,若客戶端異常崩潰或出現連接故障,存在服務器無限期的為已打開的但已失效的文件描述符消耗資源的嚴重問題。但在特殊的NFS文件系統環境下,需要客戶端和服務器端都要啟用Tcp Keepalive機制。
- TCP Keepalive不是TCP規范的一部分,有三點需要注意:
- 在短暫的故障期間,它們可能引起一個良好連接(good connection)被釋放(dropped)
- 它們消費了不必要的寬帶
- 在以數據包計費的互聯網消費(額外)花費金錢
二。Tcp keepalive 如何使用
以下環境是在Linux服務器上進行。應用程序若想使用,需要設置SO_KEEPALIVE套接口選項才能夠生效。
1. 系統內核參數配置
- tcp_keepalive_time,在TCP保活打開的情況下,最后一次數據交換到TCP發送第一個保活探測包的間隔,即允許的持續空閑時長,或者說每次正常發送心跳的周期,默認值為7200s(2h)。
- tcp_keepalive_probes 在tcp_keepalive_time之后,沒有接收到對方確認,繼續發送保活探測包次數,默認值為9(次)
- tcp_keepalive_intvl,在tcp_keepalive_time之后,沒有接收到對方確認,繼續發送保活探測包的發送頻率,默認值為75s。
發送頻率tcp_keepalive_intvl乘以發送次數tcp_keepalive_probes,就得到了從開始探測到放棄探測確定連接斷開的時間
若設置,服務器在客戶端連接空閑的時候,每90秒發送一次保活探測包到客戶端,若沒有及時收到客戶端的TCP Keepalive ACK確認,將繼續等待15秒*2=30秒。總之可以在90s+30s=120秒(兩分鐘)時間內可檢測到連接失效與否。
以下改動,需要寫入到/etc/sysctl.conf文件:
net.ipv4.tcp_keepalive_time=90
net.ipv4.tcp_keepalive_intvl=15
net.ipv4.tcp_keepalive_probes=2
保存退出,然后執行sysctl -p
生效。可通過 sysctl -a | grep keepalive
命令檢測一下是否已經生效。
針對已經設置SO_KEEPALIVE的套接字,應用程序不用重啟,內核直接生效。
2. Java/netty服務器如何使用
只需要在服務器端一方設置即可,客戶端完全不用設置,比如基于netty 4服務器程序:
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程序只能做到設置SO_KEEPALIVE選項,至于TCP_KEEPCNT,TCP_KEEPIDLE,TCP_KEEPINTVL等參數配置,只能依賴于sysctl配置,系統進行讀取。
3. C語言如何設置
下面代碼摘取自libkeepalive源碼,C語言可以設置更為詳細的TCP內核參數。
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實現
完全可以借助于第三方工具libkeepalive,通過LD_PRELOAD方式實現。比如
LD_PRELOAD=/the/path/libkeepalive.so java -jar /your/path/yourapp.jar &
這個工具還有一個比較方便的地方,可以直接在程序運行前指定TCP保活詳細參數,可以省去配置sysctl.conf的麻煩:
LD_PRELOAD=/the/path/libkeepalive.so \
> KEEPCNT=20 \
> KEEPIDLE=180 \
> KEEPINTVL=60 \
> java -jar /your/path/yourapp.jar &
針對較老很久不更新的程序,可以嘗試一下嘛。
三。Linux內核層面對keepalive處理
參數和定義
#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,可以找到對應關系:
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;
}
// 關閉狀態的處理
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); // 向上層應用匯報連接異常
goto out;
}
if (tcp_write_wakeup(sk) <= 0) {
icsk->icsk_probes_out++; // 這里僅僅是計數,并沒有再次發送保活探測包
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 函數定義:
static inline int keepalive_intvl_when(const struct tcp_sock *tp)
{
return tp->keepalive_intvl ? : sysctl_tcp_keepalive_intvl;
}
四。TCP Keepalive 引發的錯誤
啟用TCP Keepalive的應用程序,一般可以捕獲到下面幾種類型錯誤
- ETIMEOUT 超時錯誤,在發送一個探測保護包經過(tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes)時間后仍然沒有接收到ACK確認情況下觸發的異常,套接字被關閉
java.io.IOException: Connection timed out
- EHOSTUNREACH host unreachable(主機不可達)錯誤,這個應該是ICMP匯報給上層應用的。
java.io.IOException: No route to host
- 鏈接被重置,終端可能崩潰死機重啟之后,接收到來自服務器的報文,然物是人非,前朝往事,只能報以無奈重置宣告之。
java.io.IOException: Connection reset by peer
五。常見的使用模式
- 默認情況下使用keepalive周期為2個小時,如不選擇更改,屬于誤用范疇,造成資源浪費:內核會為每一個連接都打開一個保活計時器,N個連接會打開N個保活計時器。 優勢很明顯:
- TCP協議層面保活探測機制,系統內核完全替上層應用自動給做好了
- 內核層面計時器相比上層應用,更為高效
- 上層應用只需要處理數據收發、連接異常通知即可
- 數據包將更為緊湊
- 關閉TCP的keepalive,完全使用業務層面心跳保活機制 完全應用掌管心跳,靈活和可控,比如每一個連接心跳周期的可根據需要減少或延長
- 業務心跳 + TCP keepalive一起使用,互相作為補充,但TCP保活探測周期和應用的心跳周期要協調,以互補方可,不能夠差距過大,否則將達不到設想的效果。朋友的公司所做IM平臺業務心跳2-5分鐘智能調整 + tcp keepalive 300秒,組合協作,據說效果也不錯。
雖然說沒有固定的模式可遵循,那么有以下原則可以參考:
- 不想折騰,那就棄用TCP Keepalive吧,完全依賴應用層心跳機制,靈活可控性強
- 除非可以很好把控TCP Keepalive機制,那就可以根據需要自由使用吧
六。注意和 HTTP的Keep-Alive區別
- HTTP協議的Keep-Alive意圖在于連接復用,同一個連接上串行方式傳遞請求-響應數據
- TCP的keepalive機制意圖在于保活、心跳,檢測連接錯誤。