背景

近期在一套 Kubernetes 集群中,发现一个非常反常的现象

  • 在节点上执行 netstat / ss 命令异常缓慢,甚至需要几分钟才能返回结果
  • 节点本身负载并不高,CPU、内存看起来都比较正常

由于 netstat/ss 本质上只是读取内核和 /proc 信息,这种“卡死级别”的慢速,往往意味着 系统层面存在异常规模的数据结构

这篇文章记录了从一个看似普通的 netstat 慢问题,一步步定位到 Calico socket FD 泄漏 Bug,并最终通过升级版本彻底解决的完整过程。


从 netstat/ss 变慢开始

最初的直接表现是:

time netstat -ntlp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:10010         0.0.0.0:*               LISTEN      1530975/cri-dockerd 
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1165/sshd: /usr/sbi 
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      164980/nginx: worke 
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      1/systemd           
tcp        0      0 127.0.0.1:9099          0.0.0.0:*               LISTEN      1171111/calico-node 
tcp        0      0 0.0.0.0:20048           0.0.0.0:*               LISTEN      2503623/rpc.mountd  
tcp        0      0 0.0.0.0:2049            0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:10256         0.0.0.0:*               LISTEN      623016/kube-proxy   
tcp        0      0 127.0.0.1:10248         0.0.0.0:*               LISTEN      1531381/kubelet     
tcp        0      0 127.0.0.1:10249         0.0.0.0:*               LISTEN      623016/kube-proxy   
tcp        0      0 0.0.0.0:57325           0.0.0.0:*               LISTEN      2500368/rpc.statd
tcp        0      0 0.0.0.0:46433           0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      1165/sshd: /usr/sbi 
tcp6       0      0 :::111                  :::*                    LISTEN      1/systemd           
tcp6       0      0 :::20048                :::*                    LISTEN      2503623/rpc.mountd  
tcp6       0      0 :::2049                 :::*                    LISTEN      -                   
tcp6       0      0 :::2380                 :::*                    LISTEN      619966/etcd         
tcp6       0      0 :::2379                 :::*                    LISTEN      619966/etcd         
tcp6       0      0 :::40007                :::*                    LISTEN      -                   
tcp6       0      0 :::6443                 :::*                    LISTEN      621032/kube-apiserv 
tcp6       0      0 :::9369                 :::*                    LISTEN      2484838/pushprox-cl 
tcp6       0      0 :::9796                 :::*                    LISTEN      2485357/node_export 
tcp6       0      0 :::10250                :::*                    LISTEN      1531381/kubelet     
tcp6       0      0 :::10259                :::*                    LISTEN      621666/kube-schedul 
tcp6       0      0 :::10257                :::*                    LISTEN      621333/kube-control 
tcp6       0      0 :::48607                :::*                    LISTEN      2500368/rpc.statd   

real    3m21.534s
user    3m9.634s
sys     0m11.357s

可见执行非常慢。

这类问题通常有几种可能:

  • 系统中存在大量 socket / 连接
  • /proc 下某些目录规模异常
  • 某个进程打开了极多的文件描述符(FD)

为了确认 netstat 到底“卡”在什么地方,我使用了 strace 对其系统调用进行跟踪。


使用 strace 定位卡顿原因

执行:

strace netstat -ntlp

很快发现了异常点,大量重复的系统调用如下:

...
readlink("/proc/3698662/fd/77625", "socket:[837323590]", 63) = 18
getxattr("/proc/3698662/fd/77625", "security.selinux", 0x558e62a555d0, 255) = -1 EOPNOTSUPP
readlink("/proc/3698662/fd/77626", "socket:[837626395]", 63) = 18
getxattr("/proc/3698662/fd/77626", "security.selinux", 0x558e62a55670, 255) = -1 EOPNOTSUPP
readlink("/proc/3698662/fd/77627", "socket:[837554271]", 63) = 18
...

可以看到:

  • netstat遍历 /proc/3698662/fd/ 目录
  • 对每一个 fd 执行 readlink
  • fd 指向的几乎全部是 socket:[inode]

这意味着:

某个 PID 打开了数量极其夸张的 socket fd,导致 netstat 在遍历时较长

锁定异常进程:calico-node

通过 PID 确认该进程身份:

ps aux | grep 3698662

输出显示:

root 3698662  ... calico-node -felix

进一步确认 fd 数量:

ll /proc/3698662/fd | wc -l
1108645

ll /proc/3698662/fd | grep socket | wc -l
1108642

结论已经非常清晰:

  • calico-node 打开了 110 万+ 文件描述符
  • 几乎全部是 socket

这已经远远超出“连接多”的范畴,而是一个明确的 FD 泄漏问题


进一步验证:是否为泄漏?

为了确认这是“可回收的异常状态”,还是“真实业务负载”,我做了一个关键验证。

重启 calico-node(Canal Pod)

在该节点上重启 canal / calico-node Pod 后:

ll /proc/<new-pid>/fd | wc -l
35

结果立刻恢复到正常水平。

这一步直接证明:

  • socket 数量并非业务真实连接
  • 而是 calico-node 进程内部的 socket 泄漏

环境信息与问题定性

环境关键信息如下:

  • Calico 版本:v3.29.0
  • 问题特征:

    • fd 持续增长
    • 重启即恢复

综合现象可以定性为:

Calico v3.29.0 存在 socket FD 泄漏问题

解决方案:升级 Calico

为什么只能升级

  • FD 已经泄漏,无法通过配置回收
  • 调整 fs.file-max 不能解决问题,并且可能会导致calico不能正常工作,报 Too many open files
  • 根因在 Calico 代码本身

实际处理

将 Calico 从:

v3.29.0

升级到:

v3.29.3

升级完成后观察:

  • calico-node fd 数量保持稳定
  • netstat / ss 执行响应速度恢复正常
  • 问题未再复现

问题彻底解决。


从源码中确认根因:netlink socket 泄漏

在现象层面已经基本可以断定这是一个 socket FD 泄漏,但我个人更习惯从源码中找到“证据链”,否则总觉得少了最后一块拼图。

关键字搜索无果

最初我直接在 GitHub 上 Calico 项目的 issue 中搜索:

  • socket
  • fd

但并没有发现明显、直观的线索。

采用版本 diff 思路定位

既然问题在 v3.29.0 存在,而 v3.29.3 消失,那最直接、也是最可靠的方式就是:

对比 v3.29.0..v3.29.3 的源码差异

在 diff 过程中,我很快注意到一个非常关键的提交:

Fix netlink leak (#9609)

这个提交本身就已经点明了核心问题:netlink 泄漏。这个提交主要是对文件句柄进行了关闭,在 v3.29.2 上修复了该 Bug。

diff --git a/cni-plugin/internal/pkg/testutils/utils_linux.go b/cni-plugin/internal/pkg/testutils/utils_linux.go
index e1b40fc553..f88a812cd0 100644
--- a/cni-plugin/internal/pkg/testutils/utils_linux.go
+++ b/cni-plugin/internal/pkg/testutils/utils_linux.go
@@ -287,6 +287,7 @@ func RunCNIPluginWithId(
             return err
         }
 
+        defer nlHandle.Close()
         contVeth, err = nlHandle.LinkByName(ifName)
         if err != nil {
             return err
diff --git a/felix/dataplane/linux/int_dataplane.go b/felix/dataplane/linux/int_dataplane.go
index 71df22c00f..7838b66c5d 100644
--- a/felix/dataplane/linux/int_dataplane.go
+++ b/felix/dataplane/linux/int_dataplane.go
@@ -1124,6 +1124,7 @@ func findHostMTU(matchRegex *regexp.Regexp) (int, error) {
         return 0, err
     }
 
+    defer nlHandle.Delete()
     links, err := nlHandle.LinkList()
     if err != nil {
         log.WithError(err).Error("Failed to list interfaces. Unable to auto-detect MTU.")

netlink 与 socket FD 的关系

这里有一个很容易被忽略的点:

  • netlink 本质上就是一种 socket(AF_NETLINK)
  • 每一个 netlink 连接,都会对应一个 socket fd
  • 如果 netlink socket 没有被正确关闭,最终体现出来的就是:

    /proc/PID/fd 下出现大量 socket:[inode]

这与我在现场看到的现象:

  • fd 数量 110 万+
  • 几乎全部是 socket

形成了完全一致的证据闭环

标签: none

添加新评论