昨晚三点,你的告警又响了。服务挂了,你爬起来一顿操作:重启、扩容、回滚,忙活半小时终于恢复。第二天复盘,所有人都说「代码没问题」「服务器没问题」「网络没问题」。那问题在哪?
答案可能藏在一个你从来不怎么关注的角落里——TCP Keep-Alive。
TCP Keep-Alive是什么?
简单说,TCP Keep-Alive是TCP协议栈里的一个「心跳检测」机制。它干的事情很简单:客户端和服务端建立连接后,如果一段时间内没有任何数据传输,系统会自动发送一个「探测包」来确认对方还活着。
听起来很美好对吧?但魔鬼在细节里。
Linux下,Keep-Alive有三个核心参数:
- tcp_keepalive_time:连接空闲多久后开始探测。默认7200秒,也就是两个小时!
- tcp_keepalive_intvl:探测间隔,默认75秒
- tcp_keepalive_probes:探测失败多少次后判定连接死亡,默认9次
算一下:从连接空闲到系统最终判定它死亡,最长需要 7200 + 75×9 = 7875秒,超过两个小时。这意味着你的服务可能已经挂了,但操作系统还在傻乎乎地维护一个「僵尸连接」。
你可能遇到的真实场景
场景一:NAT网关超时
很多云服务商的NAT网关或负载均衡器有自己的连接超时机制,通常是60秒到300秒不等。如果你的TCP Keep-Alive比网关超时时间还长,那网关会先于系统判定连接死亡,但你以为连接还活着。
这时候你会发现奇怪的现象:连接没有报错、没有任何异常,但数据就是发不出去、收不到。因为数据包被网关静默丢弃了。
场景二:中间件悄悄断开连接
企业内网经常有各种代理、网关、防火墙。它们可能会在连接空闲一段时间后悄悄断开,但不会通知任何一方。你的应用日志里看不到任何错误,TCP连接状态看起来也是正常的,但实际已经是个死连接。
场景三:云服务器入站连接超时
阿里云、腾讯云等云服务商默认的入站连接超时不一定是7200秒。我见过最离谱的案例是客户用的某云负载均衡,默认超时60秒,而他们的应用Keep-Alive参数根本没改,客户端还在用两个小时前的连接发请求,结果自然是一脸懵。
你的应用真的开了Keep-Alive吗?
等等,很多人会说:「我用的是HTTP,HTTP有Keep-Alive啊!」
这里有个巨大的误解。HTTP的Keep-Alive和TCP的Keep-Alive是两码事。
HTTP Keep-Alive(也叫HTTP Connection复用)是指在一个HTTP请求结束后,不关闭TCP连接,继续用这个连接发下一个HTTP请求。这样可以避免频繁建立TCP连接的开销。
但这个「长连接」有前提条件:得有实际的数据传输。如果你的HTTP请求发完了、客户端在等、什么都没干,那HTTP层面根本不会有什么Keep-Alive——它只是在复用连接,不是在保活。
而TCP Keep-Alive是操作系统层面的,不管你应用层在干什么,它都会在空闲时发探测包。
开发者觉得「我用了HTTP Keep-Alive,连接不会断」,但实际上TCP连接可能早被网络设备干掉了,只是应用层不知道而已。
实战:如何正确配置TCP Keep-Alive
方案一:系统层面配置
# 查看当前配置
cat /proc/sys/net/ipv4/tcp_keepalive_time
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
cat /proc/sys/net/ipv4/tcp_keepalive_probes
# 修改配置(需要root)
echo 60 > /proc/sys/net/ipv4/tcp_keepalive_time
echo 10 > /proc/sys/net/ipv4/tcp_keepalive_intvl
echo 5 > /proc/sys/net/ipv4/tcp_keepalive_probes
# 永久生效,修改/etc/sysctl.conf
net.ipv4.tcp_keepalive_time = 60
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 5
这样配置后,空闲连接会在60秒后开始探测,每10秒探测一次,最多5次。从空闲到判定死亡最多70秒,比默认的两个小时靠谱多了。
方案二:应用层代码设置
如果你没有服务器权限,或者想针对不同应用设置不同的参数,可以在应用层配置。以Go为例:
conn, err := net.DialTimeout("tcp", address, 10*time.Second)
if err != nil {
log.Fatal(err)
}
// 设置Keep-Alive
fileConn, err := conn.(*net.TCPConn).File()
if err != nil {
log.Fatal(err)
}
err = fileConn.SetKeepAlive(true)
if err != nil {
log.Fatal(err)
}
err = fileConn.SetKeepAlivePeriod(30 * time.Second)
if err != nil {
log.Fatal(err)
}
Python版:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
方案三:使用HTTP Client的正确姿势
如果你用的是HTTP客户端库,别忘了配置超时和连接池。以Python requests为例:
import requests
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=10,
pool_maxsize=20,
max_retries=3,
pool_block=False
)
session.mount('http://', adapter)
session.mount('https://', adapter)
# 设置整体超时和读取超时
response = session.get(url, timeout=(5, 30)) # (连接超时, 读取超时)
但这里有个坑:requests的timeout参数是「这个请求的最长等待时间」,不是「Keep-Alive时间」。requests底层用的是urllib3,它默认没有开启TCP Keep-Alive。如果你需要长连接保活,还是要在系统层面配置。
怎么知道Keep-Alive有没有生效?
调试工具来了:
# 查看连接状态
ss -tna
# 或者
netstat -tna | grep ESTABLISHED
# 查看Keep-Alive探测情况
cat /proc/net/sockstat
# 查看各个状态的连接数
ss -s
如果你看到连接状态是ESTABLISHED但实际上已经死了(应用层感知不到数据),那就是遇到了前面说的场景——中间件静默断连。
如果你发现TIME_WAIT状态的连接特别多,可能需要调整tcp_tw_reuse参数,让系统重用TIME_WAIT状态的连接。
一个真实的踩坑案例
之前帮一个客户排查问题,他们的Java服务每隔几分钟就会有一批请求超时,但服务本身没挂、重启后正常、一会儿又挂。日志里没有任何异常。
排查过程:
- 看GC——正常
- 看线程——正常
- 看数据库连接池——正常
- 看网络——抓包发现大量TCP Retransmission(重传)
- 最终定位:服务端Keep-Alive参数是默认的7200秒,但客户端每30秒会发一次心跳。但他们的请求路由经过了一个代理网关,网关超时是60秒。当客户端心跳间隔刚好略长于网关超时,连接被网关断掉,但服务端不知道。
解决方案:把服务端的tcp_keepalive_time从7200改成30,和客户端心跳频率匹配。从此告警再没响过。
总结一下
TCP Keep-Alive是个容易被忽视但极其重要的机制。它不是银弹,但在很多「莫名其妙」的连接问题排查中,它往往是关键。
- TCP Keep-Alive和HTTP Keep-Alive是两回事,别混为一谈
- 默认的7200秒超时在现代服务架构中基本不可用,网络设备超时通常远短于此
- 应用层要配置合理的超时,不能依赖「连接还活着就没事」
- 配合连接池和重试机制,让系统具备自我修复能力
- 做好监控,监控连接状态、TCP重传率、Keep-Alive探测失败次数
下次你的服务再莫名其妙挂掉,别急着甩锅给代码。先查查TCP Keep-Alive,说不定有惊喜。
毕竟,真正的高手不是让问题不发生,而是让问题发生时能快速定位到根因。