Docker进程异常终止导致的僵尸进程处理思路

一、问题描述

某业务主机(云主机),其上部署Docker,用Docker实例部署某业务系统,文件映射到云主机本地;docker实例中使用sh脚本启动程序,一线反映,某次业务异常,采用了直接杀死进程,过程中Docker进程先死了,导致了docker内程序编程僵尸进程,top命令查看有4个。进程显示:[进程名],出现defunct标志;
在这里插入图片描述
在这里插入图片描述

僵尸进程(defunct/zombie process):在类UNIX系统中,僵尸进程是指完成执行(通过exit系统调用,或运行时发生致命错误或收到终止信号所致),但在操作系统的进程表中仍然存在其进程控制块(PCB),处于"终止状态"的进程。僵尸进程不能被杀死,因为它们已经死亡,只等待它们的父进程回收它们。随后,如果没有父进程回收,或它的父进程先死了,它会被一个pid为1的父进程接管,这时这个个进程又叫做孤儿进程((orphan process))。在类UNIX操作系统中,为避免孤儿进程退出时无法释放所占用的资源而僵死,任何孤儿进程产生时都会立即被系统进程init或systemd自动接收为子进程,这一过程也被称为“收养”。但注意,虽然事实上该进程已有init作为其父进程,但由于创建该进程的进程已不存在,所以仍应称之为“孤儿进程”。而pid为1的init进程是特殊的:它不获得它不想处理的信号,因此它可以忽略SIGKILL信号(一般kill命令是发信号给进程,然后被进程捕获之后执行对应操作)。

二、问题分析:

1)僵尸进程查看

top  #查看僵死进程书目及父进程ppid
ps -T -p 18239    #查看下这个僵尸进程的主线程是否退出;
ps -A -ostat,ppid,pid,cmd | grep -e '^[Zz]'  #查看僵死进程及其父进程,状态为z即僵尸,l 多线程的,其中,-A 参数列出所有进程;-o 自定义输出字段,我们设定显示字段为stat(状态),ppid(父进程pid),pid(进程pid),cmd(命令行)
kill -HUP 15329   #发送一个挂起信号,等同于kill -1,对进程进行复位尝试,会重新加载配置文件
pstree -p pid  #查看进程的父子关系
pkill -9 pid   #杀死父进程尝试,等同于kill -9,是向进程发送结束命令,让进程调用exit方法来进行结束

在这里插入图片描述
在这里插入图片描述
进程五种状态:

  • 创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态
  • 就绪状态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行
  • 执行状态:进程处于就绪状态被调度后,进程进入执行状态
  • 阻塞状态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用
  • 终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行

在这里插入图片描述

进程状态说明回顾:

D 无法中断的休眠状态(通常 IO 的进程);
R 正在运行可中在队列中可过行的;
S 处于休眠状态;
T 停止或被追踪;
W 进入内存交换(从内核2.6开始无效);
X 死掉的进程(从来没见过);
Z 僵尸进程;
< 优先级高的进程
N 优先级较低的进程
L 有些页被锁进内存;
s 进程的领导者(在它之下有子进程);
l 多进程的(使用 CLONE_THREAD, 类似 NPTL pthreads);

2)Docker产生僵尸进程的原因:

因Docker环境一般使用最小化镜像启动,通常是没有安装systemd或者sysvint这类初始化系统的进程。而我们一般拉起/运行一个Docker实例,会跟一个初始启动命令,这个就是docker实例里所有多进程的父进程(它也是pid=1),一旦该进程异常,提前死了,这就导致docker其内其他进程变为僵尸进程,甚至会产生大量的僵尸进程,影响宿主系统的运行,被宿主机init/systemd接管。实践表明:docker容器在默认的参数配置下,其init进程并没有处理孤儿进程的能力。如下命令,docker容器创建后bash就是它的父进程。

docker run -itd --name ubuntu-test ubuntu /bin/bash
docker exec -it --name ubuntu-test ubuntu sh  #进入容器后执行:
ps -auf|grep 1 #可确认容器内父进程就是bash进程

在这里插入图片描述

相关经验表明,有两个解决办法可以让docker的init进程能够处理孤儿进程。

  1. 启动docker容器时,指定init进程为bash或让有能力接管僵尸进程的服务成为 init 进程,由bash进程对孤儿进程的资源进行回收。这也是官方推荐的;使用tiny的进程来负责容器内子进程"收养"后的处理工作。注意:使用bash进程作为init进程,它 不会传递信号给它启动的进程,优雅停机等功能无法实现。
  2. 增加专门的 init 进程,比如 tini,官方文档显示,Docker 1.13及以上版本已集成tini(C 语言写的),可直接启动容器时,加参数–init即可,更多参看:https://github.com/krallin/tini;

官方文档说明:

The container’s main process is responsible for managing all processes that it starts.

In some cases, the main process isn’t well-designed, and doesn’t handle “reaping” (stopping) child processes gracefully when the container exits.

If your process falls into this category, you can use the –init option when you run the container.

The --init flag inserts a tiny init-process into the container as the main process, and handles reaping of all processes when the container exits.

Handling such processes this way is superior to using a full-fledged init process such as sysvinit, upstart, or systemd to handle process lifecycle within your container.

使用 tini 启动容器示例1:

FROM nginx

RUN export TINI_VERSION=0.9.0 && \
    export TINI_SHA=fa23d1e20732501c3bb8eeeca423c89ac80ed452 && \
    curl -fsSL https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static -o /bin/tini && \
    echo 'Calculated checksum: '$(sha1sum /bin/tini) && \
    chmod +x /bin/tini && echo "$TINI_SHA  /bin/tini" | sha1sum -c 
    
ENTRYPOINT ["/bin/tini","--","/opt/nginx/docker-entrypoint.sh"]
ENTRYPOINT ["nginx", "-c"] 
CMD ["/etc/nginx/nginx.conf"]

注意:当我们运行一个Docker容器时,镜像的ENTRYPOINT就是你的根进程,即PID 1(如果你没有ENTRYPOINT,那么CMD就会作为根进程,你可能配置了一个shell脚本,或其他的可执行程序,容器的根进程会按我们启动容器第一个执行的命令来配置

3)僵尸进程处理

首先要明确,僵尸进程是已经处于死亡状态的,kill是没办法再次杀死这个进程的。如果当下僵尸进程的父进程id(ppid)已然是1(init/systemd),这种情况下绝大部分ppid=1的僵尸进程是暂时的,数量少的话,可以等待回收,通过ps结果中start列查看出现僵尸进程的时间,分析下可能导致僵死进程的原因。但也需要注意,PID 1是无法回收异常退出进程,异常退出的进程变成僵尸进程,继续占用系统资源。另外如果父进程是一个循环,不会结束,那么子进程也会一直保持僵尸状态。

“A child that terminates, but has not been waited for(当父进程没登到子进程退出状态就提前结束时) becomes a “zombie”. The kernel maintains a minimal set of information about the zombie process (PID, termination status, resource usage information) in order to allow the parent to later perform a wait to obtain information about the child.”

僵尸进程回收:它是子进程执行完后留下了一个需要父进程收尸的一个退出状态结构,如果他的父进程没安装 SIGCHLD 信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果父进程也结束了,这些子进程会自动过继给init进程进程定期的回收。

“僵尸进程”是特性:

  • 已经退出;
  • 没有被其父进程wait(wait是指syscall父进程用于检索其子进程的退出代码);
  • 父进程已丢失(也就是说,它们的父进程已经不存在了),这意味着他们永远不会被其父进程处理;成为“僵尸进程”被创建(也就是说,一旦它的父进程非正常退出了,它也就跟着无法正常退出了),继承成为PID 1的子级,最后由PID 1会负责关闭它,另pid 1的进程会忽略一些诸如SIGTERM不利的信号。

注意,Docker容器中,当我们执行docker run启动容器将Bash作为PID 1运行,那么后续发送到Docker容器的所有信号(例如,使用docker stop或docker kill)最终都会发送到Bash,Bash默认不会将它们转发到任何地方(除非手动编写代码实现)。换句话说,如果你使用Bash来运行某一应用,那么当你运行docker stop的时候,该应用将永远收不到停止信号!从而该应用进程会因丢失父进程变成“僵尸进程”,而我们使用Tini,可通过“信号转发”解决了这个问题,向Tini(内置显式信号处理程序,用来转发信号;Tini号称可做PID 1需要做的一切,而不做其他任何事情。)发送信号后,它也会向docker内的子进程发送同样的信号;

在 Docker 镜像中使用 bash 启动应用程序时,发送 kill 命令给 bash 以后,bash 并不会将信号传递给应用程序。在执行 docker stop 以后,docker 会发送 SIGTERM(15) 信号给bash,bash并不会将这个信号传递给启动的应用程序,只能等一段时间超时,docker 会发送 kill -9 强制杀死这个docker进程,无法达到优雅停机的功能。

killall、kill -15、kill -9命令,一般都不能杀掉 defunct进程,甚至会多出更多的僵尸进程;子进程死后, 会发送SIGCHLD信号给父进程,父进程收到此信号后,执行 waitpid()函数为子进程收尸。就算父进程没有调用wait,内核也会向它发送SIGCHLD消息,默认处理是忽略, 如果想响应这个消息,可以设置一个处理函数。父进程死后,僵尸进程作为"孤儿进程",会过继给1号进程init,init始终会负责清理僵尸进程,关机或重启后所有僵尸进程都会消失。

更多参看:https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/
代码示例:https://juejin.cn/post/6844904036634722318

三、附录:僵尸进程 vs 孤儿进程

● 僵尸进程(zombie):当一个子进程结束运行(一般是调用exit、运行时发生致命错误或收到终止信号所导致)时,子进程的退出状态(返回值)会回报给操作系统,系统则以SIGCHLD信号将子进程被结束的事件告知父进程,此时子进程的进程控制块(PCB)仍驻留在内存中。一般来说,收到SIGCHLD后,父进程会使用wait系统调用以获取子进程的退出状态,然后内核就可以从内存中释放已结束的子进程的PCB;而如若父进程没有这么做的话(init不会处理异常退出的子进程),子进程的PCB就会一直驻留在内存中,也即成为僵尸进程;僵尸进程会占用原先的资源不会释放;

● 孤儿进程(orphan):是指当一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作,最终由init负责按正常回收,结束其生命,孤儿进程一般没有什么危害。

在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/ximenjianxue/article/details/128909526