TCP Keep-Alive在真实网络中基本失效,因其依赖系统默认参数(如2小时空闲后才探测),且云负载均衡器、NAT网关等中间设备不转发或响应探测包,仅按自身超时策略(5–30分钟)无差别回收连接。

net.Conn.Read 不返回 io.EOF,不代表连接还能收发业务数据——这是应用层心跳不可替代的根本原因。
为什么 TCP Keep-Alive 在真实网络中基本失效
TCP Keep-Alive 是内核行为,依赖三个系统级参数:tcp_keepalive_time(默认 7200 秒)、tcp_keepalive_intvl(默认 75 秒)、tcp_keepalive_probes(默认 9 次)。这意味着:即使对端已断电,本端最早也要等 2 小时 + 675 秒才可能感知断连。
更关键的是:云负载均衡器(AWS ALB、阿里云 SLB、华为云 ELB)、家用路由器、企业 NAT 网关根本不会转发或响应 TCP Keep-Alive 探测包。它们只按自身空闲超时策略(常见为 5–30 分钟)无差别回收连接。
Go 的 tcpConn.SetKeepAlivePeriod(30 * time.Second) 仅在 Linux 3.7+ 生效,且仍受 tcp_keepalive_time 底线限制;调用 net.Dialer.KeepAlive 只影响 HTTP 连接池,不作用于裸 net.Conn。
SetReadDeadline 只设一次 = 白设
conn.SetReadDeadline 仅在下一次 Read() 调用时生效,之后自动失效。若连接空闲、服务端不再读取,该 deadline 就永远不会触发。
- 典型误用:
conn.SetReadDeadline(time.Now().Add(30 * time.Second))只调一次 → 后续所有Read()都无超时约束 - 正确做法:每次
Read()前都重置 deadline,且必须结合心跳响应验证(例如收到"ping"后立刻回"pong"并重置) - 静默断连时,
Write()可能仍成功(数据进发送缓冲区),但Read()卡死,goroutine 和文件描述符持续泄漏
心跳必须双向验证,且走完整业务路径
单向发包(如只写 []byte{0x01})不是心跳,是噪音。真正有效的心跳需满足:
- 客户端发
"ping",服务端必须解析、应答"pong",并重置该连接的ReadDeadline - 服务端收到
"ping"后,不能只回包,还要调用业务层注册的健康检查逻辑(如PingRPC()或 DB 连通性探测)——否则无法发现 goroutine 卡死、DB 锁死等“连接活着但服务瘫痪”的情况 - 响应建议带时间戳或序列号(如
binary.Write(&buf, binary.BigEndian, uint64(time.Now().UnixNano()))),防重放和伪造 - 客户端心跳定时器(
time.Ticker)只负责发,不阻塞等响应;存活判定交给下一次Read()是否超时——轻量、可靠、无竞态
心跳间隔与超时时间怎么设才不翻车
没有通用最优值,但有硬约束:
- 心跳间隔必须 短于中间设备空闲阈值:查你所用 LB/NAT 文档,常见为 5–30 分钟;生产环境建议设为 25–30 秒
-
ReadDeadline必须 大于心跳间隔但小于设备阈值:例如设心跳 30 秒,则ReadDeadline = 45 秒;设 60 秒心跳,则 deadline 至少 90 秒 -
WriteDeadline要设(常为 5 秒),否则Write()在对端关闭后可能永久阻塞 - 别用指数退避做心跳——那是重连策略,心跳必须稳定、可预测、可监控
最易被忽略的一点:心跳协议语义必须由你自己定义并强制执行。OS 和 Go runtime 都不知道你的 "ping" 意味着什么,也不关心你回的 "pong" 是不是真从业务逻辑里跑出来的。

















