TCP keepalive 询问的是:连接安静了一段时间后,对面的 TCP 端点是否仍然响应。它不询问 HTTP handler 是否正常、数据库查询是否推进、后台工作线程是否卡住。即使应用已经停顿,内核仍然可能确认收到的数据。因此,“传输对端能够回应”和“应用给出了有用结果”必须作为两个不同的观察项。
传输层探针不是应用心跳
保活有助于暴露远端连接状态已经消失,或者网络路径悄悄停止传递报文的情况。实现会在满足空闲条件后发送探针,并按照未响应探针的策略最终报告失败;但单个探针丢失不等于对端死亡,不能把网络中的临时丢包直接解释成确定性故障。
探针在应用字节流以下处理,不会变成 HTTP 请求、数据库查询或 WebSocket 心跳。如果连接终止在代理上,探测对象就是代理自身的 TCP 端点,不是代理后面的源站。某些中间设备的连接跟踪可能受探针影响,但不能由此承诺刷新应用的空闲计时,更不能保证所有 NAT 映射都会一直保留。
已经发出而尚未确认的数据还有自己的重传与失败规则,因此保活参数不是繁忙连接的通用总截止时间。正常 FIN、连接重置、应用读取超时和静默丢包是不同事件,恢复动作也未必相同。知道是哪一层报告了什么错误,比统一把它们写成“连接死了”更有用。
配置真正承载请求的 socket
RFC 1122 对可选保活机制要求默认关闭,并规定默认空闲间隔不得短于两小时且应可配置。但应用和库可以显式启用并选择更短间隔,Go 的拨号器就有自己的设置。因此,查到操作系统默认值,并不能直接推出当前应用实际使用的策略;还需要沿着创建连接的代码检查有效配置。
Linux 的 tcp_keepalive_time、tcp_keepalive_intvl 和 tcp_keepalive_probes 分别表示探测前的空闲秒数、探针间隔和未响应探针数量,文档默认值为 7200、75、9。启用 SO_KEEPALIVE 后,还可以通过 TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT 为单个 socket 定制策略。这些名称与接口不是 macOS、Windows 通用的系统参数,不能跨平台照搬命令。
应根据期望发现故障的时间、连接规模、短暂丢包容忍度以及中间设备策略选择间隔。把空闲时间加上探针预算,只能得到条件性的估算,不是硬实时承诺。优先限定到目标连接,避免为了一个服务修改影响其他进程的全局默认值;同时检查设置操作是否成功,并确认 socket 上实际启用了保活。
坏掉的样子
只配置 keepalive、没有应用读取期限,请求仍可能长时间等不到业务回复。反过来,应用读取期限已到,也不代表 TCP 连接必然失效,更不能据此证明对端进程死亡。本地超时后贸然重发写操作,可能重复执行对端已经收到的任务。
代理更短的空闲超时仍可能先关闭连接,端点设置了保活也不能替代代理的业务策略。在大量连接上使用过于频繁的探针,还会增加网络流量和耗电。修改参数前,应定位实际终止连接的那一跳,以及触发恢复的具体错误信号,不要同时修改三个计时参数后仅凭“看起来好些了”判断原因。
快速验证
将代码保存为 keepalive_probe.go,在 macOS 或 Linux 上用 Go 1.23 或更新版本执行 go run keepalive_probe.go。读取 SO_KEEPALIVE 的部分使用 Unix socket 接口,因此这份原样代码不是 Windows 操作配方。它只创建回环连接,不修改 sysctl、防火墙或共享服务。
package main
import (
"errors"
"fmt"
"io"
"net"
"os"
"runtime"
"syscall"
"time"
)
func must(err error) {
if err != nil {
panic(err)
}
}
func check(ok bool, message string) {
if !ok {
panic(message)
}
}
func keepalive(connection *net.TCPConn) int {
raw, err := connection.SyscallConn()
must(err)
var value int
var optionError error
must(raw.Control(func(fd uintptr) {
value, optionError = syscall.GetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE)
}))
must(optionError)
return value
}
func main() {
listener, err := net.ListenTCP("tcp4", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)})
must(err)
defer listener.Close()
must(listener.SetDeadline(time.Now().Add(2 * time.Second)))
dialer := net.Dialer{Timeout: 2 * time.Second, KeepAlive: -1}
connection, err := dialer.Dial("tcp4", listener.Addr().String())
must(err)
client := connection.(*net.TCPConn)
defer client.Close()
peer, err := listener.AcceptTCP()
must(err)
defer peer.Close()
check(keepalive(client) == 0, "keepalive must start disabled")
must(client.SetKeepAliveConfig(net.KeepAliveConfig{
Enable: true, Idle: 30 * time.Second,
Interval: 10 * time.Second, Count: 3,
}))
enabled := keepalive(client)
check(enabled != 0, "SO_KEEPALIVE must be enabled")
fmt.Printf("SO_KEEPALIVE: 0 -> %d (enabled); per-socket configuration accepted\n", enabled)
must(client.SetWriteDeadline(time.Now().Add(2 * time.Second)))
n, err := client.Write([]byte("ping"))
must(err)
check(n == 4, "request must be fully written")
must(client.SetReadDeadline(time.Now().Add(200 * time.Millisecond)))
reply := make([]byte, 4)
n, err = client.Read(reply)
check(n == 0 && errors.Is(err, os.ErrDeadlineExceeded), "silent application must reach read deadline")
fmt.Println("read deadline fired while the peer application was silent")
must(peer.SetDeadline(time.Now().Add(2 * time.Second)))
request := make([]byte, 4)
_, err = io.ReadFull(peer, request)
must(err)
check(string(request) == "ping", "the original request is still queued")
n, err = peer.Write([]byte("pong"))
must(err)
check(n == 4, "reply must be fully written")
must(client.SetReadDeadline(time.Now().Add(2 * time.Second)))
_, err = io.ReadFull(client, reply)
must(err)
check(string(reply) == "pong", "same connection must deliver the reply")
fmt.Println("after resetting the read deadline, the original connection returns pong")
fmt.Println(runtime.Version(), runtime.GOOS)
}
拨号器先明确禁用保活,再使用单连接配置启用它。getsockopt 返回的启用标志必须是非零,但不保证恰好等于一;已核验的 macOS、Go 1.27.0 环境返回八。对端应用刻意不读取已经发送的四字节请求,客户端在二百毫秒的读取期限到达后得到超时错误,而不是收到业务数据。
之后,对端读取原来排队的 ping,并写回 pong。客户端重新设置读取截止时间,就能在同一条连接上收到回复,期间没有重新建连,也没有重发请求。这个现象说明超时只限制了本地等待,并没有撤回已进入字节流的数据,更没有自动取消远端工作。是否继续使用连接和如何恢复业务,需要由上层协议决定。
全部断言已经通过。这个短实验验证的是 socket 配置与应用读取期限,不是保活探针的真实发送时序:配置的首次探测空闲间隔是三十秒。它没有注入丢包,也没有测量经过 NAT 后的故障发现时间。验证那些能力,需要另行安排受控网络实验,保留报文证据,并检查报告故障之后的资源释放与安全恢复行为。
延伸阅读
来源
- RFC 1122:互联网主机要求,第 4.2.3.6 节:可选保活、空闲条件与探针丢失,已核验 2026-09-09。
- Linux tcp(7),保活默认值、socket 选项与更短的中间设备超时,已核验 2026-09-09。
- Go net,KeepAliveConfig、socket 配置与截止时间语义,已核验 2026-09-09。