容器的单进程模型

容器的单进程模型是指推荐在一个容器里只运行一个进程容器的单进程模型并不是说容器里只能运行一个进程,而是因为容器本身没有管理多个进程的能力,推荐在一个容器里只运行一个进程。 在正常的Linux操作系统里都有个1号进程init,例如CentOS的systemd,所有的其他进程都是1号进程的子进程或者是子进程的子进程。 1号进程具有进程管理能力,例如对僵尸进程的管理。而容器中的1号进程就是容器运行的应用本身,它是没有进程管理能力的,所以推荐在容器中只运行一个进程。

容器进程数的限制

子进程的创建后,一般需要父进程通过系统调用wait()waitpid()来等待子进程结束,以回收子进程的系统资源;另外还可以通过异步的方式,子进程结束后向父进程发送SIGCHLD信号,而父进程中需要注册一个SIGCHLD信号的处理函数在函数中回收子进程的系统资源。 僵尸进程是指进程结束系统资源没有被回收就变成了僵尸进程。僵尸进程会一直占有系统资源,例如进程id,操作系统有最大进程数的限制,超过这个限制系统将不能再创建任何进程。 如果子进程先于父进程退出,且父进程没有对子进程的残留资源进行回收的话,就会产生僵尸进程。如果父进程先于主进程退出,这个时候正在运行的子进程成为孤儿进程,此时操作系统会将孤儿进程的父进程设置为1(如systemd),这样可以确保systemd对孤儿进程的资源进行回收。

前面简单了解僵尸进程危害,虽然有推荐在一个容器里只运行一个进程这个指导建议,但不可避免有人在构建的容器镜像中启动很多个进程,或者因为bug导致启动容器时后运行过程中创建了很多子进程,因此就需要在启动容器时对容器的进程数量进行限制。

先看一下操作系统中的进程总数限制,一般服务器的CPU数量小于32,进程总数限制会被设置为32768,大于32会被设置为cpu数量乘以1024:

1cat /proc/sys/kernel/pid_max
232768

容器的本质就是进程,因此需要限制容器中的进程数量,恶意在容器中创建过多的进程,会直接影响宿主机上其他容器和其他程序的工作。限制容器中进程数量需要借助于pid cgroup子系统。 在使用cgroups时需要先挂载,例如在centos下pid cgroup子系统被挂载到了/sys/fs/cgroup/pids下,,在这个目录下是各个pids控制组目录,每个控制组目录下还可以有子目录,各个控制组形成了一个树状的层级关系。

例如在/sys/fs/cgroup/pids下创建一个名为foo的pids控制组目录,控制组目录中有如下内容:

1cd /sys/fs/cgroup/pids
2
3mkdir foo
4cd foo && ls
5cgroup.clone_children  cgroup.event_control  cgroup.procs  notify_on_release  pids.current  pids.max  tasks

关注pids.max文件的值,默认值是max表示不限制,可以为其写入一个数值,这个数值就是此pids控制组中最大进程数。

使用nerdctl启动containerd容器时,有两个选项与容器中进程相关:

1--pid value                   PID namespace to use
2--pids-limit value            Tune container pids limit (set -1 for unlimited) (default: -1)

--pid指定容器使用pid namespace是用来做隔离的,而--pids-limit用来限制容器中的最大进程数量。

测试启动一个容器,并限制最大进程数量是1:

1nerdctl run -it --rm --pids-limit=1 alpine:3.14 sh
2/ # ps
3sh: can't fork: Resource temporarily unavailable

上面的命令尝试启动了一个alpine容器,容器中的进程是sh,同时设置了最大进程数是1,在容器中尝试使用ps命令就会报sh: can't fork: Resource temporarily unavailable,即无法创建ps子进程。

如何KubernetesPod中的限制进程数量

既然容器的单进程模型是推荐在一个容器里只运行一个进程,Kubernetes中提供了Pod抽象,可以将多个容器编排到一个Pod里。 Kubernetes也提供了限制Pod中进程数量的功能,可以在k8s节点级别进行PID限制,也可以配置Pod级别的PID限制。

节点级别的PID限制,允许配置为k8s节点操作系统预留的一定的PID数量。k8s使用特性门控(FeatureGate)中的SupportNodePidsLimit来实现这功能,SupportNodePidsLimit这个特性门口在k8s 1.20以后的版本里面自动开启。 要为节点操作系统预留一定的数量的PID,例如在kubelet的配置文件中有以下配置:

1......
2systemReserved:
3  cpu: 0m
4  memory: 0Mi
5  pid: '1000'
6kubeReserved:
7  cpu: 0m
8  memory: 0Mi
9  pid: '100'

systemReserved用来配置为k8s node的系统预留的资源,kubeReserved用来配置为k8s系统组件预留的资源。这里配置为系统预留1000个pid,为k8s系统组件配置预留100个pid。

Pod级别的PID限制通过配置kubelet配置文件中podPidsLimit实现,默认值是-1表示不限制,下面配置为每个pod最大进程数是100:

1podPidsLimit: 100

可以到某个pod所在k8s节点的上的这个pod的pid cgroup目录里去查看, pids.max的值是100:

1cd /sys/fs/cgroup/pids/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-poda15fb63c_4335_49d2_957f_a90b1348197e.slice
2cat pids.max
3100

参考