keepalive-timeout

长连接空闲一段时间后被固定秒数断开,复用连接时偶发 reset 或 EOF。

现象

HTTP 客户端、数据库连接池或 gRPC 连接在空闲后复用时失败,报 connection reset by peer、EOF、server closed the connection unexpectedly;断开时间往往固定(如 60 秒、350 秒、5 分钟)。新连接通常正常。

快速定位

先确认断开时间是否固定,以及各层 keepalive/idle 配置:

ss -tno | grep -E 'keepalive|timer'
ss -tno state established '( dport = :3306 or dport = :5432 or dport = :6379 )'
sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes

代理与应用层:

nginx -T | grep -E 'keepalive( |_timeout|_time)|proxy_read_timeout|proxy_send_timeout'
grep -R 'IdleTimeout\|IdleConnTimeout' .

数据库侧对照(idle_session_timeout 需 PostgreSQL 14+):

mysql -e "SHOW VARIABLES LIKE 'wait_timeout'; SHOW VARIABLES LIKE 'interactive_timeout';"
psql -c "SHOW idle_session_timeout;" -c "SHOW tcp_keepalives_idle;"
redis-cli CONFIG GET timeout

云 LB/NAT 的空闲超时也要核对:

aws elbv2 describe-load-balancer-attributes --load-balancer-arn <arn> --query 'Attributes[?Key==`idle_timeout.timeout_seconds`]'
conntrack -L -p tcp --dport 443 | head

易误判:Nginx 的 keepalive_timeout 不是 TCP keepalive;upstream 块里的 keepalive 是连接池大小,keepalive_time 才是空闲时长。TCP keepalive 也不能替代应用心跳去维持 LB/NAT 的空闲计时 —— 探测包常常不被转发或不被计为活跃流量。固定 60 秒断开更像 ALB/Nginx,固定 350 秒更像 NLB/NAT;数据库报 “server closed the connection” 通常是 DB 或代理先关闭,而不是客户端。

常见根因

  1. 中间层 idle timeout 小于应用心跳/keepalive,连接被静默关闭后仍被复用。
  2. HTTP 客户端连接池 idle 时间大于服务端 keepAliveTimeout,复用时撞上正在关闭的连接。
  3. 数据库池 maxLifetime 大于 DB wait_timeout 或代理超时,拿到失效连接。
  4. TCP keepalive 默认 2 小时,远大于 NAT/LB 空闲超时,探测来不及保活。
  5. keepalive 探测不被中间设备转发,无法阻止 idle 关闭。
  6. 后端发布/重启未优雅排空,连接被直接断开。
  7. HTTP/2/gRPC keepalive 频率违反服务端 enforcement,收到 too_many_pings/GOAWAY。
  8. keepalive 设置过短,造成额外握手、端口消耗或触发限流。

修复

相关命令

读完自测

  1. Nginx 的 keepalive_timeout 与 TCP keepalive 是同一件事吗?
  2. 数据库连接池的 maxLifetime 应如何与 wait_timeout 配合?
  3. 为什么 TCP keepalive 不能阻止云 LB 断开空闲连接?
答案
  1. 不是;keepalive_timeout 是 HTTP 连接空闲超时,TCP keepalive 是内核探测参数。
  2. maxLifetime 应短于 wait_timeout 或代理超时(通常再留 30–60 秒余量),并配合连接有效性检测。
  3. LB 的空闲计时通常只看应用数据,keepalive 探测往往不被转发或不计为活跃流量。