Linux 网络问题系统化排查方案

阅读模式

一、问题背景

现象:

  • 客户端上看:到某服务器端口的 ESTABLISHED TCP 连接大约有 3 万
  • 服务器上看:对应的连接只有 3000 条左右

怀疑:大量“幽灵连接”只存在于客户端视角,服务器已经不认账,或者双方根本统计的不是同一类东西。

目标:给出一套系统化排查步骤,澄清:

  1. 两边是不是在数同一种连接
  2. 如果真有“客户端 ESTABLISHED、服务器没有”的连接,它们是怎么产生的
  3. 对应有哪些配置/架构上的解决方案

二、第一步:先把“数法”统一(修正 ss 用法)

这一步非常关键,否则后面全是玄学。

2.1 一律用 ss,不用 netstat

ssnetstat 更快更准确,语法也更强。我们约定:

  • 只看 TCP-t
  • 不做域名解析:-n
  • 过滤状态:state established
  • 加上 IP/端口方向(src/dst + sport/dport

2.2 客户端:统计“到服务器 X:PORT 的 ESTABLISHED”

假设服务器是:

  • 服务器 IP:111.200.212.155
  • 端口:6449

客户端 上统计所有连到这个服务器端口的 ESTABLISHED:

1
ss -tan 'state established and dst 111.200.212.155 and dport = :6449' | wc -l

说明:

  • dst 111.200.212.155:对端(远端)IP
  • dport = :6449:对端端口 = 6449
  • 整个 filter 用单引号包住,避免被 shell 乱拆

也可以先不 wc -l,看具体连接:

1
ss -tanp 'state established and dst 111.200.212.155 and dport = :6449'

-p 可以看到哪个进程开了连接。

2.3 服务器:统计“本地 6449 端口上的 ESTABLISHED”

服务器 上,监听端口是本地的“源端口”,所以要换成 sport

1
# 所有到本机 6449 的 ESTABLISHED 连接(不区分客户端 IP)
2
ss -tan 'state established and sport = :6449' | wc -l

如果你只关心某个客户端 IP,比如 10.0.0.123

1
ss -tan 'state established and src 10.0.0.123 and sport = :6449' | wc -l

总结一下端口方向:

  • 在客户端看服务器:用 dport
  • 在服务器看自己监听端口:用 sport

2.4 避免的典型错误写法

错误示例(会触发 bison bellows 那种错误):

1
ss -tan "111.200.212.155" dport = :6449 state established

问题:

  • 111.200.212.155 没有加 src/dst 前缀,语法非法
  • 多个 token 没用引号包起来,shell 和 ss 都看不懂你想要啥

结论:所有复杂 filter 都老老实实放进单引号里,并注明 src/dstsport/dport


三、第二步:确认“幽灵连接”是否真的只存在客户端

有了统一的统计方法之后,开始验证“3w vs 3k”是不是事实。

  1. 在客户端查一条具体连接:
1
ss -tan 'state established and dst 111.200.212.155 and dport = :6449' | head -5

拿其中一条,记住四元组:

  • client_ip:client_port -> 111.200.212.155:6449
  1. 在服务器上查这条连接:
1
ss -tan "state established and src client_ip and sport = :6449"

再用 grep client_port 过滤一下:

1
ss -tan 'state established and src client_ip and sport = :6449' | grep client_port
  • 如果 能查到:这条并不是幽灵连接,说明差异更多来自“统计口径”
  • 如果 查不到:这条就是典型“客户端认为 ESTABLISHED,但服务器没这条”的连接

重复几次,确认这种“只存在客户端的连接”是否普遍存在。


四、第三步:检查服务器是否被连接洪峰/队列溢出打爆

当连接建立得很快、服务器 accept 处理不过来时,会导致:

  • SYN 队列 / accept 队列溢出
  • 开启 syncookies
  • 部分连接半途掉在三次握手中

这会直接制造“客户自觉已连接,服务器没记住”的场景。

4.1 检查 listen 队列溢出统计

在服务器上执行:

1
netstat -s | egrep -i 'listen|SYNs to LISTEN'

关注类似这些字段:

  • times the listen queue of a socket overflowed
  • SYNs to LISTEN sockets dropped

如果数字 > 0,就说明你的监听队列确实被打爆过。

4.2 检查是否有 SYN 洪水日志(syncookies)

1
dmesg | grep -i "possible SYN flooding"

看到类似:

possible SYN flooding on port 6449. Sending cookies.

说明 Linux 内核认为这个端口的 SYN 流量太猛,开始启用 syncookies,有些连接会“救”回来,有些会直接牺牲掉 ACK。


五、第四步:检查 conntrack/NAT 是否在中间“多记账”

如果你的路径中有:

  • 本机 iptables 做 NAT
  • 中间某节点是 Linux 防火墙 / 路由 / 负载均衡

它们会用 conntrack 跟踪“连接”,而 conntrack 的“连接数”经常比真实 socket 多。

在怀疑节点上看:

1
cat /proc/sys/net/netfilter/nf_conntrack_count
2
cat /proc/sys/net/netfilter/nf_conntrack_max
  • 如果 count 接近甚至超过 max,可能会导致丢包、状态错乱
  • conntrack 里的很多 ESTABLISHED 其实已经没有对应的 socket 了,只是在“等超时回收”

这类问题本质是“统计对象不同”:一个在看 socket,一个在看 conntrack entry,天然不会相等。


六、第五步:用 tcpdump 抓一条幽灵连接的握手

要搞清楚谁在撒谎,抓一条典型“幽灵连接”的包最靠谱。

在服务器上执行(换成真实 IP/端口):

1
tcpdump -i eth0 "host client_ip and host 111.200.212.155 and port 6449" -vv

重点看:

  • 客户端有没有发出三次握手的最后那个 ACK
  • 服务器有无返回 RST / FIN
  • 中间是否有包被某个设备吃掉(比如 ACK 没到服务器)

看到完整的 SYN → SYN+ACK → ACK,且之后服务器没 socket,那就多半是队列溢出 / syncookies 导致的握手异常。


七、对应解决方案整理

根据上面几类情况,解决方案可以分几层。

7.1 服务器侧:让“真正建立的连接”多起来

  1. 调大 backlog / SYN 队列
    • 应用层 listen 调大 backlog(例如 listen(fd, 4096)
    • 系统参数:
1
net.core.somaxconn = 4096
2
net.ipv4.tcp_max_syn_backlog = 4096
  • 记得 sysctl -p 生效
  1. 提升 accept 处理吞吐
    • 不要单线程 accept
    • 用多进程/多线程/事件驱动模型(nginx、haproxy 已经做得不错)
    • 确保 ulimit -n 足够大,fd 不会先爆
  2. 合理使用 syncookies
    • 不要关掉(防攻击)
    • 但要通过加 backlog、优化应用,让它少被触发

7.2 网络侧:conntrack/NAT 调优

  • 增大 nf_conntrack_max,避免 conntrack 表爆掉
  • 为无关协议设置合理的超时时间,减少“尸体连接”
  • 对确定不需要追踪的流量使用 NOTRACK,减轻 conntrack 压力

7.3 客户端 / 协议层:别堆一座“僵尸连接山”

  1. 应用层超时 + 心跳
    • 连接建立后,若 T 秒内没有收到首条响应,主动关闭
    • 长时间 idle 的连接定期发 ping/pong,不回应则超时断开
  2. TCP keepalive(辅助)
    • 对长连接启用 SO_KEEPALIVE
    • 调低 TCP_KEEPIDLETCP_KEEPINTVLTCP_KEEPCNT,让死连接不要挂几个小时
  3. 减少连接总量
    • 使用连接池/HTTP keepalive/HTTP/2/HTTP/3
    • 限制单客户端对同一服务的最大并发连接数
    • 合理规划 ip_local_port_rangeulimit -n

八、小结:排查流程 checklist

  1. 统一统计方式
    • 两边都用 ss -tan 'state established and ...'
    • 客户端:dst + dport;服务器:src + sport
    • 坚决不用“裸 IP + 乱拼 token”的写法
  2. 验证“幽灵连接”是否真实存在
    • 客户端拿一条连接四元组
    • 服务器用同样四元组查,确认是否真查不到
  3. 检查服务器是否存在队列溢出 / SYN flood 迹象
    • netstat -s 看 listen 队列溢出统计
    • dmesgpossible SYN flooding 日志
  4. 检查 conntrack/NAT 差异
    • 明确你是在数 socket 还是在数 conntrack entry
    • nf_conntrack_count vs nf_conntrack_max
  5. 用 tcpdump 抓一条幽灵连接的握手过程
    • 看 ACK 是否真正达到服务器
    • 看服务器是否立即回 RST
  6. 根据结果调整
    • backlog、accept 模型、conntrack 参数
    • 客户端超时/心跳/连接池