You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Node.js was not designed to run as PID 1 which leads to unexpected behaviour when running inside of Docker. For example, a Node.js process running as PID 1 will not respond to SIGINT (CTRL-C) and similar signals. As of Docker 1.13, you can use the --init flag to wrap your Node.js process with a lightweight init system that properly handles running as PID 1.
docker run -it --init node
You can also include Tini directly in your Dockerfile, ensuring your process is always started with an init wrapper.
Tini
现在让我们通过 Tini 来学习了一下收尸技术,可通过下面的方式让 Tini 去代理运行 Node 程序,使得 Node 成为 Tini 的子进程。
# Add Tini
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
# Run your program under Tini
CMD ["/your/program", "-and", "-its", "arguments"]
# or docker run your-image /your/program ...
通过仔细阅读 Tini 的代码,我判断核心的收尸技术就是这个 waitpid 函数 了,其实在僵尸进程的定义中就有了如何收尸,所以先了解基础概念是非常重要的。
intreap_zombies(constpid_tchild_pid, int*constchild_exitcode_ptr) {
pid_tcurrent_pid;
intcurrent_status;
while (1) {
current_pid=waitpid(-1, ¤t_status, WNOHANG);
switch (current_pid) {
case-1:
if (errno==ECHILD) {
PRINT_TRACE("No child to wait");
break;
}
PRINT_FATAL("Error while waiting for pids: '%s'", strerror(errno));
return1;
case0:
PRINT_TRACE("No child to reap");
break;
default:
/* A child was reaped. Check whether it's the main one. If it is, then * set the exit_code, which will cause us to exit once we've reaped everyone else. */PRINT_DEBUG("Reaped child with pid: '%i'", current_pid);
if (current_pid==child_pid) {
// ...
} elseif (warn_on_reap>0) {
PRINT_WARNING("Reaped zombie process with pid=%i", current_pid);
}
// Check if other childs have been reaped.continue;
}
/* If we make it here, that's because we did not continue in the switch case. */break;
}
return0;
}
背景
收到告警通知,⚠️ 容器线程数异常(PID上限为15K,超过15K则无法新建进程)⚠️ 。该服务会定时通过 puppeteer 进行一些页面性能收集的任务,为什么残留了这么多进程没有正常退出?
进入终端调试后,发现了大量的 chrome defunct processes 🧟♀️🧟♂️ 僵尸进程。于是尝试在 puppeteer issue Zombie Process problem. #1825 中找一找答案。
尝试解决
按照 puppeteer issue 中的建议,在 browser.close() 后,新增了 ps.kill 去杀死可能会残留的相关进程。
然后又过了几天,又收到了告警通知,即本次并未解决该问题。最后又通过运行 puppeteer 时加上 --single-process 参数和定时调用 kill -9 [pid] 去杀死僵尸进程等方法都以失败告终 ❌ 。
僵尸进程
正当大家困惑的时候,同学 a 发来了一篇文章 一次 Docker 容器内大量僵尸进程排查分析,文章中进行了详细的科普,此时才真正认识了僵尸进程。
到这里给我的体会是,如果遇见了一筹莫展的问题,不妨先仔细了解一下该问题的定义与介绍。它的基础概念是什么?造成的本质原因是什么?了解完前因后果后或许能够事半功倍。
通俗的来讲,就像下面的程序一样。当子进程调用 exit 函数退出了,但是父进程没有给它收尸,于是它变成了杀不死的🧟♀️🧟♂️ ,因为它早就已经死了,现在只是在进程列表中占了一个坑位而已。
当该僵尸进程的父进程退出后,它就会被托管到 PID 为 1 的进程上面,通常 PID 为 1 的进程会扮演收尸的角色。
但是当 Node.js 为 PID 1 的进程时,不会进行收尸,从而导致了大量的僵尸进程的问题。
解决办法
当 Docker 中第一个运行的程序为 node xxx.js 时 Node 就成为了 PID 为 1 的进程,所以说问题的解决办法可以是让有能力收尸的进程为第一个运行的程序。
在 Docker and Node.js Best Practices 中官方也给出了解决方案
Tini
现在让我们通过 Tini 来学习了一下收尸技术,可通过下面的方式让 Tini 去代理运行 Node 程序,使得 Node 成为 Tini 的子进程。
通过仔细阅读 Tini 的代码,我判断核心的收尸技术就是这个 waitpid 函数 了,其实在僵尸进程的定义中就有了如何收尸,所以先了解基础概念是非常重要的。
当然 Tini 作为父进程还有其他的优点,比如
复现与定案
当我们学到核心的收尸技术后,就可以来揭发完整的案发现场了 ~
1. Docker 运行 node xxx.js
➜ ~ docker run -t -i -v /test/tini:/test 97f7595bf6c4 node /test/main.js
Tini 是一个 C 程序,这里先把 Tini 核心实现的代码复制过来,接着用 Node.js C++ 插件的方式来调用 C 这部分的代码
我们的 main.js 程序对外暴露了两个接口,来完成本次实验
2. Node 程序的 PID 会是 1
✅ 可见 Node 成为了 PID 为 1 的进程
3. 制造一个僵尸
✅ 子进程调用 exit 退出,父进程不收尸,使其顺利成为一具僵尸
产生僵尸的代码为
✅ 杀死僵尸进程的父进程,它就被 PID 为 1 的进程托管
4. 收尸
✅ kill -9 杀不死僵尸进程, 符合预期
✅ 使用 waitpid 函数去收尸,僵尸进程消失
真正收尸的代码为下面,并且通过 Node-api 把本次收尸进程的 id 和 status 返回给了 js 调用方。
可见通过我们逐步的复盘,一切也都验证了我们最初的猜想。
小结
其实僵尸进程的产生也是 puppeteer 程序的一个 bug, Node.js 不去处理也是情理之中,因为很难判断用户真正的行为,况且还要写一堆副作用的代码。
最后我们通过 docker --init 使用一个 init 进程去解决,这样进程间互相解藕,各司其职显得优雅一点。这也算践行了sidecar 模式吧 ~
The text was updated successfully, but these errors were encountered: