浅析连接池和TCP探活

本文AI率50%

DBA了解一些连接池和TCP的探活保活知识也是比较重要的,对一些业务断连报错,SQL执行报错,HA高可用都有帮助。

TCP的keepalive和PG的参数

应用(包括业务客户端、数据库server、psql)和操作系统都可以设置socket选项。如果没有显示设置,那么一般都是用的linux内核参数的默认值。

linux参数 linux默认值 Socket选项 PG server参数 libpq参数(PG client)
SO_KEEPALIVE(默认1) keepalives 1(default),on
tcp_keepalive_time 7200s TCP_KEEPIDLE tcp_keepalives_idle keepalives_idle
tcp_keepalive_intvl 75s TCP_KEEPINTVL tcp_keepalives_interval keepalives_interval
tcp_keepalive_probes 9 tcp_keepalives_count keepalives_count
tcp_retries2 15
TCP_USER_TIMEOUT tcp_user_timeout tcp_user_timeout
client_connection_check_interval

PG server和libpq默认值都是使用OS的socket默认值。

默认值的含义:达到2小时ildle的连接,tcp内核主动发送keepalive,在75s*9=11.25min后中断连接。

默认net.pv4.tcp_keepalive_time=7200s,这个值太大了,几乎毫无意义,等网络中间层比如防火墙设备掐断连接了才来做keepalive有什么意义呢。

client_connection_check_interval 是 PG 14 引入的应用层机制------PG 服务端每隔 N 毫秒对客户端 socket 做一次非阻塞 recv(),如果返回错误(连接断开)就主动清理。这不需要任何 Linux 内核参数配

TCP的FIN和RST包

参考 https://linuxvox.com/blog/what-is-the-reason-and-how-to-avoid-the-fin-ack-rst-and-rst-ack/

TCP 6个control bits:

Flag Name Purpose
SYN Synchronize Initiates a connection (used in the handshake).
ACK Acknowledge Confirms receipt of a packet (includes an ACK number for sequence tracking).
FIN Finish Signals intent to close a connection gracefully.
RST Reset Abruptly terminates a connection (no graceful closure).
PSH Push Forces immediate delivery of data (bypasses buffering).
URG Urgent Marks data as "urgent" (rarely used today).

FIN和RST都有正常情况和异常情况发的,总结几个要点:

  1. 进程退出或程序abort发的是FIN包,这包括KILL -9(已验证PG进程kill -9发FIN包,见"测试"部分)
  2. 端口不可达等网络不可用是RST包
  3. TCP keepalive超时也是RST包,因为探测出了网络不可用
  4. 防火墙也有可能做RESET
  5. RST包跟业务层 connection reset by peer 报错相关

下面是 TCP 6个控制位和 FIN/RST 的详细说明:

TCP断连测试

测试:KILL会话是否有主动断开操作

  • ORACLE无论是内置的alter system 杀会话还是kill -9杀会话,客户端均有收到服务端发的FIN包。
  • PG内置pg_terminate_backend()杀会话,客户端有收到服务端发的FIN包。
  • redis关库或者kill -9 redis-server进程,客户端有收到服务端发的FIN包。

测试结论:即便是进程异常中断,tcp内核也可以发FIN包。

另外,该轮测试中,redis-cli看起来没有正确处理FIN包,是自己rst的:

序号 时间 方向 Flags 说明
1 17:42:43.131958 服→客 . ACK 服务端回ACK
2 17:42:49.264831 服→客 F. FIN+ACK 服务端主动请求关闭
3 17:42:49.304905 客→服 . ACK 客户端确认 FIN(ack=9=8+1)
4~15 17:43:04 ~ 17:44:19 客→服 . ACK 客户端持续 ACK(保持连接?)
16 17:44:19.323962 服→客 R RST 服务端发 RST

测试:PG进程终止、正常关库、暴力关库,客户端收到什么包

测试环境:Rocky 10.1 + PG 18.2,tcpdump 抓 lo 口 TCP 包。

场景 服务端发的包 四次挥手 客户端报错
pg_terminate_backend(PID) [F.] FIN+ACK ✅ 完整 FATAL: terminating connection due to administrator command
pg_ctl stop -m fast [F.] FIN+ACK ✅ 完整 FATAL: terminating connection due to administrator command
kill -9 postmaster [F.] FIN+ACK ✅ 完整 server closed the connection unexpectedly

结论:kill -9 也发 FIN,不是 RST。 Linux TCP 内核在进程被 SIGKILL 时替进程关闭 socket,发 FIN 完成四次挥手。三种方式的客户端都收到正常的 FIN 关闭,没有任何场景发 RST。

测试:怎么产生 RST 包

端口无监听(PG 已停库)

text 复制代码
14:01:48.492004 IP 127.0.0.1.52092 > 127.0.0.1.ircu-2: Flags [S], seq 2570941791
14:01:48.492012 IP 127.0.0.1.ircu-2 > 127.0.0.1.52092: Flags [R.], seq 0, ack 2570941792, win 0

客户端 SYN → 内核返回 [R.] RST+ACK,win 0。psql 报 Connection refused

iptables REJECT --reject-with tcp-reset

text 复制代码
14:02:37.768515 IP 127.0.0.1.36436 > 127.0.0.1.ircu-2: Flags [S], seq 382980016
14:02:37.768522 IP 127.0.0.1.ircu-2 > 127.0.0.1.36436: Flags [R.], seq 0, ack 382980017, win 0

和端口无监听完全一致:[R.] RST+ACK。psql 同样报 Connection refused

iptables DROP(模拟防火墙静默丢包)

text 复制代码
14:00:07.050040 IP 127.0.0.1.33166 > 127.0.0.1.ircu-2: Flags [S], seq 985608804
14:00:08.095618 IP 127.0.0.1.33166 > 127.0.0.1.ircu-2: Flags [S], seq 985608804   ← 1s后重传
14:00:09.119647 IP 127.0.0.1.33166 > 127.0.0.1.ircu-2: Flags [S], seq 985608804   ← 2s后重传

服务端无任何回应,客户端 SYN 重传 3 次(1s、2s、4s 间隔)后超时。与 REJECT 不同,DROP 不会有 RST,客户端只能靠超时感知。

RST 产生场景总结

场景 协议层 包类型 触发方
端口无监听 TCP 内核 [R.] RST+ACK OS 内核
防火墙 REJECT iptables [R.] RST+ACK 防火墙
TCP keepalive 超时 TCP 内核 [R] RST OS 内核(keepalive 探测失败后)
进程终止 (kill -9) TCP 内核 [F.] FIN+ACK(不是 RST!) OS 内核替进程关 socket
防火墙 DROP --- ---

核心区分:FIN 是进程退出(内核替进程优雅关闭,哪怕是kill -9),RST 是网络不可达。

测试:IP下线是否会有主动断开操作

redis-cli测试,redis server端下线监听ip。

shell 复制代码
#term1:
r -h 30.181.15.96 -p 17742 -a 1qaz@WSX
sudo tcpdump host 30.181.48.7 and port 54854 -n -vv   
#term2:
sudo tcpdump host 30.181.48.7 and port 54854 -n -vv   

本次测试ip下线没有发生FIN或者RST包,只是keepalive本身发起了RST,序列如下:

序号 时间 方向 Flags 备注
1 17:02:43.004897 客户端→服务端 . ACK 客户端发ACK(15s间隔)
2 17:02:43.004960 服务端→客户端 . ACK 服务端回ACK
3 17:02:58.043896 客户端→服务端 . ACK 客户端Keep-Alive(15s间隔)
4 17:02:58.043953 服务端→客户端 . ACK 服务端回ACK
5 17:02:58.063214 服务端→客户端 . ACK 服务端重复ACK
6 17:02:58.063234 客户端→服务端 . ACK 客户端回ACK
7 17:03:13.051905 客户端→服务端 . ACK 客户端Keep-Alive(15s间隔)
8 17:03:18.059901 客户端→服务端 . ACK 客户端Keep-Alive(5s间隔)
9 17:03:23.067901 客户端→服务端 . ACK 客户端Keep-Alive(5s间隔)
10 17:03:28.075899 客户端→服务端 R. RST+ACK 客户端主动断开(5s间隔)

redis-cli没有keepalive的配置,但redis-cli源码中写死:

c 复制代码
#define REDIS_CLI_KEEPALIVE_INTERVAL 15 /* seconds */

redis-cli的keepalive是代码中写死的15s一次,所以能看到15秒一次的keepalive包。

在抓包期间有服务端IP下线操作但没有收到任何断连信息,最后由客户端Keepalive探测出socket异常,客户端主动RST。

(redis server段同样可以发起keepalive,但是这次没有触发)

测试结论:直接下线IP,内核可能不会有任何FIN/RST动作。

测试:正常数据通信是否干扰tcp_keepalive周期?

结论:会。数据通信不仅有PSH包发送到对端,也含有ACK包。

以下用redis-cli测试,redis-cli的keepalive=15s,redis-server的keepalive=2h:

客户端触发 tcp时间戳 客户端发 服务端发
tcp_keepalive 17:16:05.558570-17:16:15.048701 ACK ACK
PING 17:16:15.048312-17:16:15.048701 PSH PSH
tcp_keepalive 17:16:15.048433-17:16:30.071278 ACK ACK
tcp_keepalive 17:16:30.070906-17:16:30.071278 ACK ACK

测试:idle_in_transaction 和长时间 SQL 会发 keepalive 吗?

测试环境:Rocky 10.1 + PG 18.2,客户端 libpq 设置 keepalives_idle=5 keepalives_interval=3

idle_in_transaction:

text 复制代码
16:32:11.611  最后一条数据 ACK
16:32:16.927  客户端 → 服务端 [.] ACK  ← 5.3s 后,第一次 keepalive 探测
16:32:16.927  服务端 → 客户端 [.] ACK
16:32:21.983  客户端 → 服务端 [.] ACK  ← 5s 后,第二次探测
16:32:21.983  服务端 → 客户端 [.] ACK
16:32:27.039  客户端 → 服务端 [.] ACK  ← 5s 后,第三次探测
16:32:27.039  服务端 → 客户端 [.] ACK

结论:idle_in_transaction 会发 keepalive。每 5 秒一对探测+回应,除此之外没有任何其他 TCP 包。

长时间 SQL(服务端 tcp_keepalives_idle=10):

text 复制代码
16:32:43.148  最后一条 ACK(客户端发出 SELECT pg_sleep(30) 后)
             ← 中间 10 秒零 TCP 包 ← SQL 在跑,但无数据回传
16:32:53.279  服务端 → 客户端 [.] ACK  ← 10.1s 后,服务端发 keepalive 探测
16:32:53.279  客户端 → 服务端 [.] ACK

结论:SQL 在跑 ≠ TCP 有包。 pg_sleep(30) 期间无任何 TCP 通信,keepalive 照样触发------它只看 TCP 层有没有数据交换,不看数据库在干什么。

如果一个报表查询跑了 5 分钟且中间不返回结果,对于防火墙/NAT/负载均衡来说,这个 TCP 连接就是 5 分钟的死连接------不配 keepalive 就会被掐断。

连接探活

客户端的死连接问题只能由客户端解决------服务端已经访问不到了,不可能指望它来通知你。

连接池的两个关键概念:

  • socket.close() ≠ 连接池 close():前者是 TCP 四次挥手彻底断开,后者是把连接归还给连接池,连接保持 ESTABLISHED,状态变为 idle
  • 探活的目标:及时发现那些 socket 已断、但连接池还以为是活着的"僵尸连接"

两个常见 socket 错误状态:

  • ESTABLISHED 但实际不可用:连接池未感知到 socket 已失效,应用层操作时才报错
  • TIME_WAIT:感知到 socket 不可用但未及时释放,大量 TIME_WAIT 会耗尽端口

总体来说探活机制按网络层分为三种:

类型 动作 触发方式 发送内容
4 层探活 内核层 TCP 包 tcp_keepalive 系列参数;连接池自身的 keepalive ACK 包(空包探测对端是否存活)
7 层探活 应用层数据库命令 testOnBorrow / testOnReturn / testWhileIdle / PING/配置test-query 视驱动而定,如 SELECT 1PINGSELECT NOT pg_is_in_recovery() / SELECT @@READ_ONLY

4 层探活

Linux 的 tcp_keepalive 是 4 层探活的基础:

shell 复制代码
net.ipv4.tcp_keepalive_time   = 7200   # 空闲 2 小时后才开始探测
net.ipv4.tcp_keepalive_intvl  = 75     # 探测间隔 75 秒
net.ipv4.tcp_keepalive_probes = 9      # 探测 9 次失败后断开

默认值的问题:7200 秒(2 小时)才开始探测,中间防火墙早就把连接掐了,探测毫无意义。生产环境通常需要调小到分钟级。

如果链路中有代理(Nginx、HAProxy 等),TCP keepalive 只到代理,不到后端数据库。 代理到数据库那一段需要代理自己配 keepalive,否则代理挂了连接池感知不到。

实际通信中,有数据交互时,PSH/ACK 包本身就充当了"保活"的角色。keepalive 只在连接完全空闲时才触发------如果有持续的数据收发,keepalive 计时器会被重置,不会发送 ACK 探测包。

7 层探活

7 层探活是应用主动发数据库命令验证连接。各连接池的代表参数(不全面):

连接池 参数 说明
JDBC 通用 testOnBorrowtestOnReturntestWhileIdle 借出/归还/空闲时验证
HikariCP connectionTestQuery 验证 SQL,常用 SELECT 1
Jedis testOnBorrow 借出时验证
Lettuce pingBeforeActivateConnection 激活前 PING
Redisson pingConnectionInterval 定时 PING 间隔
Apache Commons Pool2 testOnBorrow 通用对象池验证

close()returnObject() 都是将连接归还给连接池,不是真正关闭 TCP。归还后连接处于 idle 状态,socket 仍然 ESTABLISHED。Apache Commons Pool2 通过标准化的对象池管理机制来维护这些连接。

关于 testOnBorrow 的性能影响: 每次借连接都发 SELECT 1,高并发下有额外开销。通常用 testWhileIdle + 合理检测间隔来平衡。

4 层 vs 7 层的选择:

  • 4 层:直连数据库,链路中无代理,TCP keepalive 配小即可
  • 7 层:链路中有代理、需要确认数据库真的能执行 SQL(不只是 TCP 通),且可确保整个链路是打通的。
  • 7层+角色:需要主从区分时,不能执行简单sql比如select 1来识别数据库的角色,此时需要配置自定义SQL。比如redis PING不能得知从库状态

单域名 vs 双域名

驱动配置主从地址(JDBC 的 read-write + read-only 或 Lettuce 的 Master/Replica)时,可以自动识别主从并路由。

单域名的问题:

  • 无法识别主从切换
  • 被 JVM/OS 的 DNS 缓存限制(networkaddress.cache.ttl),主从切换后可能长时间连旧 IP
  • 7 层探活配 SELECT NOT pg_is_in_recovery() 可以检测到主从变化,但不如双域名灵活

总结

FIN 和 RST 的发生场景:

  • FIN 是进程退出时内核代发的(包括 kill -9),走四次挥手优雅关闭
  • RST 是网络不可达时产生的:端口无监听、keepalive 超时、防火墙 REJECT等
  • IP 直接下线不会有任何 FIN/RST,只能靠 keepalive 探测出来
  • 防火墙 DROP 静默丢包,没有 RST,客户端只能超时

4 层和 7 层的探活机制:

  • 4 层(TCP keepalive):默认 2h 才探测,生产环境必须调小。只到代理,不到后端
  • 7 层(应用层 PING/SQL):能确认数据库真的能执行命令,但高并发下有性能开销
  • 有代理 / 需要主从区分 → 必须 7 层
  • 直连数据库 → 4 层配小即可

idle_in_transaction 和长时间 SQL 的 keepalive 行为:

  • 两者都会发 keepalive------触发条件是 TCP 层无数据交换,不是数据库状态
  • SQL 在跑 ≠ TCP 有包:长时间报表查询如果不返回中间结果,TCP 层等同于死连接
  • 不配 keepalive 的话,防火墙可能在 SQL 还在跑的时候就掐断连接

一些注意事项:

  • socket.close() ≠ 连接池归还:前者断开 TCP,后者只是把连接标记为 idle
  • 连接池探活的目标就是发现那些 socket 已断但连接池还以为活着的僵尸连接
  • testOnBorrow 每次借连接都查库,高并发有开销;testWhileIdle + 合理间隔更实用
  • 链路中有代理时,每段都需要独立配置 keepalive,一段断了另一端感知不到

ref

相关推荐
ai_coder_ai1 小时前
论P2P计算关键技术与应用
网络·网络协议·p2p
爱讲故事的1 小时前
计算机网络第 8 章复习:Network Security 网络安全
网络·计算机网络·web安全
sdm0704271 小时前
网络原理-4.数据链路层
网络
cft56200_ln2 小时前
TDA4时间同步3 网卡添加虚拟时间戳
c语言·开发语言·arm开发·驱动开发·嵌入式硬件·网络协议
User_芊芊君子2 小时前
无公网 IP 也能跨网互联:8 年技术沉淀的内网穿透 + 异地组网解决方案
网络·网络协议·tcp/ip
Rookie Linux2 小时前
使用Qt6 QML以及第三方库FluentUI、PCapPlusPlus开发一个自定义抓包软件
网络·c++·qt·cmake·qml
Stick_ZYZ2 小时前
A2A:让 Agent 从单兵作战走向团队协作
java·开发语言·网络·人工智能·python·ai
BizViewStudio2 小时前
2026 年 GEO 成为企业线上流量增长核心风口|2026 品牌 GEO 运营指南,6 家全链路优化服务商解析
运维·网络·人工智能·microsoft·ai
A_humble_scholar2 小时前
Linux(六)深入理解 Linux 进程管理:从硬件到调度
linux·网络