BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

dumb-init:一个Docker容器初始化系统

| 作者 金灵杰 关注 5 他的粉丝 发布于 2016年1月9日. 估计阅读时间: 9 分钟 | QCon上海2018 关注大数据平台技术选型、搭建、系统迁移和优化的经验。

容器化环境中,往往直接运行应用程序,而缺少初始化系统(如systemd、sysvinit等)。这可能需要应用程序来处理系统信号,接管子进程,进而导致容器无法停止、产生僵尸进程等问题。

Yelp开发的dumb-init,旨在模拟初始化系统功能,避免上述问题的发生。

问题的根源

对于开发人员来说,希望在容器中运行的进程和普通进程行为一致,这样才能大大降低容器化迁移的成本,而无须让开发人员关注容器初始化和退出的流程。

归功于Linux的名字空间(namespace),从容器中看,由容器创建的第一个进程pid为1。而对于Linux来说,pid为1的进程,有着特殊的使命:

  1. 传递信号,确保子进程完全退出
  2. 等待子进程退出

子进程的优雅退出

对于第一点,如果pid为1的进程,无法向其子进程传递信号,可能导致容器发送SIGTERM信号之后,父进程等待子进程退出。此时,如果父进程不能将信号传递到子进程,则整个容器就将无法正常退出,除非向父进程发送SIGKILL信号,使其强行退出。

考虑如下进程树:

  • bash(PID 1)
    • app(PID2)

bash进程在接受到SIGTERM信号的时候,不会向app进程传递这个信号,这会导致app进程仍然不会退出。对于传统Linux系统(bash进程PID不为1),在bash进程退出之后,app进程的父进程会被init进程(PID为1)接管,成为其父进程。但是在容器环境中,这样的行为会使app进程失去父进程,因此bash进程不会退出。

举个例子:

docker run ubuntu /bin/bash -c '(sleep 1000 &) && sleep 2000'

该命令会启动的容器,内部进程结构为:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  17960  2816 ?        Ss   13:05   0:00 /bin/bash -c (sleep 1000 &) && sleep 2000
root         7  0.0  0.0   4348   748 ?        S    13:05   0:00 sleep 1000
root         8  0.0  0.0   4348   644 ?        S    13:05   0:00 sleep 2000

此时,如果对这个容器发送SIGTERM信号,该容器将不会退出:

docker kill -s SIGTERM 8ef469d46b52

注意,直接使用docker kill命令,会向容器发送SIGKILL信号强制杀死进程。docker stop命令会先发送SIGTERM信号,等待超时时间之后,发送SIGKILL信号。因此,此时通过这两个命令都能够结束容器,但都不能“优雅的”结束进程。

僵尸子进程

另一个问题是等待子进程退出。前面提到过,init进程另一个任务,是需要接管子进程,确保其能正常退出。但是一般应用程序,不会考虑实现接管进程功能。当应用程序进程在容器中运行时,其子进程创建的子进程,就有可能成为僵尸进程。

这里来模拟这个过程,首先启动一个容器,执行sleep命令:

docker run ubuntu /bin/bash -c 'sleep 1000'

此时,容器中只有一个sleep进程,其PID为1。这时,我们进入这个容器,再启动一个bash进程和一个sleep进程,模拟应用程序派生出来的子进程。

首先进入容器,

docker exec -it 4ecdaafb501f /bin/bash

然后创建进程:

bash -c 'sleep 1000'

这时,容器中有一个PID为1的sleep进程,一个bash进程,一个父进程为bash进程的sleep进程。

root@4ecdaafb501f:/# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 13:30 ?        00:00:00 sleep 1000
root         6     0  0 13:30 ?        00:00:00 /bin/bash
root        21     6  0 13:31 ?        00:00:00 sleep 1000

再新开一个回话进入容器,然后对PID为6的bash进程发送SIGKILL信号,将其杀死,该操作模拟应用程序的子进程结束场景。此时,bash进程的子进程sleep进程,由于失去了父进程,将会由PID为1的sleep进程进行托管。但是,由于sleep命令不是标准的init系统,没有实现子进程托管的功能。此时的PID为21的进程,虽然已经结束,但是其没有被父进程回收(通过waitpid系统调用),进入僵尸进程状态。

root@4ecdaafb501f:/# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   4348   668 ?        Ss   13:30   0:00 sleep 1000
root        21  0.0  0.0      0     0 ?        Z    13:31   0:00 [sleep] 

dumb-init来了

dumb-init解决了上述两个问题:向子进程代理发送信号和接管子进程。

默认情况下,dumb-init会向子进程的进程组发送其收到的信号。原因也很简单,前面已经提到过,像bash这样的应用,自己接收到信号之后,不会向子进程发送信号。当然,dumb-init也可以通过设置环境变量DUMB_INIT_SETSID=0来控制只向它的直接子进程发送信号。

另外dumb-init也会接管失去父进程的进程,确保其能正常退出。

dumb-init使用

要在容器中使用dumb-init,可以直接安装deb包,或者从源码构建。容器启动时,使用dumb-init作为初始进程,确保所有子进程都由dumb-init进程创建:

docker run my_container dumb-init python -c 'while True: pass'

除了在容器中使用之外,dumb-init也可以直接在shell脚本中使用。使用dumb-init作为shell的父进程,可以解决shell创建的子进程优雅退出问题。这种场景使用方式类似于supervisord或者daemontools,直接将脚本的shebang改成#!/usr/bin/dumb-init /bin/sh即可。


感谢魏星对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群InfoQ好读者(已满),InfoQ读者交流群(#2)InfoQ好读者)。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

谢谢,非常好的说明 by Shen Edward

通俗易懂

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT