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