Docker的僵尸进程问题
共 2083字,需浏览 5分钟
·
2021-03-04 11:36
最近因为是招聘旺季,我自己作的产品<橙子简历>[1]的用户量增长很大,从而暴露出了不少的问题。这次记录一下昨天刚解决的一个问题: Docker容器中的僵尸进程问题。
背景
首先说一下引起问题的程序组件: puppeteer。这个组件主要是用来渲染简历的,puppeteer其实只是一个node library,真正在后台工作的其实就是chrome浏览器。那么在调用puppeteer执行任务的过程中,它会生成一个chrome的子进程。而我在几个月之前将橙子简历整体迁移到了Docker环境中,之后就经常收到类似"Resource temporarily unavailable"的报错。因为报错不是很频繁,我又忙于其他工作,所以对这个问题没有仔细研究,只是去服务器上查看了一下内存占用情况,发现是有可用内存的。最近因为用户量的上涨,这个问题日益严重,所以昨天就花了一些时间研究了一下。
研究
首先,我用ps命令查看了一下机器上的进程情况,发现有大量的chrome[defunct]
进程。而且通过我进一步测试,每当我渲染一次简历,这种进程就会增长,而且不会减少。所以我觉得初步就可以认为是这些"僵尸进程"占用了系统资源,从而不能再生成子进程。
僵尸进程
关于僵尸进程,我这里只做一下简单的介绍,更多细节可以参考[Wikipedia[2]] 。
在Linux(包括其他Unix系统,以下只用Linux代指)中,所有进程是一个树状结构,每个进程都会有一个父进程(最顶层的那个进程除外)以及多个子进程。这里最顶层的那个进程是比较特殊的,它有一个专属的名称:init进程[3]。init进程的PID总是为1。在现代的Linux环境中,这个init进程一般会是systemd或者upstartd等重量级的进程,它们会做很多额外的工作。
Linux对进程的管理有一个机制,当一个进程调用exit
退出的的时候,首先它的内存和其他系统资源会被操作系统自动回收。但是有一项资源不会被自动回收:进程的PID。Linux内核中有一张表(process table)存储所有进程相关的信息,而为了使父进程可以得到子进程的退出状态,只有当父进程调用了wait
来读取子进程的退出状态之后,才会在process table中删除已退出进程的PID,这个PID才可以被重新使用。而上面提到的init进程就会有一项重要的工作:调用wait
,从而保证不会有僵尸进城产生。
所以到这里,大家应该可以猜到,之前的"Resource temporarily unavailable"的问题就是僵尸进程占满了process table引起的。我通过docker exec命令进入容器中查看了一下进程情况,发现PID为1的进程是node,它不具备回收僵尸进程的功能,所以会不断的产生僵尸进程。而Linux中默认最大的PID是32768,所以僵尸进程虽然不会占用内存等其他资源,但对PID的占用会导致系统不能spawn新的子进程。这个问题是非常严重的。
修复
接下来要修复这个问题,我在网上搜了一下资料,发现一篇不错的文章[4]。这个问题的核心原因就是在docker环境中不存在"正常的init进程"(因为docker的理念是一个容器只运行一个worker进程)。那么解决问题的方式也很直接:给docker容器加一个init进程。这个init进程只需要做好signal的处理以及僵尸进程的回收就好了,所以我们不需要给它加一个重量级的systemd的系统。文章中的解决方案是自己写了一个轻量级的init进程,文章在最后也写到了,希望Docker官方可以解决这个问题。很幸运的是,现在的Docker已经在官方层面解决了这个问题,只需要在docker run
中加入--init
参数即可,参考docker run init[5]。
当然,如果你像我一样使用docker-compose来运行docker,那么在docker-compose.yml文件中
加入init: true
参数即可,参考compose-file init[6]
参考资料
[1]<橙子简历>: https://wonderfulcv.com
[2][Wikipedia: https://en.wikipedia.org/wiki/Zombie_process
[3]init进程: https://en.wikipedia.org/wiki/Init
[4]文章: https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/
[5]这里: https://docs.docker.com/engine/reference/run/#specify-an-init-process
[6]这里: https://docs.docker.com/compose/compose-file/compose-file-v3/#init