本文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都有正常情况和异常情况发的,总结几个要点:
- 进程退出或程序abort发的是FIN包,这包括KILL -9(已验证PG进程kill -9发FIN包,见"测试"部分)
- 端口不可达等网络不可用是RST包
- TCP keepalive超时也是RST包,因为探测出了网络不可用
- 防火墙也有可能做RESET
- 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 1、PING;SELECT 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 通用 | testOnBorrow、testOnReturn、testWhileIdle |
借出/归还/空闲时验证 |
| 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
- https://raw.githubusercontent.com/redis/redis/refs/heads/4.0/src/redis-cli.c
- https://redisson.pro/docs/configuration/
- https://docs.paic.com.cn/#/post/57844638
- https://support.huaweicloud.com/intl/en-us/dcs_faq/dcs-faq-211230001.html
- https://howtodoinjava.com/spring-data/spring-boot-redis-with-lettuce-jedis/
- https://github.com/redis/lettuce/wiki/Connection-Pooling
- https://redis.github.io/lettuce/advanced-usage/client-options/
- https://redis.github.io/lettuce/advanced-usage/connection-pooling/
- https://blog.csdn.net/u014495560/article/details/103576786
- https://www.man7.org/linux/man-pages/man7/socket.7.html
- https://www.man7.org/linux/man-pages/man7/tcp.7.html
- https://www.postgresql.org/docs/18/runtime-config-connection.html
- https://www.postgresql.org/docs/18/libpq-connect.html
- https://linuxvox.com/blog/what-is-the-reason-and-how-to-avoid-the-fin-ack-rst-and-rst-ack/
- https://docs.oracle.com/cd/E13189_01/kodo/docs324/ref_guide_dbsetup.html