一次 Docker 容器内大量僵尸进程排查分析
前段时间线上的一个使用 Google Puppeteer 生成图片的服务炸了,每个 docker容器内都有几千个孤儿僵死进程没有回收,如下图所示。
这篇文章比较长,主要就讲了下面这几个问题。
- 什么情况下会出现僵尸进程、孤儿进程
- Puppeteer 工作过程启动的进程与线上事故分析
- PID 为 1 的进程有什么特殊的地方
- 为什么 node/npm 不应该作为镜像中 PID 为 1 的进程
- 为什么 Bash 可以作为 PID 为 1 的进程,以及它做 PID 为 1 的进程有什么缺陷
- 镜像中比较推荐的 init 进程的做法是什么
Puppeteer 是一个 node 库,是 Chrome 官方提供的无界面 chrome 工具(headless chrome),它提供了操作ChromeAPI 的方式,允许开发者在程序中启动 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 leadstounexpected behaviour when running inside ofDocker."。下图来自github.com/nodejs/dock… 。
接下来会做两个实验:第一个实验是在 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, 执行 dockerrunab71925b5154,启动 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,后面有一个小兄弟为了统一启动命令将这个命令改为npmrun 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() 调用后会产生多少新进程?
推荐系统
雨林木风 winxp下载 纯净版 永久激活 winxp ghost系统 sp3 系统下载
系统大小:0MB系统类型:WinXP雨林木风在系统方面技术积累雄厚深耕多年,打造了国内重装系统行业知名品牌,雨林木风WindowsXP其系统口碑得到许多人认可,积累了广大的用户群体,是一款稳定流畅的系统,雨林木风 winxp下载 纯净版 永久激活 winxp ghost系统 sp3 系统下载,有需要的朋友速度下载吧。
系统等级:进入下载 >萝卜家园win7纯净版 ghost系统下载 x64 联想电脑专用
系统大小:0MB系统类型:Win7萝卜家园win7纯净版是款非常纯净的win7系统,此版本优化更新了大量的驱动,帮助用户们进行舒适的使用,更加的适合家庭办公的使用,方便用户,有需要的用户们快来下载安装吧。
系统等级:进入下载 >雨林木风xp系统 xp系统纯净版 winXP ghost xp sp3 纯净版系统下载
系统大小:1.01GB系统类型:WinXP雨林木风xp系统 xp系统纯净版 winXP ghost xp sp3 纯净版系统下载,雨林木风WinXP系统技术积累雄厚深耕多年,采用了新的系统功能和硬件驱动,可以更好的发挥系统的性能,优化了系统、驱动对硬件的加速,加固了系统安全策略,运行环境安全可靠稳定。
系统等级:进入下载 >萝卜家园win10企业版 免激活密钥 激活工具 V2023 X64位系统下载
系统大小:0MB系统类型:Win10萝卜家园在系统方面技术积累雄厚深耕多年,打造了国内重装系统行业的萝卜家园品牌,(win10企业版,win10 ghost,win10镜像),萝卜家园win10企业版 免激活密钥 激活工具 ghost镜像 X64位系统下载,其系统口碑得到许多人认可,积累了广大的用户群体,萝卜家园win10纯净版是一款稳定流畅的系统,一直以来都以用户为中心,是由萝卜家园win10团队推出的萝卜家园
系统等级:进入下载 >萝卜家园windows10游戏版 win10游戏专业版 V2023 X64位系统下载
系统大小:0MB系统类型:Win10萝卜家园windows10游戏版 win10游戏专业版 ghost X64位 系统下载,萝卜家园在系统方面技术积累雄厚深耕多年,打造了国内重装系统行业的萝卜家园品牌,其系统口碑得到许多人认可,积累了广大的用户群体,萝卜家园win10纯净版是一款稳定流畅的系统,一直以来都以用户为中心,是由萝卜家园win10团队推出的萝卜家园win10国内镜像版,基于国内用户的习惯,做
系统等级:进入下载 >windows11下载 萝卜家园win11专业版 X64位 V2023官网下载
系统大小:0MB系统类型:Win11萝卜家园在系统方面技术积累雄厚深耕多年,windows11下载 萝卜家园win11专业版 X64位 官网正式版可以更好的发挥系统的性能,优化了系统、驱动对硬件的加速,使得软件在WINDOWS11系统中运行得更加流畅,加固了系统安全策略,WINDOWS11系统在家用办公上跑分表现都是非常优秀,完美的兼容各种硬件和软件,运行环境安全可靠稳定。
系统等级:进入下载 >
相关文章
- 如何解决Windows 11系统中出现的蓝屏错误代码0x0000005问题
- 笔记本电脑一直有滋滋的响声是为什么(笔记本电脑滋滋响解决方法)
- 如何解决锐龙2200g死机蓝屏
- Win8.1本地搜索为什么无法使用
- Win8.1无线网络不稳定/掉线怎么办
- 电脑机箱漏电怎么消除?电脑机箱漏电是哪里的问题?
- 电脑开不了机怎么办?电脑无法开机怎么解决?
- 硬盘双击无法打开的问题该怎么办
- 风行下载速度慢甚至是为0怎么办?风行播放器下载问题及解决方法汇总
- 苹果回应新的iOS恶意软件YiSpector:已在iOS8.4中解决该问题
- 没有路由器怎么连无线 160wifi 解决没有路由器连接无线问题
- 维棠FLV下载视频失败问题汇总及解决方法
- Word2016 出现“此功能看似已中断 并需要修复”问题解决方案(图文)
- Cisco管理的35个常见问题及解答
热门系统
推荐软件
推荐应用
推荐游戏
热门文章
常用系统
- 1番茄花园Windows7 64位 装机旗舰版
- 2Windows11最新纯净版下载 萝卜家园x64位极简版 永久免费 联想笔记本专用下载
- 3番茄花园 Windows10 22H2 64位 低占用专业精简版
- 4深度技术win10专业版激活密钥系统21H2 X64位 V2023下载
- 5windows7旗舰版下载 深度技术最新高级版 ghost镜像 自动激活下载
- 6【家庭/个人】Windows11 23H2 64位 中文家庭版
- 7系统之家win7企业版纯净ghost系统 V2023镜像下载
- 8青苹果系统win7游戏版 激活密钥 官网镜像下载 GHOST v2023
- 9萝卜家园WIN11娱乐版ghost系统 ISO镜像 X64位 V2023下载