限制容器中的进程

进程的两个状态

一个进程在活着的时候只有两个状态:运行态和睡眠态

1、运行态的意思是,无论进程是正在运行中(也就是获得了 CPU 资源),还是进程在 run queue 队列里随时可以运行,都处于这个状态。

使用ps命令,可以看到处于这个状态的进程显示的是R stat

2、睡眠态是指,进程需要等待某个资源而进入的状态,要等待的资源可以是一个信号量(Semaphore), 或者是磁盘 I/O,这个状态的进程会被放入到 wait queue 队列里。

睡眠状态还包括两个子状态:一个是可以被打断的(TASK_INTERRUPTIBLE),我们用 ps 查看到的进程,显示为 S stat。还有一个是不可被打断的(TASK_UNINTERRUPTIBLE),用 ps 查看进程,就显示为 D stat。

进程除了在活着时候的两个状态运行态和睡眠态,进程在调用do_exit()退出的时候,还有两个状态:

一个是EXIT_DEAD,也就是进程在真正结束退出的那一瞬间的状态;第二个是 EXIT_ZOMBIE 状态,这是进程在 EXIT_DEAD 前的一个状态,而僵尸进程,也就是处于这个状态中。

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

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

一台 Linux 机器上的进程总数目是有限制的。如果超过这个最大值,那么系统就无法创建出新的进程了,比如你想 SSH 登录到这台机器上就不行了。

这个最大值可以我们在 /proc/sys/kernel/pid_max 这个参数中看到。

Linux 内核在初始化系统的时候,会根据机器 CPU 的数目来设置 pid_max 的值。

比如说,如果机器中 CPU 数目小于等于 32,那么 pid_max 就会被设置为 32768(32K);如果机器中的 CPU 数目大于 32,那么 pid_max 就被设置为 N*1024 (N 就是 CPU 数目)。

对于 Linux 系统而言,容器就是一组进程的集合。如果容器中的应用创建过多的进程或者出现 bug,就会产生类似 fork bomb 的行为。这个 fork bomb 就是指在计算机中,通过不断建立新进程来消耗系统中的进程资源,它是一种黑客攻击方式。这样,容器中的进程数就会把整个节点的可用进程总数给消耗完。

这样,不但会使同一个节点上的其他容器无法工作,还会让宿主机本身也无法工作。所以对于每个容器来说,我们都需要限制它的最大进程数目,而这个功能由 pids Cgroup 这个子系统来完成。

KubernetesPod中的限制进程数量

Kubernetes也提供了限制Pod中进程数量的功能,可以在k8s节点级别进行PID限制,也可以配置Pod级别的PID限制。

节点级别的pid限制

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

......
systemReserved:
  cpu: 0m
  memory: 0Mi
  pid: '1000'
kubeReserved:
  cpu: 0m
  memory: 0Mi
  pid: '100'

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

pod级别的pid限制

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

podPidsLimit: 100

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

cd /sys/fs/cgroup/pids/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-poda15fb63c_4335_49d2_957f_a90b1348197e.slice
cat pids.max
100

我们对这个值做好限制,容器就不会因为创建出过多进程而影响到其他容器和宿主机了。

清理容器中的僵尸进程

进程号资源在宿主机上是有限的,残留的僵尸进程多了以后,给系统带来最大问题就是它占用了进程号。这就意味着,残留的僵尸进程,在容器里仍然占据着进程号资源,很有可能会导致新的进程不能运转。

造成僵尸进程的原因

父进程在创建完子进程之后就不管了,这就是造成子进程变成僵尸进程的原因。

找到原因,就好解决了。就可以让父进程去管理子进程就可以了。

所以,在 Linux 中的进程退出之后,如果进入僵尸状态,我们就需要父进程调用 wait() 这个系统调用,去回收僵尸进程的最后的那些系统资源,比如进程号资源。

那么,我们在刚才那段代码里,主进程进入 sleep(100) 之前,加上一段 wait() 函数调用,就不会出现僵尸进程的残留了。

for (i = 0; i < total; i++) {
		int status;
    wait(&status);
}

而容器中所有进程的最终父进程,就是我们所说的 init 进程,由它负责生成容器中的所有其他进程。因此,容器的 init 进程有责任回收容器中的所有僵尸进程。

但是 wait() 系统调用有一个问题,wait() 系统调用是一个阻塞的调用,也就是说,如果没有子进程是僵尸进程的话,这个调用就一直不会返回,那么整个进程就会被阻塞住,而不能去做别的事了。

Linux 还提供了一个类似的系统调用 waitpid(),这个调用的参数更多。

其中就有一个参数 WNOHANG,它的含义就是,如果在调用的时候没有僵尸进程,那么函数就马上返回了,而不会像 wait() 调用那样一直等待在那里。

总结:

1.,父进程在创建完子进程之后就不管了,而每一个 Linux 进程在退出的时候都会进入一个僵尸状态,这时这些进入僵尸状态的进程就因为无法回收变成僵尸进程。

2.僵尸进程是无法直接被kill掉的,需要父进程调用wait()或watipid()回收。

3.清理僵尸进程的两个思路 (1)kill掉僵尸进程的父进程,此时僵尸进程会归附到init(1)进程下,而init进程一般都有正常的wait()或watipid()回收机制。 (2)利用dumb-init/tini之类的小型init服务来解决僵尸进程

进程对每种信号的处理

进程对每种信号的处理,包括三个选择:调用系统缺省行为、捕获、忽略。而这里的选择,其实就是程序中如何去调用 signal() 这个系统调用。

缺省

第一个选择就是缺省,如果我们在代码中对某个信号,比如 SIGTERM 信号,不做任何 signal() 相关的系统调用,那么在进程运行的时候,如果接收到信号 SIGTERM,进程就会执行内核中 SIGTERM 信号的缺省代码。

对于 SIGTERM 这个信号来说,它的缺省行为就是进程退出(terminate)。

内核中对不同的信号有不同的缺省行为,一般会采用退出(terminate),暂停(stop),忽略(ignore)这三种行为中的一种。

捕获

第二个选择是捕获,捕获指的就是我们在代码中为某个信号,调用 signal() 注册自己的 handler。这样进程在运行的时候,一旦接收到信号,就不会再去执行内核中的缺省代码,而是会执行通过 signal() 注册的 handler。

忽略

第三个选择是忽略,如果要让进程“忽略”一个信号,我们就要通过 signal() 这个系统调用,为这个信号注册一个特殊的 handler,也就是 SIG_IGN 。

**注意:**SIGKILL 和 SIGSTOP 信号是两个特权信号,它们不可以被捕获和忽略,这个特点也反映在 signal() 调用上。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值