译文-来自: Netflix 在 Linux 内核中调试 FUSE 死锁


译文-来自: Netflix 在 Linux 内核中调试 FUSE 死锁

文章插图
Netflix 的计算团队负责管理 Netflix 的所有 AWS 和容器化工作负载,包括自动缩放、容器部署、问题修复等 。作为该团队的一员 , 我致力于修复用户报告的奇怪问题 。
这个特殊问题涉及自定义内部FUSE 文件系统:ndrive 。它已经溃烂了一段时间,但需要有人坐下来愤怒地看着它 。/proc这篇博文描述了在将问题发布到内核邮件列表并了解内核等待代码的实际工作原理之前,我是如何深入了解发生了什么的!
症状:卡住 Docker Kill 和僵尸进程
我们有一个停滞的 docker API 调用:
goroutine 146 [选择,8817 分钟]:net/***/***/***/***/***/***/***/***/***/***/***/***/***/***/x/net/context/ctx***/x/net@v0.0.0-20211209124913-491a49abca63/context/ctx***/docker/docker/client.(*Client).doRequest(0xc0001a8200, 0x163bd48, 0xc00004409 0, 0xc000966100, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...) /go/pkg/mod/github.com/moby/moby@v0.0.0-20190408150954-50ebe4562dfc/client/request.go:132 +0xbegithub.com/docker/docker/client.(*Client).sendRequest(0xc0001a8200, 0x163bd48, 0xc000044090, 0x13d8643, 0x3, 0xc00079a720, 0x51, 0x0, 0x0, 0x0, ...) /go/pkg/mod /github 。com/moby/moby@v0.0.0-20190408150954-50ebe4562dfc/client/request.go:122 +0x156 github.com/docker/docker/client.(*Client).get(...) /go/pkg/mod /github.com/moby/moby@v0.0.0-20190408150954-50ebe4562dfc/client/request.go:37 github.com/docker/docker/client.(*Client).ContainerInspect(0xc0001a8200, 0x163bd48, 0xc000044090, 0xc 0006a01c0, 0x40 , 0x0, 0x0, 0x0, 0x0, 0x0, ...) /go/pkg/mod/github.com/moby/moby@v0.0.0-20190408150954-50ebe4562dfc/client/container_inspect.go:18 +0x128github.com/Netflix/titus-executor/executor/runtime/docker.(*DockerRuntime).Kill(0xc000215180, 0x163bdb8, 0xc000938600, 0x1, 0x0, 0x0) /var/lib/buildkite-agent/builds/ip-192- 168-1-90-1/netflix/titus-executor/executor/runtime/docker/docker.go:2835 +0x310 github.com/Netflix/titus-executor/executor/runner.(*Runner).doShutdown(0xc000432dc0, 0x163bd10, 0xc000938390, 0x1, 0xc000b821e0, 0x1d, 0xc0005e4710) /var/lib/buildkite-agent/builds/ip-192-168-1-90-1/netflix/titus-executor/executor/runner/runner.go:3 26 +0x4f4 github.com/Netflix/titus-executor/executor/runner.(*Runner).startRunner(0xc000432dc0, 0x163bdb8, 0xc00071e0c0, 0xc0a502e28c08b488, 0x24572b8, 0x1df5980) /var/lib/buildkite-agent/builds/ip-192-168-1-90-1/netflix/titus-executor/executor/runner/runner.go:122 +0x391由 github.com/Netflix/titus- 创建执行者/执行者/runner.StartTaskWithRuntime /var/lib/buildkite-agent/builds/ip-192-168-1-90-1/netflix/titus-executor/executor/runner/runner.go:81 +0x411在这里,我们的管理引擎对 Docker API 的 unix 套接字进行了 HTTP 调用 , 要求它终止一个容器 。我们的容器配置为通过SIGKILL. 但这很奇怪 。kill(SIGKILL)应该是比较致命的,那么容器是干什么的呢?
$ docker exec -it 6643cd073492 bash OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: process_linux.go:130: executing setns process caused: exit status 1: 未知唔 。似乎它还活着,但setns(2)失败了 。为什么会这样?如果我们通过查看进程树ps awwfux,我们会看到:
\_ containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/6643cd073492ba9166100ed30dbe389ff1caef0dc3d35 | \_ [码头工人初始化] | \_ [ndrive] <已失效>好的,所以容器的 init 进程仍然存在,但是它有一个僵尸子进程 。容器的初始化进程可能在做什么?
# cat /proc/1528591/stack [<0>] do_wait+0x156/0x2f0 [<0>] kernel_wait4+0x8d/0x140 [<0>] zap_pid_ns_processes+0x104/0x180 [<0>] do_exit+0xa41/0xb80 [< 0>] do_group_exit+0x3a/0xa0 [<0>] __x64_sys_exit_group+0x14/0x20 [<0>] do_syscall_64+0x37/0xb0 [<0>] entry_SYSCALL_64_after_hwframe+0x44/0xae它正在退出,但似乎卡住了 。不过,唯一的子进程是处于 Z(即“僵尸”)状态的 ndrive 进程 。Zombies 是已成功退出的进程,正在等待wait()其父进程的相应系统调用对其进行收割 。那么内核怎么会卡在等待僵尸呢?
# ls /proc/1544450/任务1544450 1544574啊哈 , 线程组里有两个线程 。其中一个是僵尸,也许另一个不是:
# cat /proc/1544574/stack [<0>] request_wait_answer+0x12f/0x210 [<0>] fuse_simple_request+0x109/0x2c0 [<0>] fuse_flush+0x16f/0x1b0 [<0>] filp_close+0x27/0x70 [< 0>] put_files_struct+0x6b/0xc0 [<0>] do_exit+0x360/0xb80 [<0>] do_group_exit+0x3a/0xa0 [<0>] get_signal+0x140/0x870 [<0>] arch_do_signal_or_restart+0xae/0x7c0 [< 0>] exit_to_user_mode_prepare+0x10f/0x1c0 [<0>] syscall_exit_to_user_mode+0x26/0x40 [<0>] do_syscall_64+0x46/0xb0 [<0>] entry_SYSCALL_64_after_hwframe+0x44/0xae事实上它不是僵尸 。它试图尽可能地成为一个,但由于某种原因它在 FUSE 内部阻塞 。为了找出原因,让我们看一些内核代码 。如果我们查看zap_pid_ns_processes(),它会:
/* * 在我们忽略 SIGCHLD 之前获取我们拥有的 EXIT_ZOMBIE 孩子 。* kernel_wait4() 也将阻塞,直到我们从* parent 命名空间追踪到的孩子被分离并变成 EXIT_DEAD 。*/做{ clear_thread_flag(TIF_SIGPENDING); rc = kernel_wait4( -1 , NULL , __WALL, NULL ); } while (rc != -ECHILD);这是我们卡住的地方 , 但在此之前,它已经完成了:
/* 不允许更多进程进入 pid 命名空间 */ disable_pid_allocation(pid_ns);这就是为什么 docker 不能setns()——