Linux 内核协议栈源码分析(六):TCP 保活机制与连接池设计
内核版本:Linux 5.x~6.x 主线 | 系列第六节 | 作者:资深工程师
一、问题引入:连接什么时候会"默默死去"?
先看一个生产环境中非常常见的故障场景——
⚠️ 经典故障: 客户端从连接池里拿了一个连接,调用 write(),一切正常。 |
这就是所谓的 "Dead Connection"(死连接)问题。TCP 是一个"有状态"的协议,但它的状态是被动维护的——只有当你真正发送数据时,它才会检测到"对面已经死了"。在连接空闲期间,TCP 默认不会发送任何探测包。这就是 KeepAlive 要解决的问题。
💡 本节你将学到: 1. TCP KeepAlive 的三个核心参数及各场景调优建议 |
二、TCP KeepAlive:传输层的保活探针
2.1 KeepAlive 的工作机制
TCP KeepAlive 是一个默认关闭的传输层机制。一旦开启,内核会维护一个定时器:
↓ 发送第一个 KeepAlive 探针(不带数据的 ACK 包,seq = snd_una - 1) ↓ 对端响应:ACK(连接存活) → 重置空闲计时器 对端无响应:等待 tcp_keepalive_intvl(默认 75s) ↓ 连续 tcp_keepalive_probes(默认 9)次无响应 ↓ 内核判定连接已死 → 发送 RST → 连接关闭 |
2.2 三个核心参数
| tcp_keepalive_time | |||
| tcp_keepalive_intvl | |||
| tcp_keepalive_probes |
默认检测死连接总耗时:
7200s + 9 × 75s = 7875s ≈ 2 小时 11 分钟
💡 快速系统级配置: sysctl -w net.ipv4.tcp_keepalive_time=600 |
2.3 单连接级别配置
针对单个 socket 设置 KeepAlive 参数(Linux 2.4+ 支持):
/* 单连接 KeepAlive 配置示例 */ int sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 1. 开启 KeepAlive */ int keepalive = 1; setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive)); /* 2. 空闲 60s 后开始探测 */ int idle = 60; setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle)); /* 3. 探测间隔 10s */ int intvl = 10; setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof(intvl)); /* 4. 连续 3 次无响应判死 */ int cnt = 3; setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt)); |
⚠️ 注意:TCP_KEEPIDLE/TCP_KEEPINTVL/TCP_KEEPCNT 是 Linux 专有扩展。跨平台应用推荐使用应用层心跳。 |
三、KeepAlive 内核源码透视
3.1 定时器注册
当应用层调用 SO_KEEPALIVE 时,内核设置 sk->sk_socket->keepalive = 1,空闲后触发 tcp_keepalive_timer。
/* net/ipv4/tcp.c - tcp_set_keepalive() */ inttcp_set_keepalive(struct sock *sk, int val) { struct tcp_sock *tp = tcp_sk(sk); sk->sk_socket->keepalive = !!val; if (sk->sk_socket->keepalive) { /* 连接空闲 → 启动 KeepAlive 定时器 */ if (tp->packets_out == 0) tcp_reset_keepalive_timer(sk, tcp_keepalive_time(sk)); } else { tcp_delete_keepalive_timer(sk); } return0; } |
3.2 KeepAlive 定时器核心逻辑
定时器核心函数 tcp_keepalive_timer()(net/ipv4/tcp_timer.c):
/* net/ipv4/tcp_timer.c - tcp_keepalive_timer() */ staticvoidtcp_keepalive_timer(struct timer_list *t) { struct sock *sk = from_timer(sk, t, sk_timer); struct tcp_sock *tp = tcp_sk(sk); /* 1. 状态检查:仅 ESTABLISHED 或 CLOSE_WAIT 有效 */ if (tcp_sk(sk)->state < TCP_ESTABLISHED) goto out; /* 2. 探针用完了 → 连接已死 */ if (tp->keepalive_probes >= tcp_keepalive_probes(sk)) { tcp_send_active_reset(sk, GFP_ATOMIC); tcp_set_state(sk, TCP_CLOSE); sk->sk_err = ETIMEDOUT; /* recv() 返回超时错误 */ sk_error_report(sk); /* 通知 epoll/select */ goto out; } /* 3. 发送探针 + 重设定时器 */ tp->keepalive_probes++; tcp_write_wakeup(sk); /* 发送探针 */ tcp_reset_keepalive_timer(sk, tcp_keepalive_intvl(sk)); out: bh_unlock_sock(sk); } |
💡 关键发现:KeepAlive 探针发送的是带有 seq = snd_una - 1 的 ACK 包。对端回复正确 ACK 即可确认存活。这个机制不需要对端也开启 KeepAlive——它是单方面探测。 |
四、应用层心跳 vs 传输层保活
4.1 为什么不能只靠 TCP KeepAlive
| 粒度 | ||
| 检测层面 | ||
| 跨代理 | ||
| 跨平台 |
⚠️ 关键区别:TCP KeepAlive 是"传输层"保活——只证明网络是通的。应用层心跳是"应用层"保活——证明对方的应用程序还在工作。数据库进程卡在死锁里,TCP KeepAlive 说"连接正常"——但你的查询永远不会返回。 |
4.2 各场景保活策略推荐
| 用 TCP KeepAlive: • 检测网络中断(对端断电/断网) • 防止防火墙/NAT超时删连接 • 不想改应用层代码 | 用应用层心跳: • 需要验证应用逻辑 • 跨平台应用 • 连接数较少 |
4.3 最佳实践:两者都开
TCP KeepAlive:time=600s, intvl=30s, probes=3 ← 负责兜底 → 即使应用层心跳没发出来,KeepAlive 也能在 ~690s 后发现死连接 |
五、连接池设计:避开那些坑
5.1 连接池设计的五个原则
| ① 先检测再使用 | ||
| ② 连接老化 | ||
| ③ 上限+拒绝 | ||
| ④ 后台心跳 | ||
| ⑤ 快速失败 |
5.2 三种"先检测再使用"方式对比
| ✅ 提前 PING(推荐) | ⚠️ 检查 SO_ERROR(快速但有限) getsockopt(sockfd,SOL_SOCKET,SO_ERROR) |
| ❌ try-write(危险!) 致命缺陷:如果连接已死,write() 可能要等几十秒(TCP 重传/RTO 超时)才会返回——期间整个请求线程被阻塞! |
✅ 推荐方案:PING + 超时 每次取出连接时发送 PING,设置200ms 短超时。超时未返回 → 关闭连接 → 取下一个。 |
5.3 连接老化策略
| 空闲老化 | ||
| 绝对老化 | ||
| 错误老化 | ||
| 健康检查 |
六、实战:诊断死连接问题
6.1 ss 查看 KeepAlive 状态
# 查看长连接的空闲时间(-o 显示 timer 详情) ss -tno # 输出示例: ESTAB 0 0 172.16.1.10:54321 10.0.0.1:3306 timer:(keepalive,119min,0) # ^^^^^^^^ 已空闲 119 分钟 # 筛选空闲 > 600s 的长连接 ss -tno | awk '$1=="ESTAB" && $6~"^keepalive"' | awk -F'[(,:]' '{if($8>600) print $0}' |
6.2 tcpdump 抓 KeepAlive 探针
# 抓取 KeepAlive 包(ACK 带 length 0) tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) == 0' and 'less 1' # 输出示例: 10:30:00.123456 IP 172.16.1.10.54321 > 10.0.0.1.3306: Flags [.], ack 123456, win 229, length 0 # ^^^^^^^^ ^^^^^^^^ # 纯 ACK 无数据 = 探针 |
6.3 bpftrace 追踪 KeepAlive
bpftrace -e ' kprobe:tcp_keepalive_timer { $sk = (struct sock *)arg0; $dport = $sk->__sk_common.skc_dport; time("%H:%M:%S "); printf("KEEPALIVE PROBE #%d: %s:%d -> :%d", ((struct tcp_sock *)$sk)->keepalive_probes, inet_ntoa($sk->__sk_common.skc_rcv_saddr), $sk->__sk_common.skc_num, $dport); }' |
6.4 排障流程图
├─ timer:(keepalive, >600min) → 空闲过长 ├─ timer:(on, >60s) → RTO 重传中,网络有问题 └─ CLOSE_WAIT / TIME_WAIT(大量) → 应用层 bug 或短连接风暴 ↓ ② netstat -s | grep -i retrans ├─ 重传持续增长 → 网络丢包严重 └─ 正常 → 应用层 bug ↓ ③ tcpdump 抓包 → 确认 KeepAlive 频率 / 重传标记 ↓ ④ bpftrace 追踪 → tcp_keepalive_timer / tcp_retransmit_skb |
七、小结
这一节聚焦 TCP 的 "健康管理"——保活机制和连接池设计。
| KeepAlive 核心要点 • 需要应用层开启 SO_KEEPALIVE • 默认检测死连接要 2 小时+,必须调优 • 传输层只证明 TCP 栈活着 • 应用层心跳才能证明应用正常 • 两者都开是最佳实践 | 连接池设计核心 • 连接老化(空闲+绝对) • 上限+拒绝避免打爆对端 • 后台心跳主动清理死连接 • 快速失败,不反复重试 • 避免 try-write 检测——太慢! |
第二节:数据接收(NAPI、GRO、IP层) 第三节:TCP传输层(三次握手、状态机) 第四节:数据传输(序列号、窗口、拥塞控制) 第五节:重传机制 + TIME_WAIT 第六节:保活机制 + 连接池设计 ← 你在这里 第七节预告:TCP 关闭流程——FIN、半关闭、shutdown() |
🎯 下节预告(第七节): TCP 关闭流程全解析——主动关闭 vs 被动关闭源码路径、FIN-WAIT-1/FIN-WAIT-2/CLOSING/LAST_ACK 四个状态细节、close() vs shutdown() 的本质区别、半关闭连接的应用场景、SO_LINGER 的陷阱。 |
Linux 内核协议栈源码分析系列 · 第六节 · 资深工程师出品
夜雨聆风