dumb-init:一個Docker容器初始化系統

jopen 8年前發布 | 9K 次閱讀 Docker

容器化環境中,往往直接運行應用程序,而缺少初始化系統(如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 即可。

 
來自: http://www.infoq.com/cn/news/2016/01/dumb-init-Docker

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!