Kubelet Streaming Server 端口异常关闭

Posted by Hao Liang's Blog on Saturday, July 13, 2024

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 指标的相关采集逻辑进行规避。