<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          (譯)Docker 中的 PID-1、孤兒、僵尸和信號

          共 7645字,需瀏覽 16分鐘

           ·

          2020-12-18 09:53

          使用 Docker 的時候,在多進程、信號方面會有一些邊緣用例。在 Phusion 博客上有一篇相關(guān)文章,后續(xù)內(nèi)容中會嘗試接觸這些問題,并使用 fpco/pid1 解決問題。

          Phusion 博文中試用了他們的 基礎(chǔ)鏡像。這個鏡像提供了 my_init 作為 entrypoint 來解決問題,同時還提供了 syslog 之類的額外的功能。不幸的是我們在使用其中的 syslog-ng 時遇到了麻煩,會產(chǎn)生占用 100% CPU 且無法殺死的進程。我們還在調(diào)查其根本原因,但在實踐中我們發(fā)現(xiàn),一個簡單的 init 是更加迫切的需求,因此我們創(chuàng)建了 pid1 Haskell 包 和一個 Docker 鏡像 fpco/pid1

          建議讀者閱讀本文的同時打開終端運行命令,以求獲得最大收益??吹揭粋€ Ctrl+C 無法殺死的進程會讓人更有動力。

          我們用 Haskell 自行實現(xiàn)的目的是嵌入到 Stack build tool 之中。還有一些其它輕量級初始化進程,例如 dumb-init。我也寫了關(guān)于 dumb-init 的文章。這里用的 pid1 跟其它的初始化進程之間沒有什么差別。

          和 Entrypoint 一起玩耍

          Docker 有個 Entrypoint 的概念,其中對使用 docker run 運行容器時的命令進行缺省封裝。例如下面的情況:

          docker run --entrypoint /usr/bin/env ubuntu:16.04 FOO=BAR bash -c 'echo $FOO'
          BAR

          與之等價的命令是 docker run ubuntu:16.04 /usr/bin/env FOO=BAR bash -c 'echo $FOO'

          這兩個等價的命令展示了在命令行中替代 Entrypoint 的情況。后面還會在 Dockerfile 中進行指定。Ubuntu 鏡像的缺省 entrypoint 是空的,也就是說命令部分不會經(jīng)過任何封裝,直接運行。因為目前版本的 Docker 還不支持將 entrypoint 設(shè)置為空,所以我們準備使用 /usr/bin/env 作為 entrypoint 來模擬這種狀況。當運行 /usr/bin/env foo bar baz 時,env 進程會執(zhí)行 foo 命令,foo 會變成新的 PID 1,這樣的運行結(jié)果是和空的 entrypoint 是一致的。

          fpco/pid1snoyberg/docker-testing 都會把 /sbin/pid1 作為缺省的 entrypoint。在示例命令中,為了清晰的示范,我們顯式地使用了 --entrypoint /sbin/pid1,實際上去掉這個選項,也會是同樣的效果。

          向進程發(fā)送 TERM 信號

          我們會以 sigterm.hs 命令開始,這個命令會執(zhí)行 ps,然后給自己發(fā)送一個 SIGTERM,持續(xù)循環(huán)。在 Unix 系統(tǒng)中,進程收到 SIGTERM 的缺省操作就是退出。因此我們推測我們的進程應(yīng)該啟動之后直接退出,實際情況:

          $ docker run --rm --entrypoint /usr/bin/env snoyberg/docker-testing sigterm
          PID TTY TIME CMD
          1 ? 00:00:00 sigterm
          9 ? 00:00:00 ps
          Still alive!
          Still alive!
          Still alive!
          ^C
          $

          該進程忽略了 SIGTERM 保持運行,直到我們手工輸入了 Ctrl+C。這個腳本還有個功能就是,如果使用了 install-handler 參數(shù),就會顯式地安裝一個 SIGTERM 的接收器,用于殺死進程。使用這個參數(shù)之后情況就不同了:

          $ docker run --rm --entrypoint /usr/bin/env snoyberg/docker-testing sigterm install-handler
          PID TTY TIME CMD
          1 ? 00:00:00 sigterm
          8 ? 00:00:00 ps
          Still alive!
          $

          這個結(jié)果涉及到 Linux 內(nèi)核:內(nèi)核對 PID 1 是另眼相看的,缺省情況下收到 SIGTERM 或者 SIGINT 信號不會殺死進程。這個情況讓人很不習慣。下一個測試中,使用兩個不同的終端分別執(zhí)行命令:

          $ docker run --rm --name sleeper ubuntu:16.04 sleep 100
          $ docker kill -s TERM sleeper

          我們會看到,docker run 命令并沒退出,如果檢查一下 ps aux 的輸出,會看到這個進程還在運行。原因是 sleep 進程沒有針對 PID 1 的場景進行設(shè)計,也就是說沒有專門設(shè)置信號處理工作,要正確響應(yīng)信號,有兩個選擇:

          • 確保 docker run 運行的命令顯式地處理 SIGTERM

          • 讓命令的 PID 不為 1,用設(shè)計了信號處理的應(yīng)用來充當 PID 1 的角色。

          看看 sigterm 程序在使用 /sbin/pid1 作為 entrypoint 時候的表現(xiàn):

          $ docker run --rm --entrypoint /sbin/pid1 snoyberg/docker-testing sigterm
          PID TTY TIME CMD
          1 ? 00:00:00 pid1
          8 ? 00:00:00 sigterm
          12 ? 00:00:00 ps

          程序如愿退出。但是看看 ps 的輸出:第一個進程是 pid1,而不是 sigterm。sigterm 這里的 PID 是 8,也就不會像 PID 為 1 時候的行為了,它會按照缺省行為處理 SIGTERM。這里的具體步驟是:

          1. 創(chuàng)建容器,并在其中執(zhí)行 /usr/sbin/pid1 sigterm。

          2. pid1 的 PID 為 1,并 fork/execsigterm

          3. sigterm 向自己發(fā)送了 SIGTERM,導致被殺。

          4. pid1 發(fā)現(xiàn)子進程被 SIGTERM 殺掉(sigal 15),并用 143 的返回碼退出(128+15)

          5. PID 1 死掉,容器也就死掉了。

          這并不是 sigterm 的特殊能力,sleep 也可以達到同樣的目的:

          $ docker run --rm --name sleeper fpco/pid1 sleep 100
          $ docker kill -s TERM sleeper
          ...

          ubuntu 鏡像不同,fpco/pid1entrypointsbin/pid1,這個容器會被立刻殺掉。

          sigterm 會給自己發(fā)送 TERM 信號(譯注:只要它不是 PID1,就能正常退出,它退出之后,父進程也會退出),因此并不需要一個特別的 PID1 進程。例如可以直接運行 docker run --rm --entrypoint /usr/bin/env snoyberg/docker-testing /bin/bash -c "sigterm;echo bye",但是在 sleep 的情況下,就必須有能夠正確處理信號的 PID1 了(譯注:因為 docker kill 的信號是發(fā)給 PID1 的)。

          Ctrl+C sigterm 和 sleep

          sigtermsleep 在面對 Ctrl+C 的時候會不太一樣。Ctrl+C 會發(fā)送 SIGINTdocker run 進程,它會把信號轉(zhuǎn)發(fā)給容器內(nèi)的信號。因為 Linux 內(nèi)核的優(yōu)待,sleep 也會忽略這個信號。然而 sigterm 是用 Haskell 編寫的,Haskell 運行時自帶一個包含 SIGINT 的信號處理過程,它會覆蓋 PID1 進程的缺省行為。docker attach 文檔中包含了更多關(guān)于信號轉(zhuǎn)發(fā)的內(nèi)容。

          僵尸進程

          假設(shè)有一個進程 A,A 會 exec/fork 進程 B。當進程 B 死掉時,進程 A 必須調(diào)用 waitpid,從內(nèi)核獲取進程 B 的退出狀態(tài),如果這個過程無法完成,進程 B 雖然死掉,但是還是會在系統(tǒng)進程表中留下一個記錄。這種進程通常被稱為僵尸。

          orphans.hs 的行為:

          • 生成一個子進程,用死循環(huán)調(diào)用 ps;

          • 在子進程中:

            運行 echo 命令多次,不調(diào)用 waitpid 然后退出。

          如你所見,沒有進程會回收成為僵尸的 echo 進程。進程輸出的內(nèi)容,會看到生成了僵尸:

          $ docker run --rm --entrypoint /usr/bin/env snoyberg/docker-testing orphans
          1
          2
          3
          4
          Still alive!
          PID TTY TIME CMD
          1 ? 00:00:00 orphans
          8 ? 00:00:00 orphans
          13 ? 00:00:00 echo
          14 ? 00:00:00 echo
          15 ? 00:00:00 echo
          16 ? 00:00:00 echo
          17 ? 00:00:00 ps
          Still alive!
          PID TTY TIME CMD
          1 ? 00:00:00 orphans
          13 ? 00:00:00 echo
          14 ? 00:00:00 echo
          15 ? 00:00:00 echo
          16 ? 00:00:00 echo
          18 ? 00:00:00 ps
          Still alive!

          這里看到了幾個僵尸進程。原因是我們的 PID1 沒有進行回收。你可能猜到,我們可以使用 /sbin/pid1 解決這個問題:

          $ docker run --rm --entrypoint /sbin/pid1 snoyberg/docker-testing orphans
          1
          2
          3
          4
          Still alive!
          PID TTY TIME CMD
          1 ? 00:00:00 pid1
          10 ? 00:00:00 orphans
          14 ? 00:00:00 orphans
          19 ? 00:00:00 echo
          20 ? 00:00:00 echo
          21 ? 00:00:00 echo
          22 ? 00:00:00 echo
          23 ? 00:00:00 ps
          Still alive!
          PID TTY TIME CMD
          1 ? 00:00:00 pid1
          10 ? 00:00:00 orphans
          24 ? 00:00:00 ps
          Still alive!

          pid1 會在子進程死掉時接收 echo 進程,并進行收割。

          進程清理

          我們來試點別的:A 進程是 Docker 容器的主進程,它生成了進程 B。如果 A 比 B 退出的早,會讓 Docker 容器退出。這種情況下,運行中的進程 B 會被內(nèi)核強制關(guān)閉(Stackoverflow 討論了該問題的詳情),我們可以通過 surviving.hs 來觀察這個情況:

          $ docker run --rm --entrypoint /usr/bin/env snoyberg/docker-testing surviving
          Parent sleeping
          Child: 1
          Child: 2
          Child: 4
          Child: 3
          Child: 1
          Child: 2
          Child: 3
          Child: 4
          Parent exiting

          不幸的是,我們的子進程沒機會進行清理。我們應(yīng)該給他們發(fā)送一個 SIGTERM,在一段時間后發(fā)送 SIGKILL,pid1 就是這么做的:

          $ docker run --rm --entrypoint /sbin/pid1 snoyberg/docker-testing surviving
          Parent sleeping
          Child: 2
          Child: 3
          Child: 1
          Child: 4
          Child: 2
          Child: 1
          Child: 4
          Child: 3
          Parent exiting
          Got a TERM
          Got a TERM
          Got a TERM
          Got a TERM

          Docker Run 和 PID1

          如果運行 sleep 60,然后輸入 Ctrl+Csleep 進程會收到 SIGINT。如果運行 docker run --rm fpco/pid1 sleep 60,再輸入 Ctrl+C,事情就不同了。docker run 創(chuàng)建了一個 docker run 進程,它會給 Docker 服務(wù)發(fā)送一個命令,這個服務(wù)會在容器里創(chuàng)建真正的 sleep 進程。在終端輸入 Ctrl+C 的時候,SIGINT 會被發(fā)送給 docker run,最后轉(zhuǎn)換成 sleep 進程的 SIGINT。

          如何證明呢?

          $ docker run --rm fpco/pid1 sleep 60&
          [1] 417
          $ kill -KILL $!
          $ docker ps
          CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
          69fbc70e95e2 fpco/pid1 "/sbin/pid1 sleep 60" 11 seconds ago Up 11 seconds hopeful_mayer
          [1]+ Killed docker run --rm fpco/pid1 sleep 60

          這個案例中發(fā)送 SIGKILLdocker run,相對于 SIGINT 以及 SIGTERM,SIGKILL 有些不同,docker run 無法轉(zhuǎn)發(fā)這個信號,因此會殺掉自己,但是 sleep 進程和所在的容器會持續(xù)運行。

          所以:

          • 用類似 pid1 的東西來保障 SIGINT 或者 SIGTERM 能夠真正地停止容器。

          • 如果必須要給進程發(fā)送 SIGKILL,應(yīng)該使用 docker kill

          entrypoint 的替代方案

          我們用了很多次 --entrypoint /sbin/pid1。實際上這很多余,fpco/pid1snoyberg/docker-testing 鏡像的缺省 entrypoint 都是 /sbin/pid1

          $ docker run --rm fpco/pid1 sleep 60
          ^C
          $

          如果嫌 entrypoint 麻煩,可以用在命令之中,例如:

          $ docker run --rm --entrypoint /usr/bin/env fpco/pid1 /sbin/pid1 sleep 60
          ^C
          $

          Dockerfile,command vs exec

          你可能想把 ENTRYPOINT /sbin/pid1 放到 Dockerfile 里,結(jié)果卻不盡人意:

          $ cat Dockerfile
          FROM fpco/pid1
          ENTRYPOINT /sbin/pid1
          $ docker build --tag test .
          Sending build context to Docker daemon 2.048 kB
          Step 1 : FROM fpco/pid1
          ---> aef1f7b702b9
          Step 2 : ENTRYPOINT /sbin/pid1
          ---> Using cache
          ---> f875b43a9e40
          Successfully built f875b43a9e40
          $ docker run --rm test ps
          pid1: No arguments provided

          出現(xiàn)這個問題的原因是使用的 command 形式的方法,它只是定義了一個給 Shell 處理的原始字符串,無法加入額外的命令(例如 ps),這樣一來,pid1 進程就沒有了附加語句,無法運行。正確的定義形式是 ENTRYPOINT ["/sbin/pid1"]

          $ cat Dockerfile
          FROM fpco/pid1
          ENTRYPOINT ["/sbin/pid1"]
          $ docker build --tag test .
          Sending build context to Docker daemon 2.048 kB
          Step 1 : FROM fpco/pid1
          ---> aef1f7b702b9
          Step 2 : ENTRYPOINT /sbin/pid1
          ---> Running in ba0fa8c5bd41
          ---> 4835dec4aae6
          Removing intermediate container ba0fa8c5bd41
          Successfully built 4835dec4aae6
          $ docker run --rm test ps
          PID TTY TIME CMD
          1 ? 00:00:00 pid1
          8 ? 00:00:00 ps

          盡量使用這種模式,可以避免對 shell 的需要。

          結(jié)論

          正常情況下,都需要使用一個 pid1 這樣的初始化進程。Phusion/my_init 的方式是可行的,但是太過沉重。如果不需要 syslog 以及其他的特性,最好還是用一個最小化的選擇。

          瀏覽 46
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91n成人久久 | 超碰青娱乐| 国产操B片 | 三级片欧美网站 | 亚洲视频免费在线看 |