哑巴
Dumb-Init是一个简单的过程主管和初始化系统,旨在在最小容器环境(例如Docker)中以PID 1运行。它被部署为用C的小静态二进制二进制。
轻巧的容器已经普及了没有正常的初始化系统(例如SystemD或sysvinit)运行单个过程或服务的想法。但是,省略初始化系统通常会导致对流程和信号的处理不正确,并且可能导致无法优雅停止的容器,或者泄漏应该被销毁的容器。
dumb-init使您只需将命令带有dumb-init 。它充当PID 1,并立即将您的命令作为儿童流程产生,并在收到的情况下要注意正确处理和转发信号。
为什么需要一个初始化系统
通常,当您启动Docker容器时,要执行的过程将变为PID 1,使其成为容器的初始化系统所带来的怪癖和职责。
这有两个常见的问题:
-
在大多数情况下,信号无法正确处理。
Linux内核将特殊信号处理应用于以PID 1运行的过程。
当在普通Linux系统上发送流程信号时,内核将首先检查该过程已注册该信号的任何自定义处理程序,否则会落后于默认行为(例如,在
SIGTERM上杀死该过程)。但是,如果接收信号的过程为PID 1,则可以通过内核进行特殊处理。如果尚未注册信号的处理程序,则内核将不会落后于默认行为,也不会发生任何事情。换句话说,如果您的流程没有明确处理这些信号,则将其
SIGTERM无效。一个常见的例子是CI作业,
docker run my-container script:将SIGTERM发送到docker run流程通常会杀死docker run命令,但将容器运行在后台。 -
孤立的僵尸过程没有得到适当的收获。
当过程退出时,过程将变成一个僵尸,并保持僵尸,直到其父母调用
wait()系统调用的一些变化为止。它仍然是过程表中的“已停产”过程。通常,父进程会立即致电wait(),并避免长寿僵尸。如果父母在孩子面前退出,则孩子是“孤儿”的,并根据PID 1进行了重新分配。因此,Init Systems负责在孤立的僵尸过程中
wait()。当然,大多数过程都不会在恰好与之相连的随机过程上
wait(),因此容器通常以数十个植根于PID 1的僵尸结束。
dumb-init有什么作用
dumb-init以PID 1的形式运行,就像一个简单的Init系统。它启动了一个过程,然后代理都收到了扎根于该儿童过程的会话的信号。
由于您的实际过程不再是PID 1,当它从dumb-init接收信号时,将应用默认信号处理程序,并且您的过程将按照您的预期行事。如果您的流程死亡, dumb-init也会死亡,请注意清理可能仍然存在的任何其他过程。
会话行为
在默认模式下, dumb-init建立了一个植根于孩子的会话,并将信号发送给整个过程组。如果您的行为不佳的孩子(例如壳牌脚本)通常不会在死前表示其孩子,这将很有用。
实际上,这在常规过程主管(例如Daemontools或Supervisord)中用于监督Shell脚本的常规过程主管中的Docker容器之外很有用。通常,外壳接收到的SIGTERM之类的信号不会转发给子过程。相反,只有外壳过程死亡。使用Dumb-Init,您只需在Shebang中写下带有Dumb-Init的外壳脚本:
#!/usr/bin/dumb-init /bin/sh
my-web-server & # launch a process in the background
my-other-server # launch another process in the foreground
通常,发送给外壳的SIGTERM会杀死外壳,但使这些过程运行(背景和前景!)。使用Dumb-Init,您的子流程将收到您的外壳相同的信号。
如果您希望仅将信号发送到直接的孩子,则可以使用--single-child参数运行,或者在运行dumb-init时设置环境变量DUMB_INIT_SETSID=0 。在这种模式下,愚蠢的内部是完全透明的。您甚至可以将多个串在一起(例如dumb-init dumb-init echo \'oh, hi\' )。
信号重写
愚蠢的内部允许在代理传入信号之前重写传入信号。在您拥有码头主管(例如Mesos或Kubernetes)的情况下,这很有用它总是发送标准信号(例如Sigterm)。一些应用需要不同的停止信号才能进行优雅的清理。
例如,要将信号sigterm(编号15)重写为sigquit(数字3),只需在命令行上添加--rewrite 15:3即可。
要完全删除信号,您可以将其重写为特殊编号0 。
信号重写特殊情况
在setSID模式下运行时,在大多数情况下不足以向SIGTSTP / SIGTTIN / sigttou转发sigtstp / SIGTTOU ,因为如果该过程没有为这些信号添加自定义信号处理程序,则该内核将不应用默认信号处理行为(由于它是孤儿流过程组的成员,因此将不应用默认信号处理行为。因此,我们将默认的重写设置为从这三个信号中的SIGSTOP 。如果需要,您可以通过将信号重写回原始值来选择退出此行为。
具有此功能的一个警告:对于工作控制信号( SIGTSTP , SIGTTIN , SIGTTOU ),Dumb-Init在收到信号后总是会暂停自身,即使您将其重写为其他东西。
安装在Docker容器中
您有一些使用dumb-init的选择:
选项1:从发行版的包装存储库中安装(Debian,Ubuntu等)
现在,许多流行的Linux发行版(包括debian(自stretch )和乌本图(Ubuntu)等迪比安(Debian)衍生品(自bionic )中)现在在其官方存储库中包含愚蠢的包裹。
在基于Debian的发行版中,您可以像安装其他任何软件包一样,运行apt install dumb-init来安装哑巴。
注意:与我们提供的版本不同,大多数dumb-init的发行版本都没有静态链接(请参见下面的其他选项)。通常这是完全可以的,但是意味着,这些版本的哑巴内部版本通常在复制到其他Linux发行版时不起作用,这与我们提供的静态链接版本不同。
选项2:通过内部APT服务器(Debian/Ubuntu)安装
如果您有内部APT服务器,则将.deb上传到服务器是使用dumb-init推荐方法。在您的Dockerfiles中,您可以简单地apt install dumb-init ,并且可以使用。
Debian软件包可从GitHub发布选项卡中获得,也可以自己运行自行make builddeb 。
选项3:手动安装.deb软件包(Debian/Ubuntu)
如果您没有内部APT服务器,则可以使用dpkg -i安装.deb软件包。您可以选择将.deb放在容器上的方式(安装目录或wget -ing是某些选项)。
一种可能性是在您的Dockerfile中使用以下命令:
RUN wget https://g*ith*ub.c*om/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_amd64.deb RUN dpkg -i dumb-init_*.deb
选项4:直接下载二进制
由于Dumb-Init以静态链接的二进制发布,因此通常可以将其插入图像中。这是在Dockerfile中这样做的一个示例:
RUN wget -O /usr/local/bin/dumb-init https://*gi**thub.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_x86_64 RUN chmod +x /usr/local/bin/dumb-init
选项5:从PYPI安装
尽管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命令(并确保您正在使用推荐的JSON语法)。
在Dockerfile中,最好将Dumb-Init用作容器的入口处。 “入口点”是一个部分命令,可预先到您的CMD指令,非常适合哑巴:
# 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\" ]
如果您在基本图像中声明入口点,则任何从中下降的图像也不需要声明愚蠢的内部。他们只能像往常一样设置CMD 。
对于交互式的一次性用法,您可以手动进行预处理:
$ docker run my_container dumb-init python -c \'while True: pass\'
在没有dumb-init下运行同一命令将导致没有SIGKILL容器无法停止容器,但是使用dumb-init ,您可以将其发送更多的人道信号,例如SIGTERM 。
必须将JSON语法用于CMD和ENTRYPOINT很重要。否则,Docker会调用外壳来运行您的命令,从而将Shell作为PID 1而不是Dumb-Init。
使用外壳进行启动前钩
通常,容器想做一些在建筑时间内无法完成的启动前工作。例如,您可能需要根据环境变量模板将一些配置文件模板。
将其与哑巴纳入的最佳方法是这样:
ENTRYPOINT [ \"/usr/bin/dumb-init\" , \"--\" ] CMD [ \"bash\" , \"-c\" , \"do-some-pre-start-thing && exec my-server\" ]
通过仍将愚蠢的内部用作入口点,您始终拥有适当的初始化系统。
BASH命令的exec部分很重要,因为它用您的服务器替换了BASH进程,因此Shell仅在开始时暂时存在。
建造哑巴
构建愚蠢的二进制文件需要一个工作的编译器和LIBC标题,并默认为GLIBC。
$ make
用Musl建造
由于glibc,静态编译的哑巴内部的哑巴内部超过700kb,但现在可以选择Musl。在debian/ubuntu apt-get install musl-tools以安装源和包装器,然后仅:
$ CC=musl-gcc make
当静态地与Musl编译时,二进制尺寸约为20kb。
建造Debian包装
我们使用标准的Debian惯例来指定构建依赖性(以debian/control为单位)。入门的一种简单方法是apt-get install build-essential devscripts equivs ,然后是sudo mk-build-deps -i --remove以自动安装所有丢失的构建依赖项。然后,您可以使用make builddeb来构建愚蠢的Debian软件包。
如果您喜欢使用Docker自动化的Debian软件包构建,则只需运行make builddeb-docker即可。这很容易,但是要求您让Docker在计算机上运行。
参见
- Docker和PID 1僵尸收获问题(phusion Blog)
- 在Docker容器中捕获信号(@gchudnov)
- Tini,愚蠢的替代品
- PID1,愚蠢的替代品,用Haskell编写
