1.runc hang 导致 Kubernetes 节点 NotReady
2.Containerd容器管理机制
3.kubelet 远程调试方法
runc hang 导致 Kubernetes 节点 NotReady
Kubernetes 1..3 OS: CentOS 7.9. Kernel: 5.4.-1.el7.elrepo.x_ Docker: ..6
线上告警提示集群中存在 2-3 个 K8s 节点处于 NotReady 的源码状态,并且 NotReady 状态一直持续。编译问题的源码解决可以通过两种方法,我们先来看看 A 方案。编译
针对 docker hang 住这样的源码现象,通过搜索资料后发现了以下两篇文章里也遇到了相似的编译headfirst 设计模式源码问题。这两篇文章都提到了是源码由于 pipe 容量不够导致 runc init 往 pipe 写入卡住了,将 /proc/sys/fs/pipe-user-pages-soft 的编译限制放开,就能解决问题。源码查看问题主机上 /proc/sys/fs/pipe-user-pages-soft 设置的编译是 。所以将它放大 倍 echo > /proc/sys/fs/pipe-user-pages-soft,源码然而 kubelet 还是编译没有恢复正常,pleg 报错日志还在持续,源码runc init 程序也没有退出。编译考虑到 runc init 是源码 kubelet 调用 CRI 接口创建的,可能需要将 runc init 退出才能使 kubelet 退出。通过文章中的说明,只需要将对应的 pipe 中的内容读取掉,runc init 就能退出。dnf sf 源码尝试了几个后,runc init 果然退出了。再次检查,节点状态切换成 Ready,pleg 报错日志也消失了,观察一天也没有出现节点 NotReady 的情况,问题(临时)解决。
对解决方案 A 的疑问,虽然问题解决了,但是仔细读 /proc/sys/fs/pipe-user-pages-soft 参数的说明文档,发现这个参数跟本次问题的根本原因不太对得上。pipe-user-pages-soft 含义是对没有 CAP_SYS_RESOURCE CAP_SYS_ADMIN 权限的用户使用 pipe 容量大小做出限制,默认最多只能使用 个 pipe,一个 pipe 容量大小为 k。这里就有疑问:为什么容器 root 用户 pipe 容量会超过限制。
定位问题最直接的方法,就是阅读源码。先查看下 Linux 内核跟 pipe-user-pages-soft 相关的git 源码加密代码。线上内核版本为 5.4.-1,切换到对应的版本进行检索。在创建 pipe 时,内核会通过 too_many_pipe_buffers_soft 检查是否超过当前用户可使用 pipe 容量大小。如果发现已经超过,则将容量大小从 个 PAGE_SIZE 调整成 2 个 PAGE_SIZE。通过机器上执行 getconf PAGESIZE 可以获取到 PAGESIZE 是 字节,也就是说正常情况下 pipe 大小为 字节,但是由于超过限制,pipe 大小被调整成 字节,这就有可能出现数据无法一次性写入 pipe 的问题。
找到问题根本原因的第一步,往往是在线下环境复现问题。由于线上环境已经通过方案 A 做了紧急修复,因此,需要找到一种必现的手段。功夫不负有心人,在 issue 中找到了相同的广告积分源码问题,并且可以通过以下方法复现。执行命令之后,立刻就出现 runc init 卡住的情况。通过 lsof -p 查看 runc init 打开的文件句柄情况,可以看到 fd4、fd5、fd6 都是 pipe 类型,其中,fd4 和 fd6 编号都是 ,是同一个 pipe。如何来获取 pipe 大小来实际验证下「疑问 2」中的猜想呢?Linux 下没有现成的工具可以获取 pipe 大小,但是内核开放了系统调用 fcntl(fd, F_GETPIPE_SZ)可以获取到,代码如下。编译好之后,查看 pipe 大小情况如下。重点看下 fd4 和 fd6,两个句柄对应的是同一个 pipe,获取到的景点讲解源码容量大小是 = 2 * PAGESIZE。所以的确是因为 pipe 超过软限制导致 pipe 容量被调整成了 2 * PAGESIZE。
对解决方案 A 疑问的探索,对解决方案 B 的考虑,线上应该如何做修复呢?是否需要把 docker 所有组件都升级呢?如果把 dockerd/containerd/runc 等组件都升级的话,就需要将业务切走然后才能升级,整个过程相对比较复杂,并且风险较高。因此考虑是否可以单独升级 runc?因为在 Kubernetes v1. 版本中还没有弃用 dockershim,因此运行容器整个调用链为:kubelet → dockerd → containerd → containerd-shim → runc → container。不同于 dockerd/containerd 是后台运行的服务端,containerd-shim 调用 runc,实际是调用了 runc 二进制来启动容器。因此,只需要升级 runc,对于新创建的容器,就会使用新版本的 runc 来运行容器。
通过测试环境验证,的确不会出现 runc init 卡住的情况了。最终,逐步将线上 runc 升级成 v1.1.1,并将 /proc/sys/fs/pipe-user-pages-soft 调整回原默认值。runc hang 住的问题圆满解决。
总结,本次故障的原因是,操作系统对 pipe-user-pages-soft 有软限制,但是由于容器 root 用户的 UID 与宿主机一致都是 0,内核统计 pipe 使用量时没有做区分,导致当 UID 为 0 的用户 pipe 使用量超过软限制后,新分配的 pipe 容量会变小。而 runc 1.0.0-rc 正好会因为 pipe 容量太小,导致数据无法完整写入,写入阻塞,进而 runc init 卡住,kubelet pleg 状态异常,节点 NotReady。修复方案是 runc 通过 goroutine 及时读取 pipe 内容,防止写入阻塞。
Containerd容器管理机制
containerd是一个高级容器运行时,由Docker项目衍生,实现CRI规范,现为CNCF托管,提供新的容器解决方案的基础。k8s通过containerd创建容器时,containerd生成containerd-shim进程,此进程操作容器以避免containerd挂断导致所有容器退出的问题。containerd-shim用于执行命名空间、cgroups配置,挂载根文件系统等操作。标准化实现由OCI指定,runc为参考实现。
containerd-shim调用runc启动容器,而runc执行后立即退出,containerd-shim则成为容器的父进程,负责监控、状态收集和子进程清理,确保无僵尸进程。
containerd初始化操作通过方法实现。具体创建过程包括容器对象内部处理的多项操作。初始化后,启动容器操作则由上述方法执行。
关于详细实现代码,请参考相关源码:github.com/containerd/c...
深入理解containerd创建、启动容器的代码实现,请访问进一步分析:qikqiak.com/post/contai...
更多讨论和细节分析见:colstuwjx.github.io/...
kubelet 远程调试方法
Kubelet远程调试方法详解
Kubelet作为Kubernetes的核心组件,可以通过系统服务管理和编译工具进行远程调试。首先,理解kubelet的启动命令至关重要。在v1..4的K8s集群中,kubelet作为systemd服务,其配置文件位于</etc/systemd/system/kubelet.service.d/-kubeadm.conf>。通过执行ps -ef | grep /usr/bin/kubelet,可以查看完整的启动命令。 若需修改kubelet命令,可以先停止服务,然后使用相应参数重新启动,或者修改systemd配置后重启服务。编译kubelet时,推荐使用Kubernetes makefile源码中的编译指令,调整GOLDFLAGS和GOGCFLAGS以保留调试信息。编译完成后,kubelet二进制文件会位于_output/bin/kubelet。 对于Go语言的调试,Delve是一个高效工具,尤其适合调试标准工具链构建的Go程序。可以通过安装命令轻松获取,并使用它来调试kubelet。例如,使用dlv命令行进行调试步骤包括设置地址和端口,以及在GoLand IDE中配置并启动kubelet进行调试。 除了Kubelet,其他容器软件如runc和docker-cli也可通过修改编译命令进行调试。例如,runc和dockerd的编译过程中,需要在scripts/build/binary或hack/make/.binary文件中相应位置调整编译参数。 获取更多详细教程和实践步骤,可以参考ssst0n3.github.io/post/...。通过以上步骤,你可以有效地对kubelet和其他容器软件进行远程调试,提升开发效率。