Z.S.K.'s Records

Kubernetes学习(什么, 宿主机根目录被容器coredump打爆了?)

最近在生产Kubernetes集群容器中的进程频繁地出现coredump从而导致宿主机的根目录被打到100%, 在排查的过程中发现一些技术盲区, 记录一下

问题现象

某晚突然收到磁盘告警, 提示某台机器的/路径被占用100%(所有机器的根目录都是统一规格不算大),打开监控大图发现这台机器的根目录出现很有频率地打到100%降下后又100%的趋势.
由于计算集群将docker及kubelet的volume都配置到独立的disk上,因此除了根被打满的告警外,没有收到其它的报警, 即不会引影响上面已有容器的正常运行

问题排查

由于计算节点只有集群管理员才能登录, 因此不存在是哪个开发同学直接在上面操作引起的,那么排查思路如下:

df/du

根被打满,第一反应得查一下是不是在根目录下生成了大文件,du没发现问题,监控数据不会骗人, 多次执行df之后发现确认会到100%, 但是du又没找到是哪个大文件, 那么就可能是时间上很短,du没有抓到

dmesg

使用Dmesg -T|less 相关内核日志,发现有很多如下错误:

1
2
3
kernel: Pid 38693(python) over core_pipe_limit
kernel: Skipping core dump
kernel: systemd-coredump[11718]: Core file was truncated to 2147483648 bytes.

显然,这跟一个Pid 为38693的python进程有关, 同时出现了coredump,dmesg里多次出现,那肯定不正常

coredump

关于coredump在这里不展开描述,只提一下它的作用:

当程序发生内存越界访问等行为时,会触发OS的保护机制,此时OS会产生一个信号(signal)发送给对应的进程。当进程从内核态到用户态切换时,该进程会处理这个信号。此类信号(比如SEGV)的默认处理行为生成一个coredump文件
由于coredump会占用大量磁盘资源,所以计算节点应该都默认关闭了coredump功能, 通过以下命令确认:

1
2
3
4
5
ulimit -c

# ulimit -c unlimited 不限制
# ulimit -c 1024 限制大小为1024
# ulimit -c 0 限制大小为0,即不输出core dump文件

返回的是0, 说明coredump确认是关闭的,那奇怪为何会出现coredump呢? 内核日志同样不会骗人, 从日志本身入手,
从日志中可以看到是个python进程,有pid,那么就可以查到是属于哪个容器的了,由于宿主机上跑的容器很多,所以更快的方法是: 使用coredumpctl命令查看发生coredump的进程信息,返回如下:

1
2
3
4
5
6
7
8
9
10
# coredumpctl info
PID: 38693 (python)
UID: xxxx (xxxx)
GID: xxxx (xxxx)
Signal: 11 (SEGV)
Timestamp: Wed 2023-07-28 19:58:05 CST
Command Line: python xxx
Executable: python
Control Group: /kubepods.slice/...
# 省略...

信息非常详细, 很容易拿到是哪个容器触发的coredump,由于都发生了coredump, 大概率这个python程序已经跑跪了,为了更快的地验证是不是这个容器导致宿主机根被打满,让相应的用户先将容器stop掉,发现宿主机的根目录果然降下来了且稳定在低水位。

问题延伸

虽然快速地”解决了问题”,但产生了几个疑问:

  1. 为什么宿主机上禁用了coredump还是发生了?
  2. 就算问题1成立,为什么占用的是宿主机上磁盘容量且是根目录?

经过一篇追踪及搜索后, 领了几个技术盲区后能够解释上述问题

为什么宿主机上禁用了coredump还是发生了?

首先, 虽然宿主机上禁用了coredump, 并不代表容器中就不能coredump,相反在容器中也是可以设置内核参数的,

所以, 给pod中的容器设置ulimit参数,可以通过以下方法:
是在镜像中的初始化程序中调用setrlimit()系统调用来进行设置。子进程会继承父进程的ulimit参数
这个很好排查,拿出现coredump问题的镜像,直接使用docker run 后发现ulimit -c返回的是unlimited,与宿主机不一样。

所以可以回答问题1, 即容器中能够使用coredump的原因是由于在容器中设置了ulimit -c参数

根据问题1又衍生出了另一个问题: kubernetes中给容器设置某些内核参数,哪些是会与主机冲突?

从上面的验证来看,至少ulimit -c不会覆盖宿主机的,要不然不存在问题1

其实,linux内核方面做了大量的工作,把一部分sysctl内核参数进行了namespace化(namespaced)。 也就是多个容器和主机可以各自独立设置某些内核参数。例如,可以通过net.ipv4.ip_local_port_range,在不同容器中设置不同的端口范围。
那如何判断一个参数是不是namespaced?

方式很简单: 运行一个具有privileged权限的容器,然后在容器中修改该参数,看一下在host上能否看到容器在中所做的修改。如果看不到, 那就是namespaced, 否则不是。

k8s还进一步把syctl参数分为safe和unsafe, safe的条件:

  • must not have any influence on any other pod on the node
  • must not allow to harm the node’s health
  • must not allow to gain CPU or memory resources outside of the resource limits of a pod.

非namespaced的参数肯定是unsafe。namespaced参数也只有一部分被认为是safe的。

由于sysctl是由kubelet设置的,从kubelet的源码pkg/kubelet/sysctl/namespace.go来看,目前已经namespace化的sysctl内核参数如下

1
2
3
4
5
kernel.shm*,
kernel.msg*,
kernel.sem,
fs.mqueue.*,
net.*.

注意: vm并没有namespace化, 比如vm.max_map_count 在宿主机或者一个容器中设置它,其他所有容器都会受影响,都会看到最新的值。

pkg/kubelet/sysctl/safe_sysctls.go中维护了safe sysctl参数的名单。目前只有三个参数被认为是safe的:

1
2
3
kernel.shm_rmid_forced,
net.ipv4.ip_local_port_range,
net.ipv4.tcp_syncookies

为什么占用的是宿主机上磁盘容量且是根目录?

搞清了问题1, 那为什么是占用的是宿主机上的根目录容量呢, 接着查.
通过排查coredump发现, coredump可以有好几种方式, 在使用了systemd的系统上, 默认使用的是systemd-coredump,
关于systemd-coredump在这里不展开描述,只提一下它的作用:

systemd-coredump可收集并显示内核核心转储,用于分析应用程序崩溃问题。当某个进程(或属于应用程序的所有进程)崩溃时,此工具默认会将核心转储记录到 systemd 日记(如有可能还包括回溯),并将核心转储储存在/var/lib/systemd/coredump(默认路径) 中的某个文件内.

查看宿主机的/var/lib/systemd/coredump目录,果然发现有几个产生的core文件,且size很大.
systemd-coredump有如下几个重要配置文件,默认配置如下:

1
2
3
4
5
6
7
8
9
# /etc/systemd/coredump.conf
[Coredump]
#Storage=external
#Compress=yes
#ProcessSizeMax=2G
#ExternalSizeMax=2G
#JournalSizeMax=767M
#MaxUse=
#KeepFree=

Storage可以有如下值:

  • none: 在日记中记录核心转储,但不储存。这样做有助于尽量减少敏感信息的收集与储存,例如,出于符合一般数据保护条例 (GDPR) 的目的。
  • external: 将核心储存在/var/lib/systemd/coredump中
  • journal: 将核心储存在 systemd 日记中

这里使用的是默认值external, 因此core文件存储在了/var/lib/systemd/coredump

另一个配置文件:

1
2
3
# /usr/lib/sysctl.d/50-coredusmp.conf
kernel.core_pattern=|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h %e
kernel.core_pipe_limit=16

第一行表明使用了|(管道,内核2.6.19就已支持)的方式.
第二行表示可以并发运行coredump的上限是16个, dmesg中看到的over core_pipe_limit就是超过了这个参数直接给skip了.

总结几个很重要的信息:

  1. 使用管道的时候,内核会将 core 内容作为管道后程序的 stdin,然后通过新起一个内核线程来调用管道后的程序,此时,是以root用户来运行管道后的程序
  2. 使用管道时,dump的path目录解析是在系统初始化namespace中发生的,也就是宿主机的global ns 中,所以默认会将文件生成在宿主机的path目录中
  3. 同时由于kernel.core_pattern是一个未做隔离的变量,容器中和宿主机共享此值

所有配置结合起来就导致在容器中产生的coredump事件最后产生的core文件却生成了在宿主机上了
问题2的疑惑解决

问题解决

那需要不需要将在容器中产生的coredump直接保存在容器里呢?
答案是不建议,因为本身开启coredump的原因就是为了debug问题,在出现coredump时一般容器也可能就挂了,如果这时候coredump文件还保存在容器中,那也没办法拿到,开启coredump的初衷就不存在。
由于业务方需要开启coredump来定位问题这个情况客观存在, 也不能说一棒子打死就直接不给开放了,优雅一点通过以下几种方式解决:

限制容器中ulimit参数

这次问题的本质是产生的coredump太大将宿主机根目录打满,那么可以在容器中限制一下ulimit的大小

1
ulimit -c # 降低到128M

调整宿主机上systemd-coredump配置

同时, 调整systemd-coredump的配置,限制并发数(/usr/lib/sysctl.d/50-coredusmp.conf中的kernel.core_pipe_limit参数), 同时将存储core文件的路径从根目录下迁移到大盘上或者直接设置为none

1
2
3
4
5
6
7
8
9
# /etc/systemd/coredump.conf
[Coredump]
Storage=none # 或者是大盘路径
#Compress=yes
ProcessSizeMax=0
#ExternalSizeMax=2G
#JournalSizeMax=767M
#MaxUse=
#KeepFree=

注意: 内核转储文件所占用的磁盘资源受两种不同方式的约束: (1)占用磁盘空间的大小受 /etc/systemd/coredump.conf 配置文件以及对应的配置片段的约束; (2)占用磁盘时间的长短受 systemd-tmpfiles 配置的约束(对应的配置文件默认位于 /usr/lib/tmpfiles.d/systemd.conf)。

问题总结

这次出现的问题虽没有产生实质的影响,但还是发现不少可以提高稳定性的点, 这类问题不亲身遇到, 平时也没什么机会可以关注到,从点到线及面, 出现问题不可怕, 解决能力很重要, 而且要快

参考文章:

转载请注明原作者: 周淑科(https://izsk.me)

 wechat
Scan Me To Read on Phone
I know you won't do this,but what if you did?