tcp keep-alive默认2小时触发因内核硬编码参数tcp_keepalive_time=7200秒,且中间设备静默丢弃探测包;必须用应用层心跳闭环(发ping、收pong、更新deadline、触发业务逻辑)才能可靠检测连接活性。

为什么TCP Keep-Alive默认2小时才触发探测
因为这是Linux内核硬编码的行为,由三个不可调用的系统参数控制:tcp_keepalive_time(默认7200秒)、tcp_keepalive_intvl(默认75秒)、tcp_keepalive_probes(默认9次)。哪怕你调用tcpConn.SetKeepAlivePeriod(30 * time.Second),在旧内核或容器环境里也大概率被四舍五入或直接忽略。真实场景中,最早也要等2小时+675秒才能收到ECONNRESET——业务根本等不了。
中间设备会主动kill空闲连接,且不看TCP Keep-Alive包
云厂商负载均衡器(AWS ALB、阿里云SLB)、家用路由器、运营商NAT网关,普遍按自身策略断连:超时阈值多为5~30分钟,且它们压根不转发或响应TCP层的keepalive探测包。你发的keepalive包被静默丢弃,而conn.Write()仍返回成功,conn.Read()却永远阻塞。这种“单向假活”状态,只有应用层心跳能戳破。
只靠SetReadDeadline无法感知静默断连
SetReadDeadline只在下次调用Read()时生效,如果连接空闲、服务端没再读,这个deadline就形同虚设。常见错误包括:
- 只设一次
conn.SetReadDeadline(time.Now().Add(30 * time.Second)),后续无重置 - 收到业务数据后忘记更新deadline,导致下一次心跳响应超时误判
- 把心跳响应逻辑写在
Write()之后但没Read(),等于只发不验
正确做法是:每次Read()前都动态重设deadline,且必须配合双向验证——发PING后立刻Read()期待PONG,超时或返回io.EOF/net.OpError才判定失效。
应用层心跳必须走完整业务路径,不能只测传输层
一个TCP连接可写,不代表服务可用。RPC服务可能goroutine全卡在DB锁上,Write()仍成功,但Ping()方法永不返回。所以心跳包必须触发真实业务逻辑:
- 服务端收到
PING帧,必须调用注册的PingHandler或解析并应答自定义帧头 - 心跳内容建议带时间戳或序列号(如
binary.Write(&buf, binary.BigEndian, uint64(time.Now().UnixNano()))),防重放和伪造 - 避免用JSON、换行符、空格等非确定性格式,曾有服务因
{"ts":"2026-05-23 04:27:00"}含空格被代理截断
最易被忽略的一点:心跳不是“发出去就完事”,而是“发出去 + 收到有效响应 + 更新活跃时间戳 + 下次读前重设deadline”的闭环。少一环,就可能让数百个僵尸连接在内存里躺平三天。











