前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >RLIMIT_NOFILE设置陷阱:容器应用高频异常的元凶

RLIMIT_NOFILE设置陷阱:容器应用高频异常的元凶

作者头像
zouyee
发布2024-06-19 20:21:41
880
发布2024-06-19 20:21:41
举报
文章被收录于专栏:Kubernetes GOKubernetes GO

我们在Fedora系统上将containerd.io从1.4.13版本升级到了1.5.10之后,发现多个项目中所有MySQL 容器实例消耗内存暴涨超过20GB,而在此之前它们仅消耗不到300MB。同事直接上了重启大招,但重启后问题依旧存在。最后选择回滚到1.4.13版本,该现象也随之消失。

文|zouyee

为了帮助读者深入了解Kubernetes在各种应用场景下所面临的挑战和解决方案,以及如何进行性能优化。我们推出了<<Kubernetes经典案例30篇>>,该系列涵盖了不同的使用场景,从runc到containerd,从K8s到Istio等微服务架构,全面展示了Kubernetes在实际应用中的最佳实践。通过这些案例,读者可以掌握如何应对复杂的技术难题,并提升Kubernetes集群的性能和稳定性。

问题描述

我们在Fedora系统上将containerd.io从1.4.13版本升级到了1.5.10之后,发现多个项目中所有MySQL 容器实例消耗内存暴涨超过20GB,而在此之前它们仅消耗不到300MB。同事直接上了重启大招,但重启后问题依旧存在。最后选择回滚到1.4.13版本,该现象也随之消失。

值得注意的是,在Ubuntu 18.04.6系统上运行相同版本的containerd和runc时,MySQL 容器实例一切工作正常。只有在Fedora 35系统(配置相同的containerd与runc版本),出现了内存消耗异常的情况。下面是出现异常的容器组件版本信息:

代码语言:javascript
复制
go1.16.15
containerd: 1.5.11
runc: 1.0.3

在Fedora 35上,执行以下命令执行会引发系统崩溃:

代码语言:javascript
复制
docker run -it --rm mysql:5.7.36
docker run -it --rm mysql:5.5.62

但是mysql 8.0.29版本在Fedora 35上却运行正常:

代码语言:javascript
复制
docker run -it --rm mysql:8.0.29

OOM相关信息:

代码语言:javascript
复制
2023-06-06T17:23:24.094275-04:00 laptop kernel: oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=user.slice,mems_allowed=0,global_oom,task_memcg=/system.slice/docker-xxx.scope,task=mysqld,pid=38421,uid=0
2023-06-06T17:23:24.094288-04:00 laptop kernel: Out of memory: Killed process 38421 (mysqld) total-vm:16829404kB, anon-rss:12304300kB, file-rss:108kB, shmem-rss:0kB, UID:0 pgtables:28428kB oom_score_adj:0
2022-06-06T17:23:24.094313-04:00 laptop systemd[1]: docker-xxx.scope: A process of this unit has been killed by the OOM killer.
2022-06-06T17:23:24.856029-04:00 laptop systemd[1]: docker-xxx.scope: Deactivated successfully.

原先在空闲状态下,mysql容器使用内存大约在200MB左右;但在某些操作系统上,如RedHat、Arch Linux或Fedora,一旦为容器设置了非常高的打开文件数(nofile)限制,则可能会导致mysql容器异常地占用大量内存。

代码语言:javascript
复制
cat /proc/$(pgrep dockerd)/limits | grep "Max open files"
cat /proc/$(pgrep containerd)/limits | grep "Max open files"

如果输出值为1073741816或更高,那么您可能也会遇到类似异常。

在相关社区,我们发现了类似的案例:

1. xinetd slowly

xinetd服务启动极其缓慢,我们查看了dockerd的系统设置如下:

代码语言:javascript
复制
$ cat /proc/$(pidof dockerd)/limits | grep "Max open files"
Max open files           1048576             1048576             files
代码语言:javascript
复制
$ systemctl show docker | grep LimitNOFILE
LimitNOFILE=1048576

但是,在容器内部,则是一个非常巨大的数字——1073741816

代码语言:javascript
复制
$ docker run --rm ubuntu bash -c "cat /proc/self/limits" | grep "Max open files"
Max open files           1073741816           1073741816           files

xinetd程序在初始化时使用setrlimit(2)设置文件描述符的数量,这会消耗大量的时间及CPU资源去关闭1073741816个文件描述符。

代码语言:javascript
复制
root@1b3165886528# strace xinetd
execve("/usr/sbin/xinetd", ["xinetd"], 0x7ffd3c2882e0 /* 9 vars */) = 0
brk(NULL)                               = 0x557690d7a000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffee17ce6f0) = -1 EINVAL (Invalid argument)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb14255c000
access("/etc/ld.so.preload", R_OK)     = -1 ENOENT (No such file or directory)
close(12024371)                         = -1 EBADF (Bad file descriptor)
close(12024372)                         = -1 EBADF (Bad file descriptor)
close(12024373)                         = -1 EBADF (Bad file descriptor)
close(12024374)                         = -1 EBADF (Bad file descriptor)
close(12024375)                         = -1 EBADF (Bad file descriptor)
close(12024376)                         = -1 EBADF (Bad file descriptor)
close(12024377)                         = -1 EBADF (Bad file descriptor)
close(12024378)                         = -1 EBADF (Bad file descriptor)
2. yum hang

从docker社区获取Rocky Linux 9对应的Docker版本,在容器中执行yum操作时速度非常缓慢,在CentOS 7和Rocky Linux 9宿主机上,我们都进行了以下操作:

代码语言:javascript
复制
docker run -itd --name centos7 quay.io/centos/centos:centos7
docker exec -it centos7 /bin/bash -c "time yum update -y"

在CentOS 7宿主机上,耗时在2分钟左右;而在Rocky Linux 9上,一个小时也未能完成,复现步骤如下:

代码语言:javascript
复制
docker run -itd --name centos7 quay.io/centos/centos:centos7
docker exec -it centos7 /bin/bash -c "time yum update -y"
3. rpm slow

在宿主机上执行下述命令:

代码语言:javascript
复制
time zypper --reposd-dir /workspace/zypper/reposd --cache-dir /workspace/zypper/cache --solv-cache-dir /workspace/zypper/solv --pkg-cache-dir /workspace/zypper/pkg --non-interactive --root /workspace/root install rpm subversion

消耗的各类时间如下:

代码语言:javascript
复制
real   0m11.248s
user   0m7.316s
sys     0m1.932s

在容器中执行测试

代码语言:javascript
复制
docker run --rm --net=none --log-driver=none -v "/workspace:/workspace" -v "/disks:/disks" opensuse bash -c "time zypper --reposd-dir /workspace/zypper/reposd --cache-dir /workspace/zypper/cache --solv-cache-dir /workspace/zypper/solv --pkg-cache-dir /workspace/zypper/pkg --non-interactive --root /workspace/root install rpm subversion"

消耗的各类时间激增:

代码语言:javascript
复制
real   0m31.089s
user   0m14.876s
sys     0m12.524s

我们找到了RPM的触发问题的根因,其属于RPM内部POSIX lua库 rpm-software-management/rpm@7a7c31f。

代码语言:javascript
复制
static int Pexec(lua_State *L) /** exec(path,[args]) */
{
/* ... */
open_max = sysconf(_SC_OPEN_MAX);
if (open_max == -1) {
   open_max = 1024;
}
for (fdno = 3; fdno < open_max; fdno++) {
   flag = fcntl(fdno, F_GETFD);
   if (flag == -1 || (flag & FD_CLOEXEC))
continue;
   fcntl(fdno, F_SETFD, FD_CLOEXEC);
}
/* ... */
}

类似的,如果设置的最大打开文件数限制过高,那么luaext/Pexec()和lib/doScriptExec()在尝试为所有这些文件描述符设置FD_CLOEXEC标志时,会花费过多的时间,从而导致执行如rpm或dnf等命令的时间显著增加。

4. PtyProcess.spawn slowdown in close() loop

ptyprocess存在问题的相关代码:

代码语言:javascript
复制
# Do not allow child to inherit open file descriptors from parent,
# with the exception of the exec_err_pipe_write of the pipe
# and pass_fds.
# Impose ceiling on max_fd: AIX bugfix for users with unlimited
# nofiles where resource.RLIMIT_NOFILE is 2^63-1 and os.closerange()
# occasionally raises out of range error
max_fd = min(1048576, resource.getrlimit(resource.RLIMIT_NOFILE)[0])
spass_fds = sorted(set(pass_fds) | {exec_err_pipe_write})
for pair in zip([2] + spass_fds, spass_fds + [max_fd]):
    os.closerange(pair[0]+1, pair[1])

当处理文件描述符时,为了提高效率,应避免遍历所有可能的文件描述符来关闭它们,尤其是在Linux系统上,因为这会通过close()系统调用消耗大量时间。尤其是当打开文件描述符的限制(可以通过ulimit -n、RLIMIT_NOFILE或SC_OPEN_MAX查看)被设置得非常高时,这种遍历方式将导致数百万次不必要的系统调用,显著增加了处理时间。

一个更为高效的解决方案是仅关闭那些实际上已打开的文件描述符。在Python 3中,subprocess模块已经实现了这一功能,而对于使用Python 2的用户,subprocess32的兼容库可以作为回退选项。通过利用这些库或类似的技术,我们可以显著减少不必要的系统调用,从而提高程序的运行效率。

技术背景

1. RLIMIT_NOFILE

https://github.com/systemd/systemd/blob/1742aae2aa8cd33897250d6fcfbe10928e43eb2f/NEWS#L60..L94

当前Linux内核对于用户空间进程的RLIMIT_NOFILE资源限制默认设置为1024(软限制)和4096(硬限制)。以前,systemd在派生进程时会直接传递这些未修改的限制。在systemd240版本中,systemd传递的硬限制增加到了512K,其覆盖了内核的默认值,并大大增加了非特权用户空间进程可以同时分配的文件描述符数量。

注意,从兼容性考虑,软限制仍保持在1024,传统的UNIX select()调用无法处理大于或等于1024的文件描述符(FD_SET宏不管是否越界以及越界的后果,fd_set也并非严格限制在1024,FD_SET超过1024的值,会造成越界),因此如果全局提升了软限制,那么在使用select()时可能出现异常(在现代编程中,程序不应该再使用select(),而应该选择poll()/epoll,但遗憾的是这个调用仍然大规模存在)。

在较新的内核中,分配大量文件描述符在内存和性能上比以前消耗少得多。Systemd社区中有用户称在实际应用中他们使用了约30万个文件描述符,因此Systemd认为512K作为新的默认值是足够高的。但是需要注意的是,也有报告称使用非常高的硬限制(例如1G)是有问题的,因此,超高硬限制会触发部分应用程序中过大的内存分配。

2. File Descriptor Limits

最初,文件描述符(fd)主要用于引用打开的文件和目录等资源。如今,它们被用来引用Linux用户空间中几乎所有类型的运行时资源,包括打开的设备、内存(memfd_create(2))、定时器(timefd_create(2))甚至进程(通过新的pidfd_open(2)系统调用)。文件描述符的广泛应用使得“万物皆文件描述符”成为UNIX的座右铭。

由于文件描述符的普及,现代软件往往需要同时处理更多的文件描述符。与Linux上的大多数运行时资源一样,文件描述符也有其限制:一旦达到通过RLIMIT_NOFILE配置的限制,任何进一步的分配尝试都会被拒绝,并返回EMFILE错误,除非关闭一些已经打开的文件描述符。

以前文件描述符的限制普遍较低。当Linux内核首次调用用户空间时,RLIMIT_NOFILE的默认值设置为软限制1024和硬限制4096。软限制是实际生效的限制,可以通过程序自身调整到硬限制,但超过硬限制则需要更高权限。1024个文件描述符的限制使得文件描述符成为一种稀缺资源,导致开发者在使用时非常谨慎。这也引发了一些次要描述符的使用,例如inotify观察描述符,以及代码中频繁的文件描述符关闭操作(例如ftw()/nftw()),以避免达到限制。

一些操作系统级别的API在设计时只考虑了较低的文件描述符限制,例如BSD/POSIX的select(2)系统调用,它只能处理数字范围在0到1023内的文件描述符。如果文件描述符超出这个范围,select()将越界出现异常。

Linux中的文件描述符以整数形式暴露,并且通常分配为最低未使用的整数,随着文件描述符用于引用各种资源(例如eBPF程序、cgroup等),确实需要提高这个限制。

在2019年的systemd v240版本中,采取了一些措施:

  • 在启动时,自动将两个系统控制参数fs.nr_open和fs.file-max设置为最大值,使其实际上无效,从而简化了配置。
  • 将RLIMIT_NOFILE的硬限制大幅提高到512K。
  • 保持RLIMIT_NOFILE的软限制为1024,以避免破坏使用select()的程序。但每个程序可以自行将软限制提高到硬限制,无需特权。

通过这种方法,文件描述符变得不再稀缺,配置也更简便。程序可以在启动时自行提高软限制,但要确保避免使用select()。

具体建议如下:

  1. 不要再使用select()。使用poll()、epoll、io_uring等更现代的API。
  2. 如果程序需要大量文件描述符,在启动时将RLIMIT_NOFILE的软限制提高到硬限制,但确保避免使用select()。
  3. 如果程序会fork出其他程序,在fork之前将RLIMIT_NOFILE的软限制重置为1024,因为子进程可能无法处理高于1024的文件描述符。

这些建议能帮助你在处理大量文件描述符时避免常见问题。

3. select典型应用

supervisord

  • 在2011年,supervisord报告了一个与select()相关的问题,并在2014年得到修复。这表明supervisord早期版本可能使用了select(),但后续版本已更新。

Nginx

  • Nginx允许用户通过配置提高文件描述符的软限制。2015年的bug报告指出了Nginx在某些情况下使用select()并受限于1024个文件描述符的问题。目前,提供了多种方法来处理高并发场景。

Redis

  • Redis文档建议使用高达2^16的文件描述符数量,具体取决于实际工作负载。
    • 2013年12月,redis-py的select()问题,在2014年6月修复。
    • 2015年redis/hiredis的问题,用户依赖select()。
    • 2020年11月的文章提到Redis仍将select()作为后备方案,参考了ae_select.c文件。

Apache HTTP Server

  • 2002年的commit显示了Apache HTTP Server早期使用select()。尽管Apache后续增加了对其他I/O多路复用机制的支持,但在处理较低并发连接时,仍可能使用select()。

PostgreSQL

  • PostgreSQL没有硬限制,以避免对其他运行的软件产生负面影响。在容器化环境中,这个问题不太严重,因为可以为容器设置适当的限制。PostgreSQL提供了一个配置选项max_files_per_process,限制每个进程可以打开的最大文件数。
  • PostgreSQL的源代码中仍然有使用select()的地方。

MongoDB

  • 2014年,MongoDB仍在使用select()。在3.7.5版本中,select()仍在listen.cpp中使用,但在3.7.6版本(2018年4月)中被移除。不过,MongoDB的源代码中仍然存在select()的调用。

寻根溯源

虽然 cgroup 控制器在现代资源管理中起着重要作用,但 ulimit 作为一种传统的资源管理机制,依然不可或缺。

在容器中,默认的 ulimit 设置是从 containerd 继承的(而非 dockerd),这些设置在 containerd.service 的 systemd 单元文件中被配置为无限制(特定版本):

代码语言:javascript
复制
$ grep ^Limit /lib/systemd/system/containerd.service
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity

虽然这些设置满足 containerd 自身的需求,但对于其运行的容器来说,这样的配置显得过于宽松。相比之下,主机系统上的用户(包括 root 用户)的 ulimit 设置则相当保守(以下是来自 Ubuntu 18.04 的示例)

代码语言:javascript
复制
$ ulimit -a
core file size         (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 62435
max locked memory       (kbytes, -l) 16384
max memory size         (kbytes, -m) unlimited
open files                     (-n) 1024
pipe size           (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority             (-r) 0
stack size             (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes             (-u) 62435
virtual memory         (kbytes, -v) unlimited
file locks                     (-x) unlimited

这种宽松的容器设置可能会引发一系列问题,例如容器滥用系统资源,甚至导致 DoS 攻击。尽管 cgroup 限制通常用于防止这些问题,但将 ulimit 设置为更合理的值也是必要的。

特别当 RLIMIT_NOFILE(打开文件的数量限制)被设置为 2^30(即 1073741816)时,这会导致一些程序运行缓慢,因为这些程序会遍历所有可能打开的文件描述符,并在每次 fork/exec 之前关闭这些文件描述符(或设置 CLOEXEC 位)。以下是一些具体情况:

  • rpm:在安装 RPM 以创建新的 Docker 镜像时性能缓慢 #23137 和 Red Hat Bugzilla #1537564 中有报告,修复方案为:优化并统一在文件描述符上设置 CLOEXEC 的 rpm-software-management/rpm#444(在 Fedora 28 中修复)。
  • python2:在 Docker 18.09 上 PTY 进程的创建速度大大降低 #502 中有报告,建议的修复方案为:subprocess.Popen: 在 Linux 上优化 close_fds python/cpython#11584(由于 python2 已经冻结,所以此修复方案不会被采用)。
  • python 的 pexpect/ptyprocess 库:在 PtyProcess.spawn(以及因此 pexpect)在 close() 循环中速度降低 #50 中有报告。

逐一解决这些问题既复杂且收益低,其中一些软件已经过时,另外有一些软件难以修复。上述列表并不全面,可能还有更多类似的问题尚未觉察到。

探究资源消耗

2^16(65k)个busybox容器的预估资源使用情况如下所示:

  • 在 containerd 中,共需 688k 个任务和 206 GB(192 GiB)的内存(每个容器约需 10.5 个任务和 3 MiB 的内存)。
  • 至少需要将 containerd.service 的 LimitNOFILE 设置为 262144。
  • 打开的文件数达到 249 万(其中fs.file-nr 必须低于 fs.file-max 限制),每个容器大约需要 38 个文件描述符。
  • 容器的 cgroup 需要 25 GiB 的内存(每个容器大约需要 400 KiB)。

因此LimitNOFILE=524288(自 v240 版本以来,systemd 的默认值)对于大多数系统作为默认值已经足够,其能满足 docker.service 和 containerd.service 支持 65k 个容器的资源需求。

从GO 1.19开始将隐式地将 fork / exec 进程的软限制恢复到默认值。在此之前,Docker 守护进程可以通过配置 default-ulimit 设置来强制容器使用 1024 的软限制。

测试详情

代码语言:javascript
复制
Fedora 37 VM 6.1.9 kernel x86_64 (16 GB memory)
Docker v23, containerd 1.6.18, systemd v251

# Additionally verified with builds before Go 1.19 to test soft limit lower than the hard limit:
dnf install docker-ce-3:20.10.23 docker-ce-cli-1:20.10.23 containerd.io-1.6.8

在Fedora 37 VM上大约有 1800 个文件描述符被打开(sysctl fs.file-nr)。通过 shell 循环运行 busybox 容器直到失败,并调整 docker.service 和 containerd.service 的 LimitNOFILE 来收集测试数据:

  • docker.service - 6:1 的比例(使用 --network=host 时是 5:1),在 LimitNOFILE=5120 下大约能运行 853 个容器(使用主机网络时为 1024)。
  • containerd.service - 4:1 的比例(未验证 --network=host 是否会降低了比例),LimitNOFILE=1024 能支持 256 个容器,前提是 docker.service 的 LimitNOFILE 也足够高(如 LimitNOFILE=2048)。

每个容器的资源使用模式:

  • 每个容器的 systemd .scope 有 1 个任务和大约 400 KiB 的内存(alpine 和 debian 稍少)。
  • 每个容器增加了 10.5 个任务和 3 MiB 的内存。
  • 每个正在运行的容器大约打开了 38 个文件。

在 docker.service 中设置 LimitNOFILE=768,然后执行 systemctl daemon-reload && systemctl restart docker。通过 cat /proc/$(pidof dockerd)/limits 确认该限制是否已应用。

运行以下命令列出:

  • 正在运行的容器数量。
  • 打开的文件数量。
  • containerd 和 dockerd 守护进程分别使用的任务和内存数量。
代码语言:javascript
复制
# Useful to run before the loop to compare against output after the loop is done
(pgrep containerd-shim | wc -l) && sysctl fs.file-nr \
&& (echo 'Containerd service:' && systemctl status containerd | grep -E 'Tasks|Memory') \
&& (echo 'Docker service:' && systemctl status docker | grep -E 'Tasks|Memory')

运行以下循环时,最后几个容器将失败,大约创建 123 个容器:

代码语言:javascript
复制
# When `docker.service` limit is the bottleneck, you may need to `CTRL + C` to exit the loop
# if it stalls while waiting for new FDs once exhausted and outputting errors:
for i in $(seq 1 130); do docker run --rm -d busybox sleep 180; done

可以添加额外的选项:

  • --network host:避免每次 docker run 时向默认的 Docker 桥接器创建新的 veth 接口(参见 ip link)。
  • --ulimit "nofile=1023456789":不会影响内存使用,但在基于 Debian 的发行版中,值高于 fs.nr_open(1048576)将失败,请使用该值或更低的值。
  • --cgroup-parent=LimitTests.slice:类似 docker stats 但与其他容器隔离,systemd-cgtop 报告内存使用时包括磁盘缓存(可使用 sync && sysctl vm.drop_caches=3 清除)。

为更好了解所有创建容器的资源使用情况,创建一个用于测试的临时 slice:

代码语言:javascript
复制
mkdir /sys/fs/cgroup/LimitTests.slice
systemd-cgtop --order=memory LimitTests.slice

显示整个 slice 和每个容器的内存使用情况,一个 busybox 容器大约使用 400 KiB 的内存。

限制对子进程的影响

原本以为子进程会继承父进程的文件描述符(FD)限制。然而实际却是,每个进程继承限制但有独立的计数。

  • 可以通过以下命令观察 dockerd 和 containerd 进程打开的文件描述符数量:ls -1 /proc/$(pidof dockerd)/fd | wc -l。
  • 这不适用于负责容器的 containerd-shim 进程,所以 ls -1 /proc/$(pgrep --newest --exact containerd-shim)/fd | wc -l 不会有用。

为了验证这一点,可以运行以下测试容器:docker run --rm -it --ulimit "nofile=1024:1048576" alpine bash。然后尝试以下操作:

代码语言:javascript
复制
# 创建文件夹并添加许多文件:
mkdir /tmp/test && cd /tmp/test

# 创建空文件:
for x in $(seq 3 2048); do touch "${x}.tmp"; done

# 打开文件并指定文件描述符:
for x in $(seq 1000 1030); do echo "${x}"; eval "exec ${x}< ${x}.tmp"; done
# 因为软限制在 1024,所以会失败。提高限制:
ulimit -Sn 2048

# 现在前面的循环将成功。
# 你可以覆盖整个初始软限制范围(不包括 FDs 0-2:stdin、stdout、stderr):
for x in $(seq 3 1024); do echo "${x}"; eval "exec ${x}< ${x}.tmp"; done

# 多个容器进程/子进程打开尽可能多的文件:
# 可以在新 shell 进程中运行相同的循环 `ash -c 'for ... done'`
# 或通过另一个终端的 `docker exec` 进入容器并在 `/tmp/test` 再次运行循环。
# 每个进程可以根据其当前软限制打开文件,`dockerd`、`containerd` 或容器的 PID 1 的限制无关。

############
### 提示 ###
############

# 可以观察当前应用的限制:
cat /proc/self/limits
# 如果未达到软限制(由于管道),这将报告已使用的限制:
ls -1 /proc/self/fd | wc -l
# 否则,若这是唯一运行的 `ash` 进程,可以查询其 PID 获取信息:
ls -1 /proc/$(pgrep --newest --exact ash)/fd | wc -l

# 容器中的进程数:
# `docker stats` 列出容器的 PIDs 数量,
# `systemd-cgtop` 的 Tasks 列也报告相同值。
# 或者如果知道 cgroup 名称,如 `docker-<CONTAINER_ID>.scope`:
# (注意:路径可能因 `--cgroup-parent` 不同)
cat /sys/fs/cgroup/system.slice/docker-<CONTAINER_ID>.scope/pids.current

# 列出进程及其 PIDs:
# 对于单个容器,可以可视化进程树:
pstree --arguments --show-pids $(pgrep --newest --exact containerd-shim)
# 或者如果知道 cgroup 名称,如 `docker-<CONTAINER_ID>.scope`:
systemd-cgls --unit docker-<CONTAINER_ID>.scope

# 观察内存监控中的磁盘缓存,通过创建 1GB 文件:
dd if=/dev/zero of=bigfile bs=1M count=1000
free -h
# `systemd-cgtop` 会将此容器的内存使用量增加 1GB,
# 而 `docker stats` 仅增加约 30MiB(按比例)。
# 在容器外清除缓存后再次观察内存使用情况:
sync && sysctl vm.drop_caches=3

结果观察如下:

  • 每个进程将这些文件描述符添加到 fs.file-nr 返回的打开文件计数中,并在该进程关闭时释放它们。
  • 重新运行同一进程的循环不会变化,因为文件已经被计算为该进程打开的。
  • 这涉及到内存成本:
    • 每个通过 touch 创建的文件大约占用 2048 字节(仅在打开前占用磁盘缓存)。
  • 每个打开的文件(每个文件描述符引用都会使 fs.file-nr 增加)大约需要 512 字节的内存。
    • 以这种方式创建 512k 个文件大约会占用 1.1 GiB 的内存(当至少有一个文件描述符打开时,使用 sysctl vm.drop_caches=3 也不会释放),每个进程打开等量的文件描述符还会额外使用 250 MiB(262 MB)。

错误处理

这些问题主要与系统服务的文件描述符限制有关,不同服务的限制耗尽会导致不同错误。

有时这会导致任何docker命令(如docker ps)挂起(守护进程耗尽限制)。常见现象包括:

  • 容器未运行(pgrep containerd-shim没有输出,但docker ps列出的容器超出预期的退出时间)。
  • 容器在containerd-shim进程中占用内存,即使执行了systemctl stop docker containerd。有时需要pkill containerd-shim来清理,并且systemctl start docker containerd会在journalctl中记录错误,处理已死的shims的清理(根据容器数量,这可能会超时,需要再次启动containerd服务)。
  • 即使排除了所有这些因素,仍然有额外的几百MB内存使用。由于它似乎不属于任何进程,推测是内核内存。我尝试运行的最大容器数量大约是1600个左右。
docker.service超出限制

每次docker run时,系统会输出不同的错误:

case1:

代码语言:javascript
复制
ERRO[0000] Error waiting for container: container caff476371b6897ef35a95e26429f100d0d929120ff1abecc8a16aa674d692bf: driver "overlay2" failed to remove root filesystem: open /var/lib/docker/overlay2/35f26ec862bb91d7c3214f76f8660938145bbb36eda114f67e711aad2be89578-init/diff/etc: too many open files

case2:

代码语言:javascript
复制
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error running hook #0: error running hook: exit status 1, stdout: , stderr: time="2023-03-12T02:26:20Z" level=fatal msg="failed to create a netlink handle: could not get current namespace while creating netlink socket: too many open files": unknown.

case3:

代码语言:javascript
复制
docker: Error response from daemon: failed to initialize logging driver: open /var/lib/docker/containers/b014a19f7eb89bb909dee158d21f35f001cfeb80c01e0078d6f20aac8151573f/b014a19f7eb89bb909dee158d21f35f001cfeb80c01e0078d6f20aac8151573f-json.log: too many open files.
containerd.service限制超出

我也观察到一些类似的错误:

代码语言:javascript
复制
docker: Error response from daemon: failed to start shim: start failed: : pipe2: too many open files: unknown.

总结

我们先看看Docker及Containerd社区关于LimitNOFILE变更历史:

  • 2023年8月:在docker.service中移除了LimitNOFILE=infinity。
  • 2021年5月:LimitNOFILE=infinity 和 LimitNPROC=infinity 重新添加回docker.service,以与Docker CE的配置同步。
  • 2016年7月:LimitNOFILE=infinity更改为LimitNOFILE=1048576。
    • 讨论引用了2009年StackOverflow上的回答,关于特定发行版/内核中infinity被限制为2^20。今天的一些系统上,这个上限是(2^30 == 1073741816,超过10亿)。
  • 2016年7月:LimitNOFILE和LimitNPROC从1048576更改为infinity。
  • 2014年3月:原始LimitNOFILE + LimitNPROC以1048576添加。
    • 链接的PR评论提到这个2^20的值已经高于Docker所需。

当前状态:

  • 在Docker v25之前,LimitNOFILE=infinity仍然是默认设置,除非将其回退。
  • containerd 已经合并了相应的更改,从他们的systemd服务文件中移除了LimitNOFILE设置。

Systemd < 240

在某些systemd版本中,因systemd bug,导致设置LimitNOFILE为无穷大却未生效,而是被设置为65536。请检查服务配置:

代码语言:javascript
复制
[root@XXX ~]# ulimit -n -u
open files                      (-n) 1024
max user processes              (-u) 499403

containerd的systemd服务配置如下:

代码语言:javascript
复制
cat /usr/lib/systemd/system/containerd.service
[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target local-fs.target

[Service]
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/containerd
Type=notify
Delegate=yes
KillMode=process
Restart=always
RestartSec=5
LimitNPROC=infinity
LimitCORE=infinity
LimitNOFILE=infinity
TasksMax=infinity
OOMScoreAdjust=-999

[Install]
WantedBy=multi-user.target

查看配置对docker和containerd进程的影响:

代码语言:javascript
复制
[root@XXX ~]# cat /proc/$(pidof dockerd)/limits
Limit                     Soft Limit           Hard Limit           Units     
Max open files            1048576              1048576              files
代码语言:javascript
复制
[root@XXX ~]# cat /proc/$(pidof containerd)/limits
Limit                     Soft Limit           Hard Limit           Units     
Max open files            1048576              1048576              files  

这个补丁使systemd查看/proc/sys/fs/nr_open来找到内核中编译的当前最大打开文件数,并尝试将RLIMIT_NOFILE的最大值设置为此值。这样做的好处是所选的限制值不太随意,并且改善了在设置了rlimit的容器中systemd的行为。

由于笔者时间、视野、认知有限,本文难免出现错误、疏漏等问题,期待各位读者朋友、业界专家指正交流。

参考文献

1. https://github.com/moby/moby/issues/45838

2. https://github.com/moby/moby/issues/38814

3. https://www.codenong.com/cs105896693/

4. https://github.com/moby/moby/issues/23137

5. https://0pointer.net/blog/file-descriptor-limits.html

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-06-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DCOS 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. xinetd slowly
  • 2. yum hang
  • 3. rpm slow
  • 4. PtyProcess.spawn slowdown in close() loop
  • 2. File Descriptor Limits
  • 3. select典型应用
  • 探究资源消耗
  • 测试详情
  • 限制对子进程的影响
  • 结果观察如下:
  • 错误处理
    • docker.service超出限制
      • containerd.service限制超出
      • Systemd < 240
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档