关于后端:runc-hang-导致-Kubernetes-节点-NotReady

36次阅读

共计 5238 个字符,预计需要花费 14 分钟才能阅读完成。

Kubernetes 1.19.3

OS: CentOS 7.9.2009

Kernel: 5.4.94-1.el7.elrepo.x86_64

Docker: 20.10.6

先说论断,runc v1.0.0-rc93 有 bug,会导致 docker hang 住。

发现问题

线上告警提醒集群中存在 2-3 个 K8s 节点处于 NotReady 的状态,并且 NotReady 状态始终继续。

  • kubectl describe node,有 NotReady 相干事件。

  • 登录问题机器后,查看节点负载状况,一切正常。
  • 查看 kubelet 日志,发现 PLEG 工夫过长,导致节点被标记为 NotReady。

  • docker ps 失常。
  • 执行 ps 查看过程,发现存在几个 runc init 的过程。runc 是 containerd 启动容器时调用的 OCI Runtime 程序。初步狐疑是 docker hang 住了。

要解决这个问题能够通过两种办法,首先来看一下 A 计划。

解决方案 A

针对 docker hang 住这样的景象,通过搜寻材料后发现了以下两篇文章里也遇到了类似的问题:

  • docker hang 问题排查 [https://www.likakuli.com/post…]
  • Docker hung 住问题解析系列 (一):pipe 容量不够 [https://juejin.cn/post/689155…]

这两篇文章都提到了是因为 pipe 容量不够导致 runc init 往 pipe 写入卡住了,将 /proc/sys/fs/pipe-user-pages-soft 的限度放开,就能解决问题。

于是,查看问题主机上 /proc/sys/fs/pipe-user-pages-soft 设置的是 16384。所以将它放大 10 倍 echo 163840 > /proc/sys/fs/pipe-user-pages-soft,然而 kubelet 还是没有恢复正常,pleg 报错日志还在继续,runc init 程序也没有退出。

思考到 runc init 是 kubelet 调用 CRI 接口创立的,可能须要将 runc init 退出能力使 kubelet 退出。而依据文章中的阐明,只须要将对应的 pipe 中的内容读取掉,runc init 就能退出。因为读取 pipe 的内容能够利用「UNIX/Linux 所有皆文件」的准则,通过 lsof -p 查看 runc init 关上的句柄信息,获取写入类型的 pipe 对应的编号(可能存在多个),顺次执行 cat /proc/$pid/fd/$id 的形式,读取 pipe 中的内容。尝试了几个后,runc init 果然退出了。

再次查看,节点状态切换成 Ready,pleg 报错日志也隐没了,察看一天也没有呈现节点 NotReady 的状况,问题(长期)解决。

对解决方案 A 疑难

尽管问题解决了,然而认真读 /proc/sys/fs/pipe-user-pages-soft 参数的阐明文档,不难发现这个参数跟本次问题的根本原因不太对得上。

pipe-user-pages-soft 含意是对没有 CAP_SYS_RESOURCE CAP_SYS_ADMIN 权限的用户应用 pipe 容量大小做出限度,默认最多只能应用 1024 个 pipe,一个 pipe 容量大小为 16k。

那这里就有了疑难:

  • dockerd/containerd/kubelet 等组件均通过 root 用户运行,并且 runc init 处于容器初始化阶段,实践上不会将 1024 个 pipe 消耗掉。因而,pipe-user-pages-soft 不会对 docker hang 住这个问题产生影响,然而理论参数放大后问题就隐没了,解释不通。
  • pipe 容量是固定,用户在创立 pipe 时无奈申明容量。从线上来看,pipe 确实被建进去了,容量是固定的话,不应该因为用户应用 pipe 总量超过 pipe-user-pages-soft 限度,而导致无奈写入的问题。是不是新创建的 pipe 容量变小了,导致原先能够写入的数据,本次无奈写入了?
  • 目前对 pipe-user-pages-soft 放大了 10 倍,放大 2 倍够不够,哪个值是最合适的值?

摸索

定位问题最间接的办法,就是浏览源码。

先查看下 Linux 内核跟 pipe-user-pages-soft 相干的代码。线上内核版本为 5.4.94-1,切换到对应的版本进行检索。

static bool too_many_pipe_buffers_soft(unsigned long user_bufs)
{unsigned long soft_limit = READ_ONCE(pipe_user_pages_soft);

        return soft_limit && user_bufs > soft_limit;
}

struct pipe_inode_info *alloc_pipe_info(void)
{
  ...
  unsigned long pipe_bufs = PIPE_DEF_BUFFERS;  // #define PIPE_DEF_BUFFERS        16
  ...

        if (too_many_pipe_buffers_soft(user_bufs) && is_unprivileged_user()) {user_bufs = account_pipe_buffers(user, pipe_bufs, 2);
                pipe_bufs = 2;
        }

        if (too_many_pipe_buffers_hard(user_bufs) && is_unprivileged_user())
                goto out_revert_acct;

        pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
                             GFP_KERNEL_ACCOUNT);
  ...
}

在创立 pipe 时,内核会通过 too_many_pipe_buffers_soft 查看是否超过以后用户可应用 pipe 容量大小。如果发现曾经超过,则将容量大小从 16 个 PAGE_SIZE 调整成 2 个 PAGE_SIZE。通过机器上执行 getconf PAGESIZE 能够获取到 PAGESIZE 是 4096 字节,也就是说失常状况下 pipe 大小为 164096 字节,然而因为超过限度,pipe 大小被调整成 24096 字节,这就有可能呈现数据无奈一次性写入 pipe 的问题,根本能够验证问题 2 的猜测。

至此,pipe-user-pages-soft 相干的逻辑也理顺了,绝对还是比拟好了解的。

那么,问题就回到了「为什么容器 root 用户 pipe 容量会超过限度」。

百分百复现

找到问题根本原因的第一步,往往是在线下环境复现问题。

因为线上环境曾经都通过计划 A 做了紧急修复,因而,曾经无奈在线上剖析问题了,须要找到一种必现的伎俩。

功夫不负有心人,在 issue 中找到了雷同的问题,并且能够通过以下办法复现。

https://github.com/containerd…

echo 1 > /proc/sys/fs/pipe-user-pages-soft
while true; do docker run -itd --security-opt=no-new-privileges nginx; done

执行以上命令之后,立即就呈现 runc init 卡住的状况,跟线上的景象是统一的。通过 lsof -p 查看 runc init 关上的文件句柄状况:

能够看到 fd4、fd5、fd6 都是 pipe 类型,其中,fd4 跟 fd6 编号都是 415841,是同一个 pipe。那么,如何来获取 pipe 大小来理论验证下「疑难 2」中的猜测呢?Linux 下没有现成的工具能够获取 pipe 大小,然而内核凋谢了零碎调用 fcntl(fd, F_GETPIPE_SZ)能够获取到,代码如下:

#include <unistd.h>
#include <errno.h>
#include <stdio.h>
// Must use Linux specific fcntl header.
#include </usr/include/linux/fcntl.h>

int main(int argc, char *argv[]) {int fd = open(argv[1], O_RDONLY);
    if (fd < 0) {perror("open failed");
        return 1;
    }

    long pipe_size = (long)fcntl(fd, F_GETPIPE_SZ);
    if (pipe_size == -1) {perror("get pipe size failed.");
    }
    printf("pipe size: %ld\\n", pipe_size);

    close(fd);
}

编译好之后,查看 pipe 大小状况如下:

重点看下 fd4 跟 fd6,两个句柄对应的是同一个 pipe,获取到的容量大小是 8192 = 2 PAGESIZE。所以确实是因为 pipe 超过软限度导致 pipe 容量被调整成了 2 PAGESIZE。

应用 A 计划解决问题后,咱们来看一下 B 计划。

解决方案 B

https://github.com/opencontai…

该 bug 是在 runc v1.0.0-rc93 中引入的,并且在 v1.0.0-rc94 中通过下面的 PR 修复。那么,线上应该如何做修复呢?是不是须要把 docker 所有组件都降级呢?

如果把 dockerd/containerd/runc 等组件都降级的话,就须要将业务切走而后能力降级,整个过程绝对比较复杂,并且危险较高。而且在本次问题中,出问题的只有 runc,并且只有新创建的容器受到影响。因而牵强附会思考是否能够独自降级 runc?

因为在 Kubernetes v1.19 版本中还没有弃用 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 住的问题圆满解决。

剖析 & 总结

PR 做了什么修复?

Bug 的原因。当容器开启 no-new-privileges 后,runc 会须要去卸载一段曾经加载的 bpf 代码,而后从新加载 patch 后的 bpf 代码。在 bpf 的设计中,须要先获取曾经加载的 bpf 代码,而后能力利用这段代码调用卸载接口。在获取 bpf 代码,内核凋谢了 seccomp_export_bpf 函数,runc 采纳了 pipe 作为 fd 句柄传参来获取代码,因为 seccomp_export_bpf 函数是同步阻塞的,内核会将代码写入到 fd 句柄中,因而,如果 pipe 大小太小的话,就会呈现 pipe 数据写满后无奈写入 bpf 代码导致卡住的状况。

PR 中的解决方案。启动一个 goroutine 来及时读取 pipe 中的内容,而不是等数据写入实现后再读取。

为什么超过限度?

容器的 root 用户 UID 为 0,而宿主机的 root 用户 UID 也是 0。在内核统计 pipe 使用量时,认为是同一用户,没有做辨别。所以,当 runc init 申请 pipe 时,内核判断以后用户没有特权,就查问 UID 为 0 的用户 pipe 使用量,因为内核统计的是所有 UID 为 0 用户(包含容器内)pipe 使用量的总和,所以曾经超过了 /proc/sys/fs/pipe-user-pages-soft 中的限度。而理论容器 root 用户 pipe 使用量并没有超过限度。这就解释了后面提到的疑难 2。

所以咱们最初做个总结,本次故障的起因是,操作系统对 pipe-user-pages-soft 有软限度,然而因为容器 root 用户的 UID 与宿主机统一都是 0,内核统计 pipe 使用量时没有做辨别,导致当 UID 为 0 的用户 pipe 使用量超过软限度后,新调配的 pipe 容量会变小。而 runc 1.0.0-rc93 正好会因为 pipe 容量太小,导致数据无奈残缺写入,写入阻塞,始终同步期待,进而 runc init 卡住,kubelet pleg 状态异样,节点 NotReady。

修复计划,runc 通过 goroutine 及时读取 pipe 内容,避免写入阻塞。

参考资料

https://iximiuz.com/en/posts/…

https://medium.com/@mccode/un…

https://man7.org/linux/man-pa…

https://gist.github.com/cyfde…

https://github.com/containerd…

https://github.com/opencontai…

举荐浏览

面试官问,Redis 是单线程还是多线程我懵了

【实操干货】做好这 16 项优化,你的 Linux 操作系统面目一新

正文完
 0