典型原因是应用层未及时读取或关闭连接导致内核缓冲区持续堆积;可通过ss -m查rmem/wmem、strace跟踪recv/read调用、tcpdump分析流量、检查setsockopt设置等交叉验证定位。

当 ss -m 显示某个 socket 的 rmem 或 wmem 达到几 MB 甚至上百 MB,而对应进程的 RSS(ps aux --sort=-rss 或 /proc/PID/status 中的 RSS)却只有几百 KB,这通常不是内存统计口径差异的问题,而是典型的 **socket 接收/发送队列积压未消费**,即“socket 泄漏”的一种表现——更准确地说,是 **应用层未及时读取或关闭连接导致内核缓冲区持续堆积**。
确认是否为接收队列(rmem)积压
运行 ss -tulnmp | grep :PORT(替换 PORT),重点关注 Recv-Q 和 rmem 字段:
-
Recv-Q非零且持续增长 → 应用未调用recv()/read()消费数据 -
rmem值远大于sysctl net.core.rmem_max(如显示 2MB 但 rmem_max 是 212992)→ 内核已突破默认上限,说明该 socket 被长期持有且数据不断写入 - 配合
cat /proc/PID/fd/ | wc -l查看 fd 数是否异常增长,可辅助判断是否真有大量 socket 未 close
检查应用是否卡在阻塞 I/O 或逻辑死锁
很多服务(如 Python 的 socket.recv()、Node.js 的 net.Socket、Java 的 InputStream.read())默认使用阻塞模式。一旦对端发来数据但业务逻辑未处理(比如反序列化失败、回调未注册、线程池耗尽),就会导致 recv 缓冲区越堆越多:
- 用
strace -p PID -e trace=recvfrom,read,close观察是否有长时间无系统调用返回,或recvfrom返回 0(对端 FIN)但进程没 close - 检查日志中是否有反序列化异常、超时重试循环、空指针导致 handler 退出但 socket 未释放等逻辑缺陷
- Golang 程序要特别注意 goroutine 泄漏:启动了
go handleConn()却因 channel 阻塞或 panic 未 recover,导致 conn 对象无法被 GC,底层 socket 一直存活
排查 SO_RCVBUF/SO_SNDBUF 手动设置过大或禁用自动调优
某些程序显式调用 setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &val, sizeof(val)) 并设为极大值(如 16MB),又关闭了 TCP autotuning(net.ipv4.tcp_rmem 第三项被绕过),会导致单个 socket 占用内核内存失控:
- 用
bpftrace -e 'kprobe:tcp_setsockopt { printf("pid=%d, optname=%d\\n", pid, arg2); }' | grep -E "(SO_RCVBUF|SO_SNDBUF)"监控可疑 setsockopt 调用 - 检查代码中是否硬编码了超大 buffer,并确认是否调用了
setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, ...)(需 CAP_NET_ADMIN) - 临时修复:改小
net.core.rmem_max(如sysctl -w net.core.rmem_max=4194304),观察新连接是否仍堆积;长期应删掉非必要手动 setsockopt
验证是否由对端疯狂发包但本端无响应引起
即使你的程序逻辑正常,若上游客户端不遵守流控(如 UDP 打洞后误用 TCP、IoT 设备固件 bug),也会造成单边堆积:
- 用
tcpdump -i any port PORT -w debug.pcap抓包,过滤出该 socket 的四元组,观察是否有大量重复 ACK、ZeroWindow、或持续的 PSH+ACK 数据包涌入 - 检查
/proc/net/snmp中Tcp:行的InErrs、AttemptFails是否突增,可能暗示连接建立失败后重传风暴 - 在服务端加限速:用
tc qdisc add dev eth0 root tbf rate 1mbit burst 32kbit latency 400ms临时抑制流量,看 rmem 是否回落
这类问题本质是应用与内核协作失衡,核心不在“泄漏”而在“停滞”。定位关键在于交叉比对 ss -m、strace、tcpdump 和业务日志,找到那个“收得到但不敢/不能处理”的临界点。









