dumb-init是一个简单的进程管理器和init系统,设计用于在最小容器环境(如Docker)中作为PID 1
运行。它被部署为一个用C编写的小型静态链接二进制文件。
轻量级容器已经普及了运行单个进程或服务的想法,而没有像systemd
或sysvinit
这样的普通初始化系统。但是,省略init系统通常会导致对进程和信号的错误处理,并且可能导致诸如无法正常停止容器或泄漏应该被销毁的容器之类的问题。
dumb-init
使你可以以简单的命令前缀的方式使用它,来充当PID 1
并立即将你的命令作为子进程生成,并在收到信号时正确处理和转发信号。
通常,当你启动Docker容器时,你正在执行的进程将变为PID 1
,从而赋予它作为容器的init系统所带来的怪癖和责任。这提出了两个常见问题:
SIGTERM
上终止进程)。PID 1
运行的进程应进行特殊的信号处理。如果接收信号的进程是PID 1
,内核会进行特殊处理;如果它没有为信号注册处理器,内核将不会回退到默认行为且不做任何反应。换句话说,如果你的进程没有明确处理这些信号的信号处理器,发送SIGTERM信号给它将完全没有效果。一个常见的例子是使用docker运行
my-container
脚本的CI作业:将SIGTERM
发送到docker run
进程通常会终止docker run
命令,但容器却还在后台运行着而没有被终止。
wait()
系统调用来避免产生常驻僵尸进程。PID 1
下重新挂载其他父进程。wait()
处理孤儿僵尸进程。因为,大多数进程都不会碰巧被重新挂载的随机父进程调用wait()
,因此,容器通常以几个根植于PID 1
的僵尸进程结束。进程在退出时变为僵尸,并且在其父进程调用
wait()
系统调用(或wait()
调用的某些变体)之前保持僵尸状态(作为“已停止”的进程保留在进程表中)。
dumb-init
作为PID 1
运行,就像一个简单的init系统。它启动一个进程,然后将所有收到的信号代理到以该子进程为根的会话。
由于你的进程不再是PID 1
,当它从dumb-init
接收信号时,将应用默认的信号处理程序,并且你的进程将按照预期运行。如果你的进程死了,dumb-init
也会死掉,逐一清理仍然存在的任何其他进程。
在默认模式下,dumb-init
建立以子进程为根的会话,并将信号发送到整个进程组。如果你有一个表现不佳的子进程(比如一个shell脚本),这个子进程在死亡前通常不会给它的子进程发出信号。
这实际上可以在常规进程监视器(如daemontools或supervisord)中的Docker容器之外用于监视shell脚本。通常,shell接收到的SIGTERM
之类的信号不会转发给它的子进程;只有shell进程死掉才会发送信号。如果使用dumb-init
,你可以在Shebang
中使用dumb-init
编写shell脚本:
#!/usr/bin/dumb-init /bin/sh
my-web-server & # launch a process in the background
my-other-server # launch another process in the foreground
通常,发送到shell的SIGTERM
会杀死shell,但这些进程仍然会处于运行状态(不论是后台还是前台进程)。使用dumb-init
,你的子进程将收到与shell所执行的相同的信号。
如果你希望仅将信号发送到直接子进程,则可以使用--single-child
参数运行,或者在运行dumb-init
时设置环境变量DUMB_INIT_SETSID = 0
。在这种模式下,dumb-init
完全透明;甚至可以把多个串起来(比如dumb-init dumb-init echo 'oh,hi'
)。
dumb-init
允许在代理进程之前重写输入信号。这在始终发送标准信号(例如SIGTERM)的Docker 容器管理程序(如Mesos或Kubernetes)中非常有用。
某些应用程序需要不同的停止信号才能进行优雅的清理退出。例如,要将信号SIGTERM(编号15)重写为SIGQUIT(编号3),只需在命令行中添加
--rewrite 15:3
即可。要完全丢弃信号,可以将其重写为特殊数字0。
在setsid
模式下运行时,在大多数情况下转发SIGTSTP/SIGTTIN/SIGTTOU
是不够的,因为,如果进程没有为这些信号添加自定义信号处理器,内核将不会应用默认信号处理行为(这将暂停进程),因为它是孤儿进程组的成员。因此,将这三个信号的默认重写设置为SIGSTOP
。如果需要,可以通过将信号重写回原始值来选择不使用此行为。
有一点需要注意:对于作业控制信号(SIGTSTP,SIGTTIN,SIGTTOU),dumb-init
在收到信号后总是会自动挂起,即使你把它重写为其他东西也是如此。
五种在容器中安装dumb-init
的方式:
许多主流的Linux发行版(包括Dabian从stretch版本开始)和Debian的衍生版如Ubuntu(从bionic版本开始)都已经在官方的仓库中包含了dumb-init
安装包。
基于Debian的发行版,可以运行apt install dumb-init
来安装,就像安装其他的软件包一样。
大多数发行版提供的
dumb-init
不是静态链接文件。一般来说,这没什么毛病,但意味着这些版本的dumb-init
在复制到其他Linux发行版时就不能用了,这与静态链接版本不同。
如果有内部apt服务器,将dumb-init
以.deb
的格式上传到服务器然后在使用它。在Dockerfiles中,可以通过apt install dumb-init
,它就可以使用了。
可以从GIthub的Release页面下载Debian安装包,也可以执行
nake builddeb
自行生成。
.deb
安装包(Debian/Ubuntu)如果没有apt网络服务器,可以手动执行dpkg -i
指令来安装.deb
安装包,可以选择两种方式将.deb
放到容器中:
如下所示:
RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64.deb
RUN dpkg -i dumb-init_*.deb
由于dumb-init是作为静态链接的二进制文件发布的,因此通常只需将其放入image即可。在Dockerfile中执行如下所示的操作:
RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64
RUN chmod +x /usr/local/bin/dumb-init
虽然dumb-init
完全用C语言编写,但还提供了一个Python包来编译和安装二进制文件。它可以使用pip
从PyPI安装。首先安装一个C编译器(在Debian/Ubuntu上执行apt-get install gcc
),然后执行pip install dumb-init
。
从1.2.0开始,PyPI的软件包可作为预编译的存档文件使用,无需在常见的Linux发行版上进行编译。
一旦安装在Docker容器中,只需在命令前加上dumb-init(确保按照docker推荐的JSON语法格式编写命令,查看附录部分)。
在Dockerfile中,使用dumb-init
作为容器的入口点是一个好习惯。“entrypoint
”是一个局部指令,它被添加到的CMD指令之前,使其非常适合dumb-init
:
# Runs "/usr/bin/dumb-init -- /my/script --with --args"
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
# or if you use --rewrite or other cli flags
# ENTRYPOINT ["dumb-init", "--rewrite", "2:3", "--"]
CMD ["/my/script", "--with", "--args"]
如果在基础镜像中声明入口点,那么任何以它为基础的镜像都不需要再声明dumb-init。他们可以像往常一样只设置CMD。
对于交互式一次性使用,可以手动添加它:
$ docker run my_container dumb-init python -c 'while True: pass'
# 在没有dumb-init的情况下运行同样的命令将导致无法在没有SIGKILL的情况下停止容器,但是使用dumb-init,可以发送更多人性化的信号,如SIGTERM
对于CMD
和ENTRYPOINT
使用JSON语法非常重要。否则,Docker会调用shell来运行你的命令,从而导致shell为PID 1而不是dumb-init。
容器通常需要做一些在开始构建期间无法完成的预启动工作。例如,可能希望根据环境变量模板化一些配置文件。将它与dumb-init集成的最佳方法如下:
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["bash", "-c", "do-some-pre-start-thing && exec my-server"]
通过仍然使用dumb-init作为入口点,你可以始终拥有适当的init系统。
bash命令的exec部分很重要,因为它将bash进程替换为你的服务,因此shell仅在启动时暂时存在。
构建dumb-init二进制文件需要一个有效的编译器和libc头文件,默认为glibc。
$ make
由于glibc,静态编译的dumb-init超过700KB,现在musl是一个可选选项。在Debian/Ubuntu apt-get install musl-tools上安装源代码和包装器,然后只需:
$ CC=musl-gcc make
当用musl静态编译时,二进制大小约为20KB。
我们使用标准的Debian约束来指定编译依赖关系(查看debian/control)。一个简单的入门方法是apt-get install build-essential devscripts equivs
,然后sudo mk-build-deps -i --remove
自动安装所有缺少的编译依赖项。然后,可以使用make builddeb
来编译dumb-init Debian软件包。
如果你更喜欢使用Docker自动编译Debian软件包,只需运行make builddeb-docker
即可。这更容易,但要求在计算机上运行Docker。