前不久网上的一个应用 Google Puppeteer 转化成图片的服务项目炸了,每一个 docker 器皿内都有好几千个孤儿僵死进程沒有回收利用,如下图所示。
这篇文章较为长,关键就讲了下边这一些问题。
- 什么情况会发生丧尸进程、孤儿进程
- Puppeteer 工作中全过程运行的进程与网上安全事故剖析
- PID 为 1 的进程有哪些独特的地区
- 为何 node/npm 不应该做为镜像文件中 PID 为 1 的进程
- 为何 Bash 可以做为 PID 为 1 的进程,及其它做 PID 为 1 的进程有哪些缺点
- 镜像文件中较为强烈推荐的 init 进程的制作方法是啥
Puppeteer 是一个 node 库,是 Chrome 官方网给予的无页面 chrome 专用工具(headless chrome),它保证了实际操作 Chrome API 的方法,容许开发人员在系统中运行 chrome 进程,启用 JS 的 API 完成页面加载、数据爬取、web 功能测试等作用。
本实例中采用的情景是应用 Puppeteer 载入 html,接着截屏转化成一张分销商宣传海报的图片。文章内容研究了这个问题身后的缘故,下面逐渐宣布的內容。
进程
每一个进程都有一个唯一的标志,称之为 pid,pid 是一个非负的整数金额值,应用 ps 指令可以查询,在我的 Mac 电脑实行 ps -ef 能够看见现阶段运作的全部进程,如下所示所显示。
在其中 PID 是表明进程号。
系统软件中每一个进程都有相匹配的父进程,上边 ps 輸出中的 PPID 就表明进程的父进程号。最高层的进程的 PID 为 1,PPID 为 0。
开启 iTerm,在终端设备中运行一个指令,例如 "ls",事实上系统软件会建立新的 iTerm 子进程,这一 iTerm 进程又建立了 zsh 子进程。在 zsh 中键入的 ls 指令,则是 zsh 进程又运行了一个 ls 子进程。在 iTerm 中键入 ls 指令全过程的进程关联如下所示所显示。
进程与 fork
前边提及的父进程“建立”子进程,更严格的表述是 fork(卵化、衍化)。下边看来一个真实的事例,新创建一个 fork_demo.c 文档。
实行上的编码,会輸出如下所示的句子。
能够看见 if、else 句子都强制执行了。
fork 启用
fork 是一个系统进程,它的方式申明如下所示所显示。
fork 启用进行后会形成一个新的子进程,且父子俩进程都从 fork 回到处执行。这儿必须注意的是 fork 的传参的含意,在父进程和新的子进程中,他们的含意不一样。
- 在父进程中 fork 的传参是创好的子进程 id
- 在构建的子进程中 fork 的传参自始至终相当于 0
因而可以根据 fork 的传参区别父子俩进程,在运转环节中可以应用 getpid 方式获得当下的进程 id。fork 典型性的应用方法以下所显示。
实行里面的代码,輸出結果如下所示所示。
子进程是父进程的团本,子进程有着父进程数据信息室内空间、堆、栈的拷贝团本 ,fork 选用了 copy-on-write 技术性,fork 实际操作几乎一瞬间可以进行。仅有在子进程改动了相对的地区才会开展真实的复制。
孤儿进程:不可以同年同月同日生,也不会同年同月同日死
下面问一个问题,父进程挂掉时,子进程会挂掉吗?
想像实际中的情景,爸爸没有了,孩子还能够活吗?回答是毫无疑问的。相匹配于进程,父进程撤出时,子进程会再次运作,不容易一起同往冥府。
一个父进程早已停止的进程被称作孤儿进程(orphan process)。电脑操作系统这一大家长是非常个性化的,没人管的孤儿进程会被进程 ID 为 1 的进程接手。这一 PID 为 1 的进程后边还会继续再讲到。
下面对以前的代码稍加改动,让父进程 fork 子进程之后自尽撤出,转化成孤儿进程。代码如下所示所示。
编译程序运作上边的代码
輸出結果如下所示。
能够看见父进程 id 为 21629, 转化成的子进程 id 为 21630。
应用 ps 查询现阶段进程信息内容,結果如下所示所示。
能够看见这时孤儿子进程 21630 的父 ID 早已变成了高层的 ID 为 1 的进程。
丧尸进程
父进程承担生,如果不负责养,那么就并不是一个好爸爸。子进程挂掉,假如父进程不给子进程“收尸”(启用 wait/waitpid),那这一子进程小可怜就变成了丧尸进程。
新创建一个 make_zombie.c 文档,內容如下所示。
编译程序运作上边的代码,就可以转化成一个进程号为 22538 的丧尸进程,如下所示所示。
CMD 名中的 defunct 表明这是一个丧尸进程。
也应用 ps 指令查询进程的情况,表明为 "Z" 或是 "Z " 表明这是一个丧尸进程,如下所示所示。
子进程撤出后绝大多数資源早已被释放出来可供别的进应用,可是核心的进程表格中的槽位沒有释放出来。
丧尸进程有一个很奇妙的特点,应用 kill -9 必杀技数据信号都没有办法杀死丧尸进程,那样的设计方案利与弊各半,好的地点是父进程可以一直还有机会实行 wait/waitpid 等指令收种子进程,坏的地点是没法强制性回收利用这类丧尸进程。
PID 为 1 的进程
Linux 中核心复位之后会运行系统软件的第一个进程,PID 为 1,还可以称作 init 进程或是根(ROOT)进程。在我的 Centos 设备上,这一 init 进程是 systemd,如下所示所示。
在我的 Mac 电脑,这一进程为 launchd,如下所示所示。
init 进程有下边这好多个作用
- 假如一个进程的父进程撤出了,那麼这一 init 进程便会接手这一遗孤进程。
- 假如一个进程的父进程未实行 wait/waitpid 就撤出了,init 进程会接管子进程并全自动启用 wait 方式,进而确保系统中的丧尸进程可以被清除。
- 传送数据信号给子进程,这一点后边会详细介绍。
为何 Node.js 不适合做 Docker 镜像文件中 PID 为 1 的进程
在 Node.js 的官方网最佳实践里有提到 "Node.js was not designed to run as PID 1 which leads to unexpected behaviour when running inside of Docker."。下面的图来源于 github.com/nodejs/dock… 。
下面会做2个试验:第一个实验是在 Centos 设备上,第二个试验是在 Docker 镜像文件中
试验一:在 Centos 上,systemd 做为 PID 为 1 的进程
下边来做一些检测,改动上边的编码,将父进程 sleep 的時间裁短为 15s,新创建一个 make_zombie.c 文档,如下所示所示。
编译程序转化成可执行程序 make_zombie。
随后创建一个 run.js 编码,内部结构运行一个进程运作 make_zombie,如下所示所示。
实行 node run.js 运作这一段 js 编码,应用 ps -ef 查询进程关联如下所示。
过 15s 之后,再度实行 ps -ef 查看现阶段运作的进程,能够看见 make_zombie 有关进程都不见了。
这是由于 PID 为 29519 的 make_zombie 父进程在 15s 之后撤出,丧尸子进程被代管到 init 进程,这一进程会启用 wait/waitfor 为这一丧尸收尸。
试验二:在 Docker 上,node 做为 PID 为 1 的进程
将 make_zombie 可执行程序和 run.js 装包为 .tar.gz 包,接着新创建一个 Dockerfile,內容如下所示。
实行 docker build 指令搭建一个镜像文件,在我的电脑上 Image ID 为 ab71925b5154, 实行 docker run ab71925b5154,运行 docker 镜像文件,应用 docker ps 寻找镜像文件 CONTAINER ID,这儿为 e37f7e3c2e39。随后应用 docker exec 进到到镜像文件终端设备
实行 ps 指令查询当下的过程情况,如下所示所显示。
等一段时间(15s),再度实行 ps 查询现阶段过程,如下所示所显示。
能够看见 PID 为 13 的僵尸进程早已代管到 PID 为 1 的 node 过程,可是并没有被回收利用。
这也是 node 不适合做 init 过程的最关键缘故:没法回收利用僵尸进程。
说到 node,这儿提一下 npm,npm 事实上是应用 npm 过程运行了一个子过程运行了 package.json 中 scripts 里写的启动脚本制作,实例 package.json 脚本制作如下所示所显示。
应用 npm run start 运行,获得的过程如下所示所显示。
与 node 一样,npm 也不会解决丧尸子过程回收利用。
网上问题分析
大家网上出问题的情形下应用 npm start 来运行一个 Puppeteer 新项目,每转化成一次照片便会建立 4 个 chrome 有关的过程,如下所示所显示。
在图片生成过去进行时,chrome 主过程撤出,剩余的三个遗孤僵尸进程被代管到高层 npm 过程下,可是 npm 过程乏力回收利用,全部每转化成一次照片便会新增加三个僵尸进程。在许多次图片生成之后,系统软件中就充满了僵尸进程。
解决方案
为了更好地彻底解决这个问题,不可以让 node/npm 变成 init 过程,让有工作能力接手僵尸进程的服务项目变成 init 过程就可以,有两个解决方案。
- 应用 bash 运行 node 或是 npm
- 提升专业的 init 过程,例如 tini
处理方法一:应用 bash 运行 node
让 bash 变成高层过程是非常快的一种方法,bash 过程会承担回收利用僵尸进程,改动 Dockerfile,如下所示所显示。
应用这类形式是非常简单,并且之战地上沒有出问题恰好是由于一开始是应用这类 bash 方法运行 node,后边有一个小兄弟为了更好地统一启动命令将这一指令改成 npm run start,问题才发生的。
但应用 bash 并不是很好的计划方案,它有一个严重的问题,bash 不容易传递信号给它运行的进程,雅致关机等作用没法完成。
下面做一个试验,认证 bash 不容易传递信号给子进程的观点,新创建一个 signal_test.c 文档,它解决 SIGQUIT、SIGTERM、SIGTERM 三个信号,內容如下所示。
在我 Centos 和 Mac 上运作这一 signal_test 程序流程时,推送 kill -2、-3、-15 给这一程序流程,都是会有相应的输出打印,表明收到了信号。如下所示所显示。
在 Docker 镜像文件中应用 bash 运行这一程序流程时,推送 kill 指令给 bash 之后,bash 并不会将信号传递给 signal_test 程序流程。在实行 docker stop 之后,docker 会推送 SIGTERM(15) 信号给 bash,bash 并不会将这一信号传递给运行的应用软件,只有等一段时间请求超时,docker 会推送 kill -9 强制性杀掉这一 docker 进程,没法做到雅致关机的作用。
因此有接下来的第二种解决方法。
处理方法二:应用专业的 init 进程
Node.js 给予了这两种计划方案,第一种是应用 docker 官方网的轻量 init 系统软件,如下所示所显示。
这类运行方法会以 /sbin/docker-init 为 PID 为 1 的 init 进程,不容易把 Dockerfile 中 CMD 做为第一个运行进程。
以下边的 Dockerfile 內容为例子
实行 docker run -it --init image_id 运行 docker 镜像文件,这时镜像文件内的进程如下所示所显示。
能够看见 signal_test 程序流程做为 docker-init 的子进程运行了。
在 docker stop 指令推送 SIGTERM 信号给镜像文件之后,docker-init 进程会将这一信号转至 signal_test,这一运用进程就可以接到 SIGTERM 信号做自定的解决,例如雅致关机等。
除开 docker 的官方网计划方案,Node.js 的最佳实践还介绍了一个 tini 那样一个 C 语言表达写的很小的 init 进程,github.com/krallin/tin… 。它的编码较短,很非常值得一读,对了解信号传递、解决丧尸进程十分有协助。
总结
根据这篇文章,期待你能弄懂丧尸进程、遗孤进程、PID 为 1 的进程是啥,及其为何 node/npm 不适合做 PID 为 1 的进程,bash 做为 PID 为 1 的进程有哪些缺点。
下边留一个复习题,考考你对进程 fork 函数公式的了解。如下所示程序流程持续启用三次 fork() 调用后会造成是多少新进程?