跳过正文
  1. 文章/

浅析连接池和TCP探活

liuzhilong62
作者
liuzhilong62
PostgreSQL DBA,关注数据库内核、案例分析、源码解读

本文AI率50%

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

TCP的keepalive和PG的参数
#

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

linux参数linux默认值Socket选项PG server参数libpq参数(PG client)
SO_KEEPALIVE(默认1)keepalives1(default),on
tcp_keepalive_time7200sTCP_KEEPIDLEtcp_keepalives_idlekeepalives_idle
tcp_keepalive_intvl75sTCP_KEEPINTVLtcp_keepalives_intervalkeepalives_interval
tcp_keepalive_probes9tcp_keepalives_countkeepalives_count
tcp_retries215
TCP_USER_TIMEOUTtcp_user_timeouttcp_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:

FlagNamePurpose
SYNSynchronizeInitiates a connection (used in the handshake).
ACKAcknowledgeConfirms receipt of a packet (includes an ACK number for sequence tracking).
FINFinishSignals intent to close a connection gracefully.
RSTResetAbruptly terminates a connection (no graceful closure).
PSHPushForces immediate delivery of data (bypasses buffering).
URGUrgentMarks 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说明
117:42:43.131958服→客. ACK服务端回ACK
217:42:49.264831服→客[F.] FIN+ACK服务端主动请求关闭
317:42:49.304905客→服. ACK客户端确认 FIN(ack=9=8+1)
4~1517:43:04 ~ 17:44:19客→服. ACK客户端持续 ACK(保持连接?)
1617: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 已停库)

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

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(模拟防火墙静默丢包)

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+ACKOS 内核
防火墙 REJECTiptables[R.] RST+ACK防火墙
TCP keepalive 超时TCP 内核[R] RSTOS 内核(keepalive 探测失败后)
进程终止 (kill -9)TCP 内核[F.] FIN+ACK(不是 RST!)OS 内核替进程关 socket
防火墙 DROP

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

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

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

#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备注
117:02:43.004897客户端→服务端. ACK客户端发ACK(15s间隔)
217:02:43.004960服务端→客户端. ACK服务端回ACK
317:02:58.043896客户端→服务端. ACK客户端Keep-Alive(15s间隔)
417:02:58.043953服务端→客户端. ACK服务端回ACK
517:02:58.063214服务端→客户端. ACK服务端重复ACK
617:02:58.063234客户端→服务端. ACK客户端回ACK
717:03:13.051905客户端→服务端. ACK客户端Keep-Alive(15s间隔)
817:03:18.059901客户端→服务端. ACK客户端Keep-Alive(5s间隔)
917:03:23.067901客户端→服务端. ACK客户端Keep-Alive(5s间隔)
1017:03:28.075899客户端→服务端[R.] RST+ACK客户端主动断开(5s间隔)

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

#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_keepalive17:16:05.558570-17:16:15.048701ACKACK
PING17:16:15.048312-17:16:15.048701PSHPSH
tcp_keepalive17:16:15.048433-17:16:30.071278ACKACK
tcp_keepalive17:16:30.070906-17:16:30.071278ACKACK

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

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

idle_in_transaction:

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):

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 系列参数;连接池自身的 keepaliveACK 包(空包探测对端是否存活)
7 层探活应用层数据库命令testOnBorrow / testOnReturn / testWhileIdle / PING/配置test-query视驱动而定,如 SELECT 1PINGSELECT NOT pg_is_in_recovery() / SELECT @@READ_ONLY

4 层探活
#

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

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借出/归还/空闲时验证
HikariCPconnectionTestQuery验证 SQL,常用 SELECT 1
JedistestOnBorrow借出时验证
LettucepingBeforeActivateConnection激活前 PING
RedissonpingConnectionInterval定时 PING 间隔
Apache Commons Pool2testOnBorrow通用对象池验证

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
#

相关文章

UUIDv4和v7两篇精彩文章-碰撞和性能

素材来源:HN UUID v4 碰撞帖、dev.to UUID Benchmark AI率99% 太长不看 # UUID v4 碰撞了——HackerNews 上有人真的撞了。原因是软件栈的 bug,不是数学。v4 和 v7 在碰撞安全性上没本质区别,差异在索引性能:v7 有时序,B-tree 更紧凑,写入快 35%、索引小 22%。你的 UUID v4 大概率没事,但如果你追求索引性能,换 v7 有实惠。

一个 DBA 视角看 0526 安可数据库名单

·1954 字·4 分钟
AI率 5% 太长不看 # 5 月 26 日,信创数据库名单 2026 年第 2 号发布,23 款产品通过(8 集中式 + 15 分布式),为历次最多。最值得关注:平安、银联、移动、电信四家大甲方各自孵化的数据库首次入围。信创逻辑变了——甲方不再只是买方的角色。