我不会起名字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。