目录
发现现象
我们的爬虫服务会使用chromedp库(https://github.com/chromedp/chromedp)模拟浏览器登录,抓取网页数据,某天在pod内查看服务运行状态时,发现有大量的zombie进程,看了下是Chrome进程,在这里记录下排查以及解决的过程,希望可以给其他遇到相同问题的人提供一些思路。
爬虫服务使用Chrome,应该是以创建子进程的方式来启动Chrome,如果在子进程销毁时没有wait或者waitpid来处理,那么子进程会成为zombie进程。
每个zombie进程会占用系统少量资源,造成资源泄露,最明显的是pid数量会逐渐变多,最终结果会造成无法启动新的进程。
如何解决?
临时解决
发现问题首先想的是快速恢复,而不是去定位解决问题。一开始准备将所有的僵尸进程kill掉,但是发现僵尸进程很难被kill掉,kill -9也不行。
后考虑kill掉其父进程,然后所有的僵尸进程就可以挂到init进程上,init会清理僵尸进程,然后看了下发现容器内没有init进程,所有僵尸进程的父进程都是1,进程1对应的是爬虫服务,杀掉1进程会导致容器挂掉重启……
后面直接重启服务临时解决了。
根本上解决
考虑到僵尸进程的产生原因,首先想到的是对于chromedp库的使用有问题,可能没有正确的关闭浏览器,去看了下代码内的使用,以及官方示例的代码,发现没有问题,所有的资源都被关闭了……
然后去chromedp库的issue中查找相关的内容,可能会有人遇到相同的问题。
总结了几个观点:
- 关闭浏览器时,kill掉chrome进程,但是这样太暴力,可能会影响其他使用chrome的线程。
- 一些Chrome在被启动的时候,会有一个wrapper,例如shell,shell会启动Chrome作为子进程,执行完后,shell退出了,Chrome成为孤儿进程,挂到pid1进程上,但是容器内pid为1的进程为业务进程,没有回收僵尸进程的能力。
看了下chrome运行时的状况,发现开始的父进程并不是1,但到最后都变成了1,并且变为defunct状态。
在上面的issue中,有人提供了一个pr,并且被merge了,所以考虑升级库版本来解决,但是发现升级没有解决……
考虑其他方案。
后来发现一种方案,在docker运行时加入--init参数,这样docker内1号进程就是docker-init进程,业务进程则是其子进程。
docker-init进程会将收到的信号传递给其子进程,并且会处理僵尸进程。
然后自己在本地实验了下,同样的使用chromedp库,发现拥有docker-init进程的容器内,没有产生僵尸进程,因为1号进程已经具备处理僵尸进程的能力。
而1号进程是业务进程的容器内,僵尸进程逐渐积累……
同时实验了下信号捕获,看业务进程能否捕获退出信号,实验也是ok的。
ps:
通过本地实验,发现bash作为1号进程、业务进程作为其父进程的情况下,也可以处理僵尸进程,但是却不能传递退出信号给业务进程。
所以对于Linux 来说,pid 为 1 的进程,有着特殊的使命:
- 传递退出信号,确保子进程正确退出
- wait子进程退出,回收僵尸进程
修复过程
本地测试时在docker run参数中加–init,经测试发现是ok的,但是服务是部署在k8s集群中,查了好久发现无法在deployment yaml文件中传递docker run的参数…
只能从镜像层面考虑解决。
首先打了一个有tini(docker-init实际使用的是tini)的镜像,然后修改Dockerfile:
最终上线完美解决!
僵尸进程已经消失不见。