一次 Calico socket FD 泄漏问题的排查
背景
近期在一套 Kubernetes 集群中,发现一个非常反常的现象:
- 在节点上执行
netstat/ss命令异常缓慢,甚至需要几分钟才能返回结果 - 节点本身负载并不高,CPU、内存看起来都比较正常
由于 netstat/ss 本质上只是读取内核和 /proc 信息,这种“卡死级别”的慢速,往往意味着 系统层面存在异常规模的数据结构。
这篇文章记录了从一个看似普通的 netstat 慢问题,一步步定位到 Calico socket FD 泄漏 Bug,并最终通过升级版本彻底解决的完整过程。
从 netstat/ss 变慢开始
最初的直接表现是:
time netstat -ntlpActive 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-nodefd 数量保持稳定netstat / ss执行响应速度恢复正常- 问题未再复现
问题彻底解决。
从源码中确认根因:netlink socket 泄漏
在现象层面已经基本可以断定这是一个 socket FD 泄漏,但我个人更习惯从源码中找到“证据链”,否则总觉得少了最后一块拼图。
关键字搜索无果
最初我直接在 GitHub 上 Calico 项目的 issue 中搜索:
socketfd
但并没有发现明显、直观的线索。
采用版本 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
形成了完全一致的证据闭环。