Go 进程在容器中无 coredump 产生问题分析
Go 进程在容器中无 coredump 产生问题分析
0x01 起因
coredump 作为一种非常重要的高度手段,在日常开发中经常用到,切换到容器环境后一直没关注。最近测试了下,发现出不了 core。觉得有些奇怪,抽点时间研究了下。
0x02 实验
一开始认为是 Go 运行时的问题,做了一以下对比。以下实现已保证了 GOTRACEBACK=crash 和 ulimit 和 /proc/sys/kernel/core_pattern 配置是正常的。
1. 以 Go 进程为 init 进程启动容器。
2. 在 上述容器中,额外运行一个 Go 进程。
得到的结果是,第1个不会出 core。第2个可以出 core 。对比了两者的差异,唯一不一样就是在容器里的 pid ,前者是1,后者不是1。
通过 bpftrace 工具外加 Go 运行时抛异常时打日志等调试方法,确认了信号是发给了内核,内核的 kernel/signal.c:get_signal 函数中没有走到 coredump 的分支。又补充了以下实验。用 C 语言写了一个 demo,构造空地址写异常。放在容器中作为 init 进程启动,发现可以正常出 core。
得到的结论就是:Go 作为容器中的1号进程时,无法正常 coredump 出来。
本来是想用 bpftrace 中的 kprobe + 指令偏移,一步步找出信号是在 get_signal 函数中的哪一步被忽略的,经过几次实验后发现这样效率太低了,还要去了解内核的信号处理流程。内核这种级别的项目,如果有什么特殊处理,一定会在代码中详细说明的。果然就在 get_signal 函数中找到了。
0x03 需要特殊对待的 init 进程
注释见如下:
/*
* Global init gets no signals it doesn't want.
* Container-init gets no signals it doesn't want from same
* container.
*
* Note that if global/container-init sees a sig_kernel_only()
* signal here, the signal must have been generated internally
* or must have come
* case, the signal cannot be dropped.
*/
大意是容器里的 init 进程地位跟宿主机的 init 进程地位一样重要,所以不能被自己(自杀)和自己的子孙进程杀死( 计算机世界里的父慈子孝 😄),如果收到了这类信号,就会忽略掉。你可以试下能不能把宿主的 init 进程,用 kill -6 1命令杀掉(当然是杀不掉的)。由于 Go 使用的 SIGABRT 信号属于这类信号,所以被忽略了。有了类似的注释,不难搜到当时的 patch 邮件Container-init signal semantics [LWN.net]。
还有问题:为什么 C 语言就可以正常出 core 呢?它不也是自杀吗?
是不一样。细节没有研究的很清楚,大概区别如下:
Go 是自己捕获了 SIGSEGV 信号,因为它要做一层地址转换,以方便调试。之后,自己向内核报告我挂了,请杀掉我。这是明确的 suicide。
C 一般没有注册 SIGSEGV 信号的处理函数,访问非法地址后,直接被内核杀掉,不是 suicide。
内核是假定某些应用可以处理掉 SIGSEGV 的异常,所以应用可以自己捕获这个信号。但 SIGABRT 是明确主动要求终止进程的信号。
0x04 解决方法
我们不大可能改内核的行为,所以只能调整自己。只要让这个进程不是 init 进程就行了。在 k8s 下的方法就是,在 Pod Spec 中,添加 shareProcessNamespace: true,与 infra 容器共享 pid 命名空间即可,让 pause 充当 init 进程。这样就能正常 dump 出 core 了。
同事提供了另外一种方法,使用 tini 作为容器的 init 进程,tini 非常小巧,只有几十 KiB。也是一种不错的思路。

浙公网安备 33010602011771号