Hi there 👋

同步近期 containerd 的高频问题

最近 Issue 8698 有用户说容器启动和清理都偏慢,尤其多个 Pod 同时启动时现象特别明显。之前有过类似的问题: containerd 启动容器前,它需要临时挂载 rootfs 来读取 uid/gid 信息。因为挂载的是可写属性的 overlayfs,卸载时内核会强制刷盘。当系统大量的脏页数据需要回写时,这个刷盘动作容易造成系统卡顿。 oci: use readonly mount to read user/group info 已经解决读取 uid/gid 的性能问题了,但这一次是 Pod Init-container 带来的 。 Init-container rootfs 大部分都是可写模式的 overlayfs,如果 Init-container 是做数据预下载的话,那么 containerd 在删除 Init-container 时,内核一定会刷盘。在大部分场景下,同一个节点上的 Pod 共享同一块数据盘,这种不预期的刷盘很容易把系统打崩。还有 Issue 8647 用户说,他的系统一开始还好好的,跑几天就不稳定了。后来查看他提供的日志,发现有几个 Pod 一直启动失败,相当于每隔几秒都要去刷盘,导致整个系统不稳定。 这个问题的最佳解决方案应该是做好 Pod 的存储隔离,但显然这成本确要高不少。然而 Kubernetes 场景下的容器并不会重启,即使在「失败后无限重启」的策略下,kubelet 依然是删除重建,这也意味着容器 rootfs 并不需要持久化。个人觉得,成本最低的解决方案应该是使用 overlayfs-volatile-mount,它需要 Linux Kernel ≥ 5.10。以下是个人目前了解到的情况,大部分云厂家都支持了 overlayfs-volatile。 Azure Ubuntu 22.04 LTS - Kernel 5.15 (GA) AWS Kubernetes ≥ 1....

August 13, 2023

containerd 1.7: 垃圾回收的拓展

之前和苦总/Ace 维护 pouch-container/containerd 的时候,我们遇到比较多的问题是资源泄漏,比如节点负载太高以至于无法 umount 容器根目录(容器 bundle 目录残留),内核 pidns 死锁问题导致容器进程僵尸态( cgroup 残留),还有进程重启导致 CNI 网络资源泄漏等等。对于内核死锁等问题,重启可能是唯一的解决方案;而那些因为短期高负载或者进程重启导致的资源泄漏,它们需要从系统层面解决,至少应该让资源的创建者有机会去清理。 containerd 它本身就具备垃圾回收能力,但它只关注内部资源的清理,比如镜像数据以及容器可写层。而 CNI 网络资源以及 containerd-shim 等外部资源并不在垃圾回收的管理范围内,这部分资源容易出现泄漏的情况,比如 GKE 平台的用户就多次遇到了 Containerd IP leakage : 直到 pause 容器创建之前,containerd 仅在内存里保持对网络资源的引用;在将网络资源绑定给 pod 以前,containerd 一旦被强制重启就会发生泄漏。当时 containerd 社区的处理方式是提前创建 pod 记录,以此来提前关联网络资源,并利用 kubelet 的垃圾回收机制来触发资源清理。问题倒是解决了,但该方案仅适用于 kubernetes 场景,最合理的方案应该是创建者应具备周期性的清理流程。 考虑我们还有 shim 资源泄漏的问题,containerd 社区提了 Add collectible resources to metadata gc 方案来管理外部资源清理。在介绍这个方案之前,我想先介绍下 containerd 的垃圾清理机制。 基于标签系统和 lease 构建的垃圾回收 containerd 核心插件 metadata 管理着容器和镜像数据,如下图所示,其中虚线方块代表着子模块,比如 Images 代表着 image service,它仅用来管理镜像名字和镜像 manifest 的映射关系,而镜像的 blob 数据和解压后的文件内容分别由 content service 和 snapshot service 管理;需要说明的是,container service 仅用来保存用于启动容器的配置信息,它并不负责管理容器进程的生命周期。...

March 19, 2023

containerd 1.7: Image Transfer Service

从 HELM Chart 成功对接容器镜像仓库开始,到近两年 OCI 社区 artifacts 标准化的落地推广,现在 OCI Registry as Storage 已经是事实标准,不同业务场景的数据存储都可以对接 OCI registry 的分发协议。同时,这几年容器镜像的安全供应链需求徒增,以及各大云厂商对容器镜像 lazy-loading 探索力度在加大,OCI 社区在 artifacts 标准的基础上为容器镜像引入 referrers 定义:在保证前后兼容的情况下,每一个容器镜像都可以通过关联的 artifacts 来拓展自身的属性,如下图所示,用户可以通过 referrers 关联特性为 net-monitor:v1 镜像提供了镜像签名和 SBOM 信息。当然,容器镜像 lazy-loading 属性也可以与之相关联,比如阿里云的 overlaybd,蚂蚁金服的 nydus 以及亚马逊的 soci。 NOTE: 图片取自 Feynman Zhou 发表的 Azure Container Registry: the first cloud registry to support the OCI Specifications 1.1 文章。 可预见的是,各大云厂商的镜像仓库都会跟进这一标准,安全供应链以及容器镜像加速的增值服务也会因此得到大面积推广。而作为容器镜像的消费端,为了支持这一特性,containerd 社区的想法是引入组合型插件 Image Transfer Service,将镜像分发和打包处理都抽象成数据流的转发,并统一收编在服务端处理。 Pull 需要新的抽象 在 containerd 1.7 版本之前,镜像数据处理的绝大部分操作都集中在客户端。containerd 设计偏向于把服务端做薄,服务端仅做底层资源的 CRUD;而客户端则采用类似 Backends For Frontend 理念来为复杂流程封装接口,如下图所示 Pull 镜像的函数接口。下载镜像涉及到镜像地址解析、密钥管理、并发下载以及解压处理,但该流程仅适用于下载标准格式的镜像,任何调整都容易产生不适。...

March 13, 2023

containerd 1.7: UserNamespace Stateless Pod

containerd 1.7 版本有比较多的实验特性。在这里,我会介绍 containerd 对 UserNamespace Stateless Pod 支持的情况,算是个人对 containerd 1.7 版本特性介绍系列的开篇。 1. UserNamespace 安全特性 Linux 内核是基于进程的 credentials(7) 凭证来做访问控制,比如进程的拥有者标识 UID/GID 和用于系统资源访问控制判定的 Effective UID/GID 等凭证。而 user_namespace(7) 提供了安全隔离特性。在不同的 user namespace(userns) 下,同一个进程不仅有不同的 UID/GID,同时还具备了不同的 capabilities(7) 权限。比如 u1001 用户进程 bash 通过 unshare -r bash 进入到了新的 userns,并将自己映射成了 root 用户,还具备了所有的 capabilities。但这个进程真的就变成了特权进程?这取决于该用户在系统资源所属的 userns 里是否拥有访问权限。 在介绍具体的判定规则前,先花点时间了解下内核是如何管理 UID/GID 的映射关系。 每个进程都必须归属于某一个 userns。在初始状态下,进程都属于 initial userns。Initial userns 比较特殊,它没有任何映射关系;Linux 支持嵌套的 userns,所有正在使用的 userns 组成的关系图将会是以 initial userns 为根的树状图。在 parent userns 里的进程,只要具备 CAP_SET{UID,GID} 能力,这些进程就可以通过 /proc/[pid]/{uid,gid}_map 文件接口,以 ID-inside-ns ID-outside-ns Range-length 的格式,给刚进入 child userns 的进程设置 UID/GID 映射关系,而创建该 userns 进程的 Effective UID(EUID) 将成为 userns 的所有者,如下图所示。...

March 4, 2023

使用 unshare(CLONE_FS) 来优化 OverlayFS 挂载

背景 在 Linux 平台上,大部分情况下会使用 OverlayFS 文件系统来管理容器镜像存储,而 OverlayFS 文件的特点也比较符合容器场景使用:它不仅可以将多个目录合并成统一的访问视图,还能做到读写分离。 mount -t overlay overlay \ -olowerdir=/lower1:/lower2:/lower3,upperdir=/upper,workdir=/work \ /merged 如上面的挂载命令所示, lowerdir 代表着容器镜像层解压后的目录。从 OCI Image 标准 定义来看,容器镜像的层数并没有限制。但 mount(2) 系统调用的参数被严格限制在 4KiB,所以实际使用的容器镜像层级有限制的。 为了解决这个层级的问题,Docker 采用压缩 lowerdir 参数来尽可能地支持更多层级的容器镜像。Docker 存储插件使用 l/${random-id(len=26)} 软链接指向实际的存储目录,然后跳到 /var/lib/docker/overlay2 目录下进行挂载,这样就不需要在 lowerdir 参数里重复填写 /var/lib/docker/overlay2/ 这 25 个字符。按照 Docker 代码里的注释,Overlay 镜像存储最大可支持到 128 层。 /var/lib/docker/overlay2/l/63WSQBTYICXV2O7SOZXAXYLAY2 -> ../f98d68377b05c44bacc062397f7ebaaf066b070fce15fbcfe824698d15f2eaa8/diff 但在当时,Go 并没有提供太多的线程操作,所有被 Go-Runtime 管理的线程都使用了 CLONE_FS。一旦某个 Goroutine 通过 Chdir 修改了当前工作目录,这会污染到整个进程,Docker 无法基于这样的方式来并发处理 OverlayFS 挂载请求,所以在当时只能选择 Fork-Exec 子进程来处理。考虑到维护多个二进制的成本过高,Docker 采用了 Re-exec 的方式。 不管怎么样,Fork-Exec 处理挂载成本很高,而且这样挂载逻辑没法独立成一个 Go Package,它要求使用者在 Go-Main-Init 函数里添加启动的预处理逻辑。所以在 containerd 项目里,我们采用了 Clone-Thread 的形式。...

October 15, 2022