news 2026/10/31 6:53:36

长连接空闲被断:keepalive 三个参数的真实含义

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
长连接空闲被断:keepalive 三个参数的真实含义

我不会起名字322· 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • 长连接空闲被断:keepalive 三个参数的真实含义
    • 一、现象:连接"莫名其妙"就用不了了
    • 二、TCP keepalive 的三个参数
      • `tcp_keepalive_time`:空闲多久开始探测
      • `tcp_keepalive_intvl`:探测间隔
      • `tcp_keepalive_probes`:探测几次算失败
    • 三、关键:内核参数是全局默认值,必须在 socket 上显式开启
      • `IdleConnTimeout` 和 keepalive 是两个东西
    • 四、应用层心跳 vs TCP keepalive
    • 五、带走物

长连接空闲被断:keepalive 三个参数的真实含义

一、现象:连接"莫名其妙"就用不了了

典型场景:

  • 服务跑了一晚上,早上第一批请求全部失败,报错connection reset by peer或者use of closed network connection;
  • 连接池里取出来的连接,第一次用就报错;
  • 加了健康检查之后问题消失,但不知道为什么。

根因通常是:连接空闲太久,被中间设备或者对端悄悄断开了,而你的程序不知道。

TCP 连接在没有数据传输时不会有任何报文。一条空闲连接可以静静躺几个小时,双方都不发包。问题在于路径上的设备(NAT 网关、负载均衡、防火墙)会按自己的超时时间清理空闲连接表项——比如阿里云 SLB 默认 900 秒、AWS NLB 默认 350 秒、很多公司的 NAT 网关是 300~1200 秒。

设备把表项删了,但你的两端都不知道。等你想发数据时:

  • 如果设备直接丢包 → 你等超时;
  • 如果设备回了 RST → 你收到connection reset。

这就是"长连接空闲被断"的完整链路。

二、TCP keepalive 的三个参数

Linux 的 TCP keepalive 就是为解决这个问题设计的,但它默认关着,而且默认时间特别长。

三个参数(注意单位是秒):

sysctlnet.ipv4.tcp_keepalive_time# 默认 7200(2 小时)sysctlnet.ipv4.tcp_keepalive_intvl# 默认 75(75 秒)sysctlnet.ipv4.tcp_keepalive_probes# 默认 9(9 次)

含义分别是:

tcp_keepalive_time:空闲多久开始探测

连接空闲超过这个时间后,内核开始发 keepalive 探测包。

默认 7200 秒(2 小时)太长了——如果你的 NAT 网关是 300 秒超时,那 2 小时后连接早就没了,探测还没开始。

这就是为什么必须显式调小它:

sysctl-wnet.ipv4.tcp_keepalive_time=600# 10 分钟

经验值:tcp_keepalive_time要小于路径上最小的空闲超时。不知道是多少的话,设 300~600 秒比较安全。

tcp_keepalive_intvl:探测间隔

开始探测后,每隔多久发一次探测包。默认 75 秒。

tcp_keepalive_probes:探测几次算失败

探测包发出去之后:

  • 收到响应 → 连接正常,重置计时器;
  • 没响应 → 隔intvl秒再发,连续probes次都没响应 →判定连接已死,内核关闭 socket 并通知应用。

所以一条"死连接"从开始探测到被判定死亡,最长耗时:

tcp_keepalive_time + tcp_keepalive_intvl × tcp_keepalive_probes = 7200 + 75 × 9 = 7875 秒 ≈ 2.2 小时(默认配置)

改成更实用的组合后:

600 + 15 × 3 = 645 秒 ≈ 11 分钟

三、关键:内核参数是全局默认值,必须在 socket 上显式开启

这是最容易被忽略的一步。调了sysctl只是改了默认值,socket 必须显式开启SO_KEEPALIVE才会生效。

Go 里默认其实是关着的,需要显式设置:

import("net""time""golang.org/x/sys/unix")dialer:=&net.Dialer{Timeout:5*time.Second,KeepAlive:60*time.Second,// 关键:开启 SO_KEEPALIVE,空闲 60s 开始探测}conn,err:=dialer.Dial("tcp",addr)// 如果想进一步控制 intvl 和 probes(Linux)iftc,ok:=conn.(*net.TCPConn);ok{raw,_:=tc.SyscallConn()raw.Control(func(fduintptr){unix.SetsockoptInt(int(fd),unix.IPPROTO_TCP,unix.TCP_KEEPINTVL,15)unix.SetsockoptInt(int(fd),unix.IPPROTO_TCP,unix.TCP_KEEPCNT,3)})}

net.Dialer.KeepAlive就是那个开关:不设(0)就不开,设了才开。

HTTP 也是同理:

tr:=&http.Transport{DialContext:(&net.Dialer{Timeout:5*time.Second,KeepAlive:60*time.Second,// 必须显式设}).DialContext,IdleConnTimeout:90*time.Second,// 空闲连接保留多久MaxIdleConnsPerHost:100,}

IdleConnTimeout和 keepalive 是两个东西

这俩经常被混为一谈:

作用位置语义
IdleConnTimeout应用层(Go 的 transport)空闲多久后 Go 主动关闭连接、从池里移除
TCP keepalive内核空闲多久后内核发探测包,判断连接是否还活着

IdleConnTimeout更可靠也更可控:它让 Go 在连接变"陈旧"之前就主动关掉,下次请求新建连接,根本不会用到死连接。

实践建议:

IdleConnTimeout 应该 < 路径上最小的空闲超时 比如 NAT 是 300s → IdleConnTimeout 设 90~120s

这样连接在被设备清理之前就被应用主动回收了,从源头避免了"用到死连接"。

四、应用层心跳 vs TCP keepalive

还有一个方案是应用层心跳(比如 Redis 的 PING、WebSocket 的 ping/pong、gRPC 的 keepalive ping)。

两者对比:

TCP keepalive应用层心跳
实现位置内核应用代码
能否穿越 NAT✅ 能刷新 NAT 表项✅ 能
能否检测"对端进程假死"❌ 不能(内核会回包,即使进程卡死)✅ 能
配置方式sysctl + socket 选项应用代码
开销极小(空包)取决于实现

关键差异:TCP keepalive 只能证明"对端内核还活着",证明不了"对端应用还能处理请求"。如果应用进程死锁了但内核正常,keepalive 依然会得到响应。

所以:

  • 防 NAT 超时→ TCP keepalive 足够;
  • 要检测对端是否真的可用→ 需要应用层心跳或健康检查。

gRPC 自带应用层 keepalive:

conn,_:=grpc.Dial(addr,grpc.WithKeepaliveParams(keepalive.ClientParameters{Time:30*time.Second,// 空闲 30s 发一次 pingTimeout:5*time.Second,// ping 超时PermitWithoutStream:true,// 没有活跃流时也发}),)

注意PermitWithoutStream:默认false,意味着没有活跃 RPC 时不发 ping——如果你想要空闲保活,必须设成true。

五、带走物

问题解法
空闲连接被 NAT/防火墙清理调小tcp_keepalive_time(sysctl)+socket 显式开启
用到已死的连接调小IdleConnTimeout,让应用主动提前回收
对端进程假死检测不到应用层心跳(gRPC keepalive ping 要设PermitWithoutStream=true)

一套可直接用的配置:

# 内核默认(对所有显式开启 SO_KEEPALIVE 的 socket 生效)sysctl-wnet.ipv4.tcp_keepalive_time=600sysctl-wnet.ipv4.tcp_keepalive_intvl=15sysctl-wnet.ipv4.tcp_keepalive_probes=3
// Go 侧dialer:=&net.Dialer{KeepAlive:60*time.Second}tr:=&http.Transport{DialContext:dialer.DialContext,IdleConnTimeout:90*time.Second,// 小于路径最小空闲超时MaxIdleConnsPerHost:100,// 默认 2 会导致疯狂建连}

一句话结论:默认的 keepalive 是 2 小时后才开始探测,这个时间比几乎所有 NAT 网关的超时都长——所以"改了 sysctl 但还是断"的真相,往往是 socket 根本没开SO_KEEPALIVE。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 6:52:54

10分钟上手Grounded-SAM:文本驱动图像分割入门指南

10分钟上手Grounded-SAM&#xff1a;文本驱动图像分割入门指南 【免费下载链接】Grounded-Segment-Anything Grounded SAM: Marrying Grounding DINO with Segment Anything & Stable Diffusion & Recognize Anything - Automatically Detect , Segment and Generate A…

作者头像 李华
网站建设 2026/10/31 6:50:48

WebAssembly在前端的应用(进阶篇)最佳实践与踩坑记录

本文深入探讨WebAssembly在前端的应用&#xff08;进阶篇&#xff09;&#xff0c;涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。 很多团队在前端性能优化场景中都会遇到与WebAssembly在前端的应用&#xff08;进阶篇&#xff09;相关的挑战。本文结合生产…

作者头像 李华
网站建设 2026/10/31 6:35:38

(最新)基于Springboot食品安全信息平台【附源码+文档】

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/31 6:31:50

部署安装jdk8\redis\mysql8\nginx

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华