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

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

  1. Containerd CVE-2020–15257细节说明
  2. OpenAI关于Kubernetes集群近万节点的生产实践
  3. 一条K8s命令行引发的血案
  4. 揭开K8s适配CgroupV2内存虚高的迷局
  5. 探索Kubernetes 1.28调度器OOM的根源
  6. 解读Kubernetes常见错误码
  7. RLIMIT_NOFILE设置陷阱:容器应用高频异常的隐形元凶
  8. 容器干扰检测与治理(上篇)

问题描述

针对该问题的信息做了部分加工处理,thanks polarathene

我们在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版本),出现了内存消耗异常的情况。下面是出现异常的容器组件版本信息:

1
2
3
go1.16.15
containerd: 1.5.11
runc: 1.0.3

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

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

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

1
docker run -it --rm mysql:8.0.29

OOM相关信息:

1
2
3
4
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容器异常地占用大量内存。

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

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

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

xinetd slowly

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)

yum hang

从docker社区获取Rocky Linux 9对应的Docker版本,在容器中执行yum操作时速度非常缓慢。

在CentOS 7和Rocky Linux 9宿主机上,我们都进行了以下操作:

1
2
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上,一个小时也未能完成。

复现步骤如下:

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

rpm slow

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

1
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

消耗的各类时间如下:

1
2
3
real    0m11.248s
user 0m7.316s
sys 0m1.932s

在容器中执行测试

1
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"

消耗的各类时间激增:

1
2
3
real    0m31.089s
user 0m14.876s
sys 0m12.524s

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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标志时,会花费过多的时间,从而导致执行如rpmdnf等命令的时间显著增加。

PtyProcess.spawn slowdown in close() loop

ptyprocess存在问题的相关代码:

1
2
3
4
5
6
7
8
9
10
# 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 -nRLIMIT_NOFILESC_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_openfs.file-max设置为最大值,使其实际上无效,从而简化了配置。
  • RLIMIT_NOFILE的硬限制大幅提高到512K。
  • 保持RLIMIT_NOFILE的软限制为1024,以避免破坏使用select()的程序。但每个程序可以自行将软限制提高到硬限制,无需特权。

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

具体建议如下:

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

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

select

supervisord

Nginx

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

Redis

Apache HTTP Server

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

PostgreSQL

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

MongoDB

寻根溯源

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

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

1
2
3
4
$ grep ^Limit /lib/systemd/system/containerd.service
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sh复制代码$ 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 位)。以下是一些具体情况:

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

探究资源消耗

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

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

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

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

  1. 测试详情
1
2
3
4
5
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.servicecontainerd.serviceLimitNOFILE 来收集测试数据:

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

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

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

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

运行以下命令列出:

  • 正在运行的容器数量。
  • 打开的文件数量。
  • containerddockerd 守护进程分别使用的任务和内存数量。
1
2
3
4
# 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 个容器:

1
2
3
# 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:

1
2
mkdir /sys/fs/cgroup/LimitTests.slice
systemd-cgtop --order=memory LimitTests.slice

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

  1. 限制对子进程的影响

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

  • 可以通过以下命令观察 dockerdcontainerd 进程打开的文件描述符数量: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。然后尝试以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 创建文件夹并添加许多文件:
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
  1. 结果观察
  • 每个进程将这些文件描述符添加到 fs.file-nr 返回的打开文件计数中,并在该进程关闭时释放它们。
  • 重新运行同一进程的循环不会变化,因为文件已经被计算为该进程打开的。
  • 这涉及到内存成本:
    • 每个通过 touch 创建的文件大约占用 2048 字节(仅在打开前占用磁盘缓存)。
  • 每个打开的文件(每个文件描述符引用都会使 fs.file-nr 增加)大约需要 512 字节的内存。
    • 以这种方式创建 512k 个文件大约会占用 1.1 GiB 的内存(当至少有一个文件描述符打开时,使用 sysctl vm.drop_caches=3 也不会释放),每个进程打开等量的文件描述符还会额外使用 250 MiB(262 MB)。
  1. 错误处理

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

有时这会导致任何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时,系统会输出不同的错误:

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

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

1
docker: Error response from daemon: failed to start shim: start failed: : pipe2: too many open files: unknown.

总结

当前状态:

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

Systemd < 240

在某些systemd版本中,设置LimitNOFILE为无穷大可能导致它被限制为65536。请检查服务配置:

1
2
3
[root@XXX ~]# ulimit -n -u
open files (-n) 1024
max user processes (-u) 499403

containerd的systemd服务配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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进程的影响:

1
2
3
[root@XXX ~]# cat /proc/$(pidof dockerd)/limits
Limit Soft Limit Hard Limit Units
Max open files 1048576 1048576 files
1
2
3
[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的行为。

详细讨论见:systemd GitHub issue

参考链接

  1. https://github.com/moby/moby/issues/45838
  2. https://github.com/moby/moby/issues/23137
  3. https://0pointer.net/blog/file-descriptor-limits.html
  4. https://www.codenong.com/cs105896693/
  5. https://github.com/moby/moby/issues/38814
  6. https://github.com/cri-o/cri-o/issues/7703
  7. https://github.com/envoyproxy/envoy/issues/31502
  8. https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#Process%20Properties