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

文摘   科技   2024-06-10 08:03   江苏  
我们在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版本),出现了内存消耗异常的情况。下面是出现异常的容器组件版本信息:

go1.16.15containerd: 1.5.11runc: 1.0.3

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

docker run -it --rm mysql:5.7.36docker run -it --rm mysql:5.5.62

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

docker run -it --rm mysql:8.0.29

OOM相关信息:

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=02023-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:02022-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容器异常地占用大量内存。

cat /proc/$(pgrep dockerd)/limits | grep "Max open files"cat /proc/$(pgrep containerd)/limits | grep "Max open files"

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

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

   1. xinetd slowly

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

$ cat /proc/$(pidof dockerd)/limits | grep "Max open files"Max open files           1048576             1048576             files
$ systemctl show docker | grep LimitNOFILELimitNOFILE=1048576

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

$ 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个文件描述符。

root@1b3165886528# strace xinetdexecve("/usr/sbin/xinetd", ["xinetd"], 0x7ffd3c2882e0 /* 9 vars */) = 0brk(NULL)                               = 0x557690d7a000arch_prctl(0x3001 /* ARCH_??? */, 0x7ffee17ce6f0) = -1 EINVAL (Invalid argument)mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb14255c000access("/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宿主机上,我们都进行了以下操作:

docker run -itd --name centos7 quay.io/centos/centos:centos7docker exec -it centos7 /bin/bash -c "time yum update -y"

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

docker run -itd --name centos7 quay.io/centos/centos:centos7docker exec -it centos7 /bin/bash -c "time yum update -y"

3. rpm slow

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

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

消耗的各类时间如下:

real   0m11.248suser   0m7.316ssys     0m1.932s

在容器中执行测试

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"

消耗的各类时间激增:

real   0m31.089suser   0m14.876ssys     0m12.524s

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

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存在问题的相关代码:

# 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 errormax_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 单元文件中被配置为无限制(特定版本):

$ grep ^Limit /lib/systemd/system/containerd.serviceLimitNOFILE=infinityLimitNPROC=infinityLimitCORE=infinity

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

$ ulimit -acore file size         (blocks, -c) 0data seg size           (kbytes, -d) unlimitedscheduling priority             (-e) 0file size               (blocks, -f) unlimitedpending signals                 (-i) 62435max locked memory       (kbytes, -l) 16384max memory size         (kbytes, -m) unlimitedopen files                     (-n) 1024pipe size           (512 bytes, -p) 8POSIX message queues     (bytes, -q) 819200real-time priority             (-r) 0stack size             (kbytes, -s) 8192cpu time               (seconds, -t) unlimitedmax user processes             (-u) 62435virtual memory         (kbytes, -v) unlimitedfile 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 的软限制。

测试详情

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 守护进程分别使用的任务和内存数量。

# 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 个容器:

# 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:

mkdir /sys/fs/cgroup/LimitTests.slicesystemd-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。然后尝试以下操作

# 创建文件夹并添加许多文件: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=1000free -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:

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:

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:

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限制超出

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

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。请检查服务配置:

[root@XXX ~]# ulimit -n -uopen files                      (-n) 1024max user processes              (-u) 499403

containerd的systemd服务配置如下:

cat /usr/lib/systemd/system/containerd.service[Unit]Description=containerd container runtimeDocumentation=https://containerd.ioAfter=network.target local-fs.target
[Service]ExecStartPre=-/sbin/modprobe overlayExecStart=/usr/local/bin/containerdType=notifyDelegate=yesKillMode=processRestart=alwaysRestartSec=5LimitNPROC=infinityLimitCORE=infinityLimitNOFILE=infinityTasksMax=infinityOOMScoreAdjust=-999
[Install]WantedBy=multi-user.target

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

[root@XXX ~]# cat /proc/$(pidof dockerd)/limitsLimit                     Soft Limit           Hard Limit           Units     Max open files            1048576              1048576              files
[root@XXX ~]# cat /proc/$(pidof containerd)/limitsLimit                     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




真诚推荐你关注



来个“分享、点赞、在看”👇

DCOS
CNCF 云原生基金会大使,CoreDNS 开源项目维护者。主要分享云原生技术、云原生架构、容器、函数计算等方面的内容,包括但不限于 Kubernetes,Containerd、CoreDNS、Service Mesh,Istio等等