你有没有遇到过这种情况?某个网站在压测时表现还不错,QPS跑得飞起,延迟也还行。但一上线几天,Web服务就开始间歇性崩溃,甚至整个服务池响应超时,而 CPU、内存都没打满。这时候你以为是代码的问题,查了又查,结果:一切正常。直到有一天你突然发现——连接数爆炸了,而且是 Keep-Alive 连接没关干净。
你这才意识到:原来是连接泄漏了。
Keep-Alive 明明是用来提高性能的,怎么变成了隐形炸弹?监控不到它,它就悄悄把服务器拖垮。
我们今天就来掀开它的“伪装”,彻底搞清楚:高并发场景下,Keep-Alive 是怎么失控的?怎么监控?怎么预警?怎么防止它把你的服务器榨干又拍手走人?
HTTP Keep-Alive(也叫 Persistent Connection)允许客户端在完成一次请求后不立即关闭 TCP 连接,而是复用它来发后续的请求。少了三次握手,性能能提升不少,尤其在 HTTPS 上更明显,因为握手更贵。
但问题来了:连接是双向的资源绑定,服务器也要维护这个 TCP 状态,而且是“挂着”的。
高并发场景下一旦客户端不断发起请求却不主动断开,服务器就得不停地“养着”这些连接——每个连接都要占用 file descriptor、内存、线程池/连接池资源,甚至可能占用某些锁。如果你不给它一个明确的连接关闭机制,泄漏就悄无声息地开始了。
很多人对连接泄漏的理解是“连接池里忘了释放连接”——那是数据库连接。
Web 的 Keep-Alive 泄漏有三种经典场景:
比如浏览器默认 Keep-Alive 是 60 秒,而你服务器配置是 120 秒。这时候浏览器早就断开了,但服务器还傻傻地等着。连接成了僵尸,没人收尸。
像 Nginx 或 HAProxy,代理后端服务时,前后都可能开启 Keep-Alive。如果你没统一设置超时、重试策略、连接上限,轻则内存泄漏,重则连接爆表。
这是最坑的一种,尤其是自动化测试或批量调用 API 的程序。一个循环一万次的请求,Keep-Alive 默认开着,结果你服务端还以为是 10 个用户连接,实际每个都没断。
想要“监控”,首先你得知道“异常”长什么样:
这些通常可以通过系统级别指标如:
bash
netstat -anp | grep ESTABLISHED | wc -l
lsof -i | grep httpd | wc -l
ss -s
或者通过 Prometheus + Node Exporter 的 metrics,例如:
node_netstat_Tcp_CurrEstab
node_sockstat_TCP_alloc
当然,这些只是“外围信号”,更精确的还是要靠 Web 服务本身的指标。
如果你用的是 Nginx、Apache、OpenResty:
$connection_requests
、$connection_time
日志字段,查看连接请求次数与时间是否异常Active
, Reading
, Writing
, Waiting
四类状态如果你用的是 Java(Tomcat、Jetty)、Node.js、Go HTTP server:
req.socket.remoteAddress
与 req.keepAlive
net/http/httptrace
包追踪 TCP 生命周期这一步一般适合在“系统已被拖慢”但还没挂掉时紧急排查:
ss -ant
查看 ESTABLISHED 多到爆的 IP 和端口lsof -nP | grep TCP | wc -l
看是不是描述符满了netstat -an | grep 80 | grep ESTABLISHED
看具体连接来源tcpdump
配合 src host
或 dst port
分析流量模式(比如一直在发包但服务器没响应)如果你怀疑是短时间“被刷爆”,可以开启 conntrack
:
bash
cat /proc/sys/net/netfilter/nf_conntrack_max
cat /proc/sys/net/netfilter/nf_conntrack_count
当 count 接近 max 时,说明连接压到极限了。
建议在业务逻辑里埋点,例如:
go
log.Infof("连接建立:remote=%s keepAlive=%v", conn.RemoteAddr(), conn.SetKeepAlive)
也可以记录 headers:
Connection: keep-alive
Keep-Alive: timeout=60, max=1000
如果客户端没带 Keep-Alive
,但你服务端收到了几十秒的连接不释放,那问题基本在你这边。
光监控不行,还得设置告警阈值。
建议几个维度:
yaml
alert: WebConnectionLeak
expr: node_netstat_Tcp_CurrEstab > 5000
for: 1m
labels:
severity: critical
annotations:
summary: "连接数过高,疑似连接泄漏"
这类指标可以在应用层自定义打点,结合 Prometheus Histogram。
go
// 记录连接生命周期
connStart := time.Now()
defer func() {
connDuration.Observe(time.Since(connStart).Seconds())
}()
然后设置分位数阈值告警,比如 p95 超过 60 秒:
yaml
expr: histogram_quantile(0.95, sum(rate(connDuration_bucket[5m])) by (le)) > 60
这个可以用 fail2ban 或 eBPF 做防刷:
bash
ss -ant | awk '{print $5}' | cut -d':' -f1 | sort | uniq -c | sort -nr | head
出现某个 IP 连接数 > 200,就封掉它。
处理连接泄漏,核心是“别让连接挂着不死”。
以 Nginx 为例:
nginx
keepalive_timeout 15;
keepalive_requests 100;
worker_connections 10240;
Node.js 示例:
js
server.keepAliveTimeout = 15000; // 15 秒
server.headersTimeout = 17000;
Go 示例:
go
&http.Server{
IdleTimeout: 15 * time.Second,
}
Java 示例:
xml
爬虫/工具测试时一定设置:
h
Connection: close
或者显式 .destroy()
/.abort()
来断开连接。
HTTP/2 默认复用连接并带有流控机制,比 HTTP/1.1 更能限制“连接泛滥”。而且部分框架(如 gRPC)已经内建 Keep-Alive 策略管理。
你还可以加一些“高阶手段”:
Keep-Alive 本意是提升性能,但在高并发场景下,它就像一条看不见的藤蔓,悄悄缠住了你的服务器。它不会立刻让系统崩溃,但它会持续蚕食资源,直到临界点到来,一切“突然”爆炸。
别相信“再加两台机器就能撑住”,真正的解决办法,是 看见它、理解它、控制它。
你给不了连接“边界”,它就会反过来控制你。