Docker 中的 PID 1 和 tini:为什么你的容器不响应 Ctrl-C
之前我写过一篇文,讲我是怎么处置 Docker 容器产生的僵尸进程的,正巧前两天上网乱刷,看到有个人也被容器中的僵尸进程困扰,有一条回复提到了一个关键词 tini
,说能根治这个问题,于是继续上网冲浪,翻到了 Medium 上的这篇文章,感觉很有用,所以翻译出来。
以下内容除特别注明外,皆翻译自原文。我亦不对内容做任何的担保,并不对任何可能产生的后果(包括但不限于文件丢失)负责。
在使用 Docker 的时候,你有可能会遇到这么一种很难受的情况,就是你敲了 Ctrl-C 想停掉这个容器,但这个容器却无动于衷。或者又可能你的容器停止了,但留下了一堆僵尸进程。这些问题通常来自于一个开发者们从一开始就没想明白的问题 —— 如果你的程序成为了容器中的 PID 1 会怎么样。
什么是 PID 1
在 Linux 系统中,PID 1(进程号 1)是在系统启动过程中第一个启动的进程。它扮演着一个特殊的角色,即系统的初始化者。它将负责启动和管理所有其他的进程。在 Docker 容器中,你启动的第一个进程将默认成为 PID 1。
例如:
1 | docker run -it node:18 node |
这时候,Node.js 的进程就是 PID 1。
为什么 PID 1 很特别?
PID 1 的进程在 Linux 中会有如下几种特殊的行为:
- 响应信号的方式不同
大多数 UNIX 进程会自动接收并处理类似SIGINT
(来自 Ctrl-C)和SIGTERM
(来自docker stop
命令)的信号。
但是 PID 1 的进程默认不会接收这些信号,除非它们主动监听。 - 收割僵尸进程
如果 PID 1 不等待子进程,那么这些子进程就会变成僵尸。尽管它们已经退出了,但仍然会消耗系统资源。(译者注:我理解就是在容器停止的时候,主进程不等待它的子进程全部退出成功再退出,而是就自己拍拍屁股走人了)
这会慢慢地拖慢这个容器的性能,或让这个容器的行为变得失控。
问题示例
假设我们有这样一个简单的 Node.js 应用:
1 | // app.js |
这个应用会运行在一个 Docker 容器中:
1 | docker run -it node:18 node app.js |
如果此时你尝试使用 Ctrl-C 退出,那么什么都不会发生。很奇怪对吧?因为:
- Node.js 在容器中是 PID 1 进程
- 当它是 PID 1 的时候,它没有正确转发或监听
SIGINT
信号
最终,你可能会一边纳闷为啥 Ctrl-C 不好使,一边反复敲它。这时候就是 tini
出场的时候了。
tini:轻量的 init
tini
是个简化的初始化(init)系统,体积只有几 KB,并且专为容器环境设计。Docker 甚至集成了它,你只需要用 --init
参数就能开启。
tini 负责干什么?
- 信号转发
它会监听类似SIGINT
和SIGTERM
之类的信号,并正确地将其转发到你的应用程序。这样一来,Ctrl-C 或docker stop
就会按照预期工作了。 - 收割僵尸进程
tini
会收割死亡的子进程,这样它们就不会变成僵尸了。 - 就像一个负责任的 PID 1 一样干活
它工作起来就像一个真正的 Linux 初始化系统,只是更小了点。
怎么用 tini?
方法 1: 使用 Docker 内置的功能
Docker 在引擎内部已经集成了 tini
,所以你在运行容器时加上 --init
参数,那么 Docker 就会自动使用 tini
作为容器内的 PID 1 进程。你不需要额外安装或配置什么东西,只需要在命令中加上 --init
,就像这样:
1 | docker run --init -it node:18 node app.js |
方法 2: 在 Dockerfile 中手动添加 tini
你也可以显式地为 Dockerfile
添加 tini
:
1 | FROM node:18 |
然后你可以像往常一样构建和运行它:
1 | docker build -t node-app . |
为什么在生产环境会很重要?
在生产环境(比如 Kubernetes)中,无法处理信号可能会引发这些问题:
- 应用程序无法干净的退出
- 数据因为处理停机的逻辑没有触发而导致损坏
- 僵尸进程造成资源泄漏
使用 tini
就可以用最小的成本避免这些问题。
最后的一点想法
尽管容器内部的行为很容易被忽略,但理解 PID 1 如何工作,以及 tini
解决了什么问题,能让你的容器变得更加干净、安全,并更容易维护。所以下次你遇到哪个应用不响应 Ctrl-C,记得叫来小小的 tini
。
译者注:如果你使用 Docker Compose 编排容器的话,那么在配置文件中指定 init: true
就可以引入 tini
了,就像这样:
1 | services: |