容器与pod中的僵尸进程

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 容器与pod中的僵尸进程

三种状态的进程模型


按进程在执行过程中的不同情况至少要定义三种状态:


  • 运行(running)态:进程占有处理器正在运行的状态。进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态;在多处理机系统中,则有多个进程处于执行状态。
  • 就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行的状态。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行,进程这时的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。
  • 等待(wait)态:又称阻塞态或睡眠态,指进程不具备运行条件,正在等待某个时间完成的状态。也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。


640.png


引起进程状态转换的具体原因如下:


  • 运行态→等待态:等待使用资源;如等待外设传输;等待人工干预。
  • 等待态→就绪态:资源得到满足;如外设传输结束;人工干预完成。
  • 运行态→就绪态:运行时间片到;出现有更高优先权进程。
  • 就绪态—→运行态:CPU 空闲时选择一个就绪进程。


五种状态的进程模型


五态模型在三态模型的基础上增加了新建态(new)和终止态(exit)。


  • 新建态:对应于进程被创建时的状态,尚未进入就绪队列。创建一个进程需要通过两个步骤:1.为新进程分配所需要资源和建立必要的管理信息。2.设置该进程为就绪态,并等待被调度执行。
  • 终止态:指进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。处于终止态的进程不再被调度执行,下一步将被系统撤销,最终从系统中消失。终止一个进程需要两个步骤:1.先等待操作系统或相关的进程进行善后处理(如抽取信息)。2.然后回收占用的资源并被系统删除。


640.png


引起进程状态转换的具体原因如下:


  • NULL→新建态:执行一个程序,创建一个子进程。
  • 新建态→就绪态:当操作系统完成了进程创建的必要操作,并且当前系统的性能和虚拟内存的容量均允许。
  • 运行态→终止态:当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结。
  • 运行态→就绪态:运行时间片到;出现有更高优先权进程。
  • 运行态→等待态:等待使用资源;如等待外设传输;等待人工干预。
  • 就绪态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
  • 等待态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
  • 终止态→NULL:完成善后操作。


linux的进程状态


fcd5b5335d432490e5d058f67152b9ac.png


无论进程还是线程,在 Linux 内核里其实都是用 task_struct{}这个结构来表示的。它其实就是任务(task),也就是 Linux 里基本的调度单位。


Linux进程状态有:


  • TASK_RUNNING : 就绪态或者运行态,进程就绪可以运行,但是不一定正在占有CPU,对应进程状态的R。
  • TASK_INTERRUPTIBLE:睡眠态,但是进程处于浅度睡眠,可以响应信号,一般是进程主动sleep进入的状态,对应进程状态S。
  • TASK_UNINTERRUPTIBLE:睡眠态,深度睡眠,不响应信号,典型场景是进程获取信号量阻塞,对应进程状态D。
  • TASK_ZOMBIE:僵尸态,进程已退出或者结束,但是父进程还不知道,没有回收时的状态,结束之前的一个状态,对应进程状态Z。
  • EXIT_DEAD:结束态,进程已经结束,也就是进程结束退出那一瞬间的状态,对应进程状态X。
  • TASK_STOPED:停止,调试状态,对应进程状态T。


孤儿进程


一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1,也有可能是容器中的init)所收养,并由init进程对它们完成状态收集工作。孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。


僵尸进程


在类UNIX系统中,僵尸进程是指完成执行(通过 exit 系统调用,或运行时发生致命错误或收到终止信号所致),但在操作系统的进程表中仍然存在其进程控制块,处于"终止状态"的进程。我们从上面的概念中得知,僵尸进程仍然存在在进程表中。进程会占用系统系统资源,僵尸进程过多会导致资源泄露,最主要的资源就是PID。我们来看一下linux系统中的PID。这个最大值可以我们在 /proc/sys/kernel/pid_max 这个参数中看到。


[root@k8s-dev]# cat /proc/sys/kernel/pid_max
32768


Linux 内核在进行初始化时,会根据CPU 的数目对 pid_max 进行设置。


  • 如果CPU 的数目小于等于32,那么 pid_max 会被设置为32768;
  • 如果CPU 的数目大于32,那么 pid_max = 1024 * (CPU 数目)。


所以如果超过这个最大值,那么系统就无法创建出新的进程了,比如你想 SSH 登录到这台机器上就不行了。清理僵尸进程:了解了僵尸进程的危害,我们来看看怎么清理僵尸进程:收割僵尸进程的方法是通过kill命令手工向其父进程发送SIGCHLD信号。如果其父进程仍然拒绝收割僵尸进程,则终止父进程,使得init进程收养僵尸进程。init进程周期执行wait系统调用收割其收养的所有僵尸进程。为避免产生僵尸进程,实际应用中一般采取的方式是:


  • 将父进程中对SIGCHLD信号的处理函数设为SIG_IGN(忽略信号);
  • fork两次并杀死一级子进程,令二级子进程成为孤儿进程而被init所“收养”、清理。


docker容器中的进程


容器中的PID


在Docker中,进程管理的基础就是Linux内核中的PID名空间技术。在不同PID名空间中,进程ID是独立的;即在两个不同名空间下的进程可以有相同的PID。


Linux内核为所有的PID名空间维护了一个树状结构:最顶层的是系统初始化时创建的root namespace(根名空间),再创建的新PID namespace就称之为child namespace(子名空间),而原先的PID名空间就是新创建的PID名空间的parent namespace(父名空间)。通过这种方式,系统中的PID名空间会形成一个层级体系。父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响。反过来,子节点不能看到父节点名空间中的任何内容,也不可能通过kill或ptrace影响父节点或其他名空间中的进程。


在Docker中,每个Container都是Docker Daemon的子进程,每个Container进程缺省都具有不同的PID名空间。通过名空间技术,Docker实现容器间的进程隔离。另外Docker Daemon也会利用PID名空间的树状结构,实现了对容器中的进程交互、监控和回收。注:Docker还利用了其他名空间(UTS,IPC,USER)等实现了各种系统资源的隔离,由于这些内容和进程管理关联不多,本文不会涉及。


容器退出


当创建一个Docker容器的时候,就会新建一个PID名空间。容器启动进程在该名空间内PID为1。当PID1进程结束之后,Docker会销毁对应的PID名空间,并向容器内所有其它的子进程发送SIGKILL。


当执行docker stop命令时,docker会首先向容器的PID1进程发送一个SIGTERM信号,用于容器内程序的退出。如果容器在收到SIGTERM后没有结束, 那么Docker Daemon会在等待一段时间(默认是10s)后,再向容器发送SIGKILL信号,将容器杀死变为退出状态。这种方式给Docker应用提供了一个优雅的退出(graceful stop)机制,允许应用在收到stop命令时清理和释放使用中的资源。


docker kill可以向容器内PID1进程发送任何信号,缺省是发送SIGKILL信号来强制退出应用。


容器中的僵尸进程


产生原因


容器化后,由于单容器单进程,已经没有传统意义上的 init 进程了。应用进程直接占用了 pid 1 的进程号。从而导致以下两个问题。


  1. 常见的使用是 docker run my-container script 。给 docker run进程发送SIGTERM 信号会杀掉 docker run 进程,但是容器还在后台运行。


  • 2.当进程退出时,它会变成僵尸进程,直到它的父进程调用 wait()( 或其变种 ) 的系统调用。process table 里面会把它的标记为 defunct 状态。一般情况下,父进程应该立即调用 wait(), 以防僵尸进程时间过长。


如果父进程在子进程之前退出,子进程会变成孤儿进程, 它的父进程会变成 PID 1。因此,init进程就要对这些进程负责,并在适当的时候调用 wait() 方法。通常情况下,大部分应用进程不会处理偶然依附在自己进程上的随机子进程,所以在容器中,会出现许多僵尸进程。


解决方案


解决这个的办法就是pid为1的跑一个支持信号转发且支持回收孤儿僵尸进程的进程就行了,为此有人开发出了tini项目,感兴趣可以github上搜下下,现在tini已经内置在docker里了。使用tini可以在docker run的时候添加选项–init即可,底层我猜测是复制docker-init到容器的/dev/init路径里然后启动entrypoint cmd,大家可以在run的时候测试下上面的步骤会发现根本不会有僵尸进程遗留。这里不多说,如果是想默认使用tini可以把tini构建到镜像里(例如k8s目前不支持docker run 的--init,所以需要把tini做到镜像里),参照jenkins官方镜像dockerfile和tini的github地址文档 https://github.com/krallin/tini


k8s pod中的僵尸进程


k8s 可以将多个容器编排到一个 pod 里面,共享同一个 Linux Namespace。这项技术的本质是使用 k8s 提供一个 pause 镜像,也就是说先启动一个 pause 容器,相当于实例化出 Namespace,然后其他容器加入这个 Namespace 从而实现 Namespace 的共享。我们来介绍一下 pause。


pause 是 k8s 在 1.16 版本引入的技术,要使用 pause,我们只需要在 pod 创建的 yaml 中指定 shareProcessNamespace 参数为 true,如下:


apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  shareProcessNamespace: true
  containers:
  - name: nginx
    image: nginx
  - name: shell
    image: busybox
    securityContext:
      capabilities:
        add:
        - SYS_PTRACE
    stdin: true
    tty: true


attach到pod中,ps查看进程列表:


/ # kubectl attach POD -c CONTAINER 
/ # ps ax
PID   USER     TIME  COMMAND
    1 root      0:00 /pause
    8 root      0:00 nginx: master process nginx -g daemon off;
   14 101       0:00 nginx: worker process
   15 root      0:00 sh
   21 root      0:00 ps ax


我们可以看到 pod 中的 1 号进程变成了 /pause,其他容器的 entrypoint 进程都变成了 1 号进程的子进程。这个时候开始逐渐逼近事情的本质了:/pause 进程是如何处理 将孤儿进程的父进程置为 1 号进程进而避免僵尸进程的呢?


pause 镜像的源码如下:pause.c


#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
static void sigdown(int signo) {
  psignal(signo, "Shutting down, got signal");
  exit(0);
}
// 关注1
static void sigreap(int signo) {
  while (waitpid(-1, NULL, WNOHANG) > 0)
    ;
}
int main(int argc, char **argv) {
  int i;
  for (i = 1; i < argc; ++i) {
    if (!strcasecmp(argv[i], "-v")) {
      printf("pause.c %s\n", VERSION_STRING(VERSION));
      return 0;
    }
  }
  if (getpid() != 1)
    /* Not an error because pause sees use outside of infra containers. */
    fprintf(stderr, "Warning: pause should be the first process\n");
  if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 1;
  if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 2;
  // 关注2
  if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
                                             .sa_flags = SA_NOCLDSTOP},
                NULL) < 0)
    return 3;
  for (;;)
    pause(); // 编者注:该系统调用的作用是wait for signal
  fprintf(stderr, "Error: infinite loop terminated\n");
  return 42;
}


重点关注一下void sigreap(int signo){...}和if (sigaction(SIGCHLD,...) ,这个不就是我们上面说的 除了这种方式外,还可以通过异步的方式来进行回收,这种方式的基础是子进程结束之后会向父进程发送 SIGCHLD 信号,基于此父进程注册一个 SIGCHLD 信号的处理函数来进行子进程的资源回收就可以了。


SIGCHLD 信号的处理函数核心就是这一行 while (waitpid(-1, NULL, WNOHANG) > 0) ,其中各参数示意如下:


  • -1:meaning wait for any child process.
  • NULL:?
  • WNOHANG :return immediately if no child has exited.


结论


得出pause 容器的两个最重要的特性:


  • 在 pod 中作为容器共享namespace的基础
  • 作为 pod 内的所有容器的父容器,扮演 init 进程(即systemd)的作用。


最后


通过这篇文章的学习,大家能解决容器中出现僵尸进程的问题。

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
5月前
|
存储 消息中间件 容器
当一个 Pod 中包含多个容器时,容器间共享一些重要的资源和环境,这使得它们能够更有效地协同工作和交互。
当一个 Pod 中包含多个容器时,容器间共享一些重要的资源和环境,这使得它们能够更有效地协同工作和交互。
|
5月前
|
Kubernetes 开发者 容器
"Kubernetes的生死抉择:揭秘Pod容器重启策略如何决定应用命运的惊天大戏"
【8月更文挑战第20天】Kubernetes (k8s) 是一个强大的容器编排平台,其中Pod是最小的运行单元。Pod的重启策略确保服务连续性,主要有Always(总是重启)、OnFailure(失败时重启)和Never(从不重启)。默认策略为Always。根据不同场景,如Web服务、批处理作业或一次性任务,可以选择合适的策略。K8s还支持健康检查等高级机制来控制容器重启。合理配置这些策略对维护应用稳定性至关重要。
182 4
|
2月前
|
存储 Kubernetes Docker
【赵渝强老师】Kubernetes中Pod的基础容器
Pod 是 Kubernetes 中的基本单位,代表集群上运行的一个进程。它由一个或多个容器组成,包括业务容器、基础容器、初始化容器和临时容器。基础容器负责维护 Pod 的网络空间,对用户透明。文中附有图片和视频讲解,详细介绍了 Pod 的组成结构及其在网络配置中的作用。
【赵渝强老师】Kubernetes中Pod的基础容器
|
2月前
|
运维 Kubernetes Shell
【赵渝强老师】K8s中Pod的临时容器
Pod 是 Kubernetes 中的基本调度单位,由一个或多个容器组成,包括业务容器、基础容器、初始化容器和临时容器。临时容器用于故障排查和性能诊断,不适用于构建应用程序。当 Pod 中的容器异常退出或容器镜像不包含调试工具时,临时容器非常有用。文中通过示例展示了如何使用 `kubectl debug` 命令创建临时容器进行调试。
|
2月前
|
Kubernetes 调度 容器
【赵渝强老师】K8s中Pod中的业务容器
Pod 是 Kubernetes 中的基本调度单元,由一个或多个容器组成。除了业务容器,Pod 还包括基础容器、初始化容器和临时容器。本文通过示例介绍如何创建包含业务容器的 Pod,并提供了一个视频讲解。示例中创建了一个名为 &quot;busybox-container&quot; 的业务容器,并使用 `kubectl create -f firstpod.yaml` 命令部署 Pod。
|
2月前
|
Kubernetes 容器 Perl
【赵渝强老师】K8s中Pod中的初始化容器
Kubernetes的Pod包含业务容器、基础容器、初始化容器和临时容器。初始化容器在业务容器前运行,用于执行必要的初始化任务。本文介绍了初始化容器的作用、配置方法及优势,并提供了一个示例。
|
3月前
|
Linux C++
Linux c/c++进程之僵尸进程和守护进程
这篇文章介绍了Linux系统中僵尸进程和守护进程的概念、产生原因、解决方法以及如何创建守护进程。
42 0
|
5月前
|
消息中间件 Kubernetes 容器
在K8S中,同⼀个Pod的不同容器互相可以访问是怎么做到的?
在K8S中,同⼀个Pod的不同容器互相可以访问是怎么做到的?
|
5月前
|
存储 Kubernetes 数据中心
在K8S中,同⼀个Pod内不同容器哪些资源是共用的,哪些资源是隔离的?
在K8S中,同⼀个Pod内不同容器哪些资源是共用的,哪些资源是隔离的?
|
5月前
|
Kubernetes 前端开发 API
在K8S中,在容器内如何获取pod和namespace名字?
在K8S中,在容器内如何获取pod和namespace名字?