1. 问题描述
内核版本:5.4.241
kubelet版本:1.22.5
nvidia驱动版本:535.161.08 和 535.154.05
节点上的 kubelet 进程启动后,监听了一个 ip_local_port_range
范围内的随机端口(46127)
ss -lntpe |grep kubelet
对应代码:
运行了一段时间后,端口突然消失了
对应的 fd(fd=13)也关闭了,但是 kubelet 进程却没有退出
2. 问题分析
查看 kubelet 对应源码,发现 streaming server 是通过单独的 goroutine 拉起的。
于是打开 pprof 查看 goroutine 调用栈,对应的 streaming server goroutine 还存在,也就说明 server 启动后仍然在监听端口,并没有退出
正常情况下,在 linux 内核中,A 进程的 socket fd 只能由 A 进程本身调用 close() 关闭,B 进程无法在不杀死改 A 进程的情况下关闭 A 进程的 fd。
但现实却出现了 kubelet 进程本身没有执行关闭 fd 的操作,fd 却被莫名关闭了,开始怀疑是内核本身的问题。
要定位内核的问题,首先想到的是注入 ko 模块或通过 ebpf 拦截内核特定的函数调用,打出调用栈进行分析。
在这里我们选择实现上更为简单的 ebpf,kprobe 注入点选择 filp_close
,只要有 kubelet 进程所监听端口对应的 fd 被删除了,就将其调用栈打印出来。
SEC ("kprobe/filp_close")
int trace_filp_close(struct pt_regs *ctx)
{
struct task_struct *cur = (struct task_struct *)bpf_get_current_task();
u32 tgid = BPF_CORE_READ(cur, tgid);
struct event event = 10;
u32 zero_key = 0;
struct bpf_arg *bpf_arg;
struct file *file;
struct inode *f_inode;
unsigned long i _ino;
umode_t mode;
bpf_arg = bpf_map_lookup_elem(&bpf_arg_map, &zero_key);
if (Ibpf_arg || (bpf_arg-›tgid != 0 8& bpf_arg-›tgid != tgid))
return 0;
file = (struct file *)PT_REGS_PARM1(ctx);
if(!file)
return 0;
f_inode = BPF_CORE_READ(file, f_inode);
if(!f_inode)
return 0;
i_ino = BPF_CORE_READ(F_inode, i_ino);
if (bpf_arg-›ino !=0 && i_ino != bpf_arg-›ino)
return 0;
mode = BPF_CORE_READ(f_inode, i_mode);
if(!S_ISSOCK(mode))
return 0;
BPF_CORE_READ__STR_INTO(&event.task, cur, comm);
event.pid = bpf_get_current_pid_tgid();
event.tgid = tgid;
event.ino = i_ino;
event.u_stack_id = bpf_get_stackid(ctx, &user_stackmap, BPF_F_USER_STACK | BPF_F_REUSE_STACKID);
event.k_stack_id = bpf_get_stackid(ctx, &kernel_stackmap, KERN_STACKID_FLAGS | BPF_F_REUSE_STACKID);
bpf_perf_event_output(ct, &events, BPF_F_CURRENT_CPU, &event, sizeof (event));
return 0;
}
问题再次复现时,抓到了调用栈:
3. 阶段性结论
从调用栈中可以看到,是 kubelet 的 cadvisor 周期性访问容器进程的 /proc/$pid/fd 目录时触发了 nvidia 驱动的 os_nv_cap_close_fd
调用,
最终调用内核的 filp_close 关闭了 fd。
背景补充:
kubelet 的 cadvisor 中会采集 container_file_descriptors 和 container_sockets 这两个监控指标,需要对容器进程的 /proc/$pid/fd 目录做 getdents64 调用
简单来说,kubelet 的 cadvisor 采集容器监控指标的过程中,getdents64
系统调用触发了 nvidia.ko 内核模块的 os_nv_cap_close_fd
函数,最终导致了 fd 的关闭。
由于 nvidia 驱动没有完全开源,只能将问题反馈 nvidia 厂商,目前得到反馈是:几个月前也有用户反馈类似的现象,nvidia 暂时没有找到稳定复现的方法和解决办法,临时可以通过注释掉 kubelet 中 cadvisor 的 container_file_descriptors 和 container_sockets 指标的相关采集逻辑进行规避。