
有一种问题,在系统表面风平浪静时悄悄蔓延:服务稳定,QPS 正常,CPU 也没爆,但某天你忽然发现 Nginx 响应开始卡顿,连接数猛涨,甚至整个服务池突然超时断崖式下跌。你怀疑代码、排查数据库、加机器、重启服务……什么都不灵。
真正的元凶——Keep‑Alive 连接泄漏。
它不像典型的崩溃那样立刻炸你一脸,而是像水管漏水,开始你根本看不到,但一天、一周过去,整个系统的 file descriptor 被悄悄榨干,资源压住回收不了,直到系统自己喘不上气挂掉。
你以为 Keep‑Alive 是性能优化的“捷径”?对,它能提升吞吐。但当你不设限、不监控、不收割,它也可能变成一群吃光你资源还不关门走人的“僵尸连接”。
所以,这篇文章我们就实打实聊一个问题:如何自动化检测这些鬼魅一样的 Keep‑Alive 泄漏。不靠猜,不靠肉眼,而是真·实战可执行的6种监控方法。
Keep‑Alive 本质上不是“长连接”,而是“未断连接”
很多人把 Keep‑Alive 想成“长连接”,好像它是专门为了维持连接而存在的。其实它是 HTTP 协议里的一个“协商机制”,允许客户端复用已有的 TCP 连接,跳过三次握手,减少开销。
简单说,就是:
“你先别挂,我一会儿还有别的请求。”
但问题是,“一会儿”到底是多久?没人说得准。
服务器这边保持连接,默认是出于好意,等着你。客户端那边如果不来、卡死、崩了、超时了……你就永远等下去。你的连接线程、socket、fd 资源,全都被绑住。
一台服务器撑得住几十个、几百个这种连接,但成千上万呢?尤其在高并发下,这种小概率的“未关闭连接”就会指数式堆积,最后压垮系统。
Keep‑Alive 泄漏,就这么发生了。
泄漏不可怕,不可见才可怕
很多团队根本不监控连接生命周期,只关注请求耗时、流量、状态码,错过了最关键的一条命脉:连接数变动趋势。
举个比喻:
- 你开的不是饭店,而是“自助餐+不限时卡座”
- 顾客吃完不走,一直坐在那,不点新单,也不离席
- 你看不出问题,因为他“没再吃”
- 直到下班你发现,所有桌子都被人“占着”,新客进不来……
所以,我们真正需要的是一种手段,能在连接看似“正常挂着”时,就识别出它是个“僵尸”。
下面这6种方法,就是为此而生。
方法一:系统层抓 ESTABLISHED 长时间挂起连接
这是最直接也是最容易忽略的一种方式。通过 ss
或 netstat
查看当前 TCP 连接状态。
bashss -ant | grep ESTAB | awk '{print $1,$2,$3,$4,$5}'
你会看到连接的状态、时间戳和远程地址。
关键指标:
- 连接状态为 ESTABLISHED 且持续存在 60 秒以上
- 远端无进一步请求但连接未释放
- 总连接数线性增长,释放速率极低
加上 conntrack
追踪效果更好:
bashcat /proc/net/nf_conntrack | grep tcp | grep ESTABLISHED | wc -l
把这个数定时采集进 Prometheus,一旦出现持续上涨、长时间不下降,就可以触发告警。
缺点:
- 系统层抓得粗,需要手动排查具体业务连接
- 不知道是哪段代码挂的
但优点是简单、无侵入,适合作为第一道防线。
方法二:Nginx stub_status + 连接生命周期分析
如果你有使用 Nginx 反代,那么它天然是个连接监控前哨站。
开启 stub_status
页面:
nginxlocation /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
访问返回如下:
yamlActive connections: 291
server accepts handled requests
398765 398765 878654
Reading: 2 Writing: 3 Waiting: 286
重点看:
- Active connections:当前总连接数
- Waiting:Keep-Alive 挂起连接数
你定时采集这个 /nginx_status
页面,通过 Prometheus 抓取 waiting/active
比例,如果这个值持续高企,极大可能是连接没被正确关闭。
延伸技巧:
- 配合
log_format
自定义输出连接生命周期 - 加入
$request_time
字段,判断是否有过长连接未请求
方法三:应用层打点暴露连接池生命周期
如果你用的是 Java/Tomcat、Go/Net HTTP、Node.js Express 等自建服务,可以在中间件层直接打点。
以 Go 为例:
gohttp.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
conn, _, err := w.(http.Hijacker).Hijack()
defer conn.Close()
start := time.Now()
...
duration := time.Since(start)
prometheusHistogram.Observe(duration.Seconds())
})
你可以记录:
- 每个连接的建立时间
- 活跃时间 vs 挂起时间
- 是否持续复用
然后再结合 connection_id
分析请求链路和生命周期。
这种方式虽然复杂,但能精确地识别应用级的连接泄漏来源。
方法四:eBPF 跟踪 socket 生命周期(内核级别)
如果你想玩点高端的——eBPF 就是你的工具。
通过工具如 bcc、bpftrace 或 cilium ebpf,可以挂载 hook 到 socket 创建/关闭事件:
bashsudo bpftrace -e 'tracepoint:tcp:tcp_set_state /args->state == 1/ { @[comm] = count(); }'
你能实时追踪:
- 哪个进程建立了 TCP 连接
- 哪些连接处于
ESTABLISHED
却长期未变化 - 哪些连接未释放,生命周期超过 X 秒
优点:
- 准确度高,性能损耗小
- 适合在问题难复现、持续挂起情况下使用
缺点:
- 配置和调试成本略高
- 对内核版本有依赖
方法五:Prometheus 中自定义告警规则(连接超时)
假设你已经把 Nginx 或服务端连接信息导入 Prometheus,接下来就是定义告警。
示例 PromQL:
promqlnginx_http_current_connections{state="waiting"} / nginx_http_current_connections{state="active"} > 0.9
这代表挂起连接占比超过90%。
再配合时间窗口触发:
yamlfor: 1m
alert: KeepAliveLeakSuspected
expr: ...
你也可以定义连接存活时间分布(需要服务端埋点):
promqlhistogram_quantile(0.95, rate(http_connection_duration_seconds_bucket[5m])) > 60
如果 P95 的连接时间大于60秒,很大概率是“僵尸”。
方法六:流量回放模拟重现连接挂起场景
最后一种更偏“排查型”。当你发现连接泄漏,却找不到原因,可以利用 tcpdump
或 Wireshark
抓包还原客户端行为。
bashtcpdump -i eth0 port 80 and 'tcp[tcpflags] & tcp-push != 0'
你抓下某一段时间的 TCP 会话,重放它,看看客户端是否存在:
- 不发送 FIN
- 不响应 Keep‑Alive 超时
- 一直保持连接但不发请求
甚至可以构造模拟请求,批量创建连接但不释放,观察服务端资源变化,用来回归测试连接策略是否生效。
监控不是终点,收口策略才是闭环
你监控到连接泄漏了,下一步怎么做?靠人工手动干掉连接?No。
你需要设置合理的连接生命周期策略,例如:
服务端:
keepalive_timeout 10;
keepalive_requests 100;
worker_connections 4096;
应用框架:
- Go:
IdleTimeout: 10 * time.Second
- Node.js:
server.keepAliveTimeout = 10000
客户端 SDK:
- 明确设置
Connection: close
或请求结束主动abort()
如果你使用的是云负载均衡或 API 网关,如 Kong、Envoy、Nginx Plus,也可以用连接配额和空闲清理功能做自动收口。
有些连接,它不是没事做,而是做了你没想过的事。
连接泄漏这种东西,最吓人的不是它爆了你,而是你压根不知道它存在。它就像你开会时,屋角落那个看起来没发言的人,会议结束你才发现他不是你部门的,甚至根本不是公司的人——他就坐那看了你一小时。
今天这6种方法就是用来把“看起来还活着但早该离场”的连接全揪出来。
别让僵尸吃掉你的线程池,也别让你的系统最后变成连接的坟场。