1. 介紹
Docker的大部分重點是在隔離的容器中打包和運行應用程序的過程。有無數的教程說明了如何在Docker容器中運行應用程序,但是很少有教程討論如何正確停止容器化的應用程序。這似乎是一個愚蠢的話題-誰在乎您如何停止容器?
嗯,根據您的應用程序,停止應用程序的過程可能非常重要。如果您的應用程序正在處理HTTP請求,則可能需要先完成所有未完成的請求,然后再關閉容器。如果您的應用程序寫入文件,則可能要確保在退出容器之前正確刷新數據並關閉文件。
如果您只是啟動一個容器並永久運行,事情將會很容易,但是很有可能需要停止並重新啟動您的應用程序,以方便升級或遷移到另一個主機。在那些需要停止正在運行的容器的情況下,如果進程可以平穩關閉而不是突然斷開用戶連接並破壞文件,那將是更好的選擇。
因此,讓我們來看一些可以優雅地停止Docker容器的操作。
2. 發送信號
您可以使用許多不同的Docker命令來停止正在運行的容器。
2.1 docker stop
當您發出docker stop
命令時,Docker首先會很好地要求停止該過程,如果它在10秒鍾內不符合要求,它將強行殺死它。如果您曾經發出docker stop
過命令,並且不得不等待10秒才能返回命令,那么您已經看到了它的作用。
該docker stop
命令首先嘗試通過向容器中的根進程(PID 1)發送SIGTERM信號來停止正在運行的容器。如果該進程在超時時間內仍未退出,則將發送SIGKILL信號。
進程可以選擇忽略SIGTERM,而SIGKILL則直接進入將終止該進程的內核。該過程甚至根本看不到信號。
當docker stop
您唯一可以控制的是Docker守護程序在發送SIGKILL之前將等待的秒數:
docker stop --time=30 foo
2.2 docker kill
默認情況下,該docker kill
命令不會給容器進程提供正常退出的機會-它只是發出SIGKILL來終止容器。但是,它卻可以接受一個--signal
標志,該標志使您可以將SIGKILL之外的其他信號發送到容器進程。
例如,如果要將SIGINT(相當於終端上的Ctrl-C)發送到容器“ foo”,則可以使用以下命令:
docker kill --signal=SIGINT foo
與docker stop
命令不同,kill
它沒有任何超時時間。它僅發出一個信號(默認的SIGKILL或您使用--signal
標志指定的任何信號)。
請注意,該docker kill
命令的默認行為不同於kill
其模仿的標准Linux 命令。如果未指定其他參數,則Linux kill
命令將發送SIGTERM(與相似docker stop
)。另一方面,使用docker kill
更像是在做Linux kill -9
或Linux kill -SIGKILL
。
2.3 docker rm -f
停止正在運行的容器的最后一個選擇是將--force 或
-f
標志與docker rm
命令結合使用。通常,docker rm
用於刪除已經停止的容器,但是使用該-f
標志會使它首先發出SIGKILL。
docker rm --force foo
如果您的目標是清除正在運行的容器的所有痕跡,那么這docker rm -f
是最快的方法。但是,如果要允許容器正常關閉,則應避免使用此選項。
3. 處理信號
雖然操作系統定義了一組信號列表,但是進程對特定信號的響應方式是特定於應用程序的。
例如,如果要啟動Nginx服務器的正常關機,則應發送SIGQUIT。默認情況下,所有Docker命令都不會發出SIGQUIT,因此您需要使用以下docker kill
命令:
docker kill --signal=SIGQUIT nginx
收到SIGQUIT時,nginx日志輸出如下所示:
2015/05/11 20:30:20 [notice] 1#0: signal 3 (SIGQUIT) received, shutting down 2015/05/11 20:30:20 [notice] 9#0: gracefully shutting down 2015/05/11 20:30:20 [notice] 9#0: exiting 2015/05/11 20:30:20 [notice] 9#0: exit 2015/05/11 20:30:20 [notice] 1#0: signal 17 (SIGCHLD) received 2015/05/11 20:30:20 [notice] 1#0: worker process 9 exited with code 0 2015/05/11 20:30:20 [notice] 1#0: exit
相反,Apache使用SIGWINCH觸發正常關閉:
docker kill --signal=SIGWINCH apache
根據Apache文檔一個SIGTERM會導致服務器立即退出和終止任何正在進行的請求,所以你可能不希望使用docker stop在
Apache的容器上。
如果您在容器中運行第三方應用程序,則可能需要查看該應用程序的文檔,以了解其如何響應不同的信號。簡單地運行一個docker stop
可能不會給您想要的結果。
在容器中運行自己的應用程序時,必須確定應用程序將如何解釋不同的信號。您將需要確保在應用程序代碼中捕獲了相關信號,並采取了必要的措施以完全關閉該過程。
如果您知道將應用程序打包在Docker映像中,則可以考慮使用SIGTERM作為正常關閉信號,因為這是docker stop
命令發送的內容。
無論您使用哪種語言,它都有可能支持某種形式的信號處理。我在以下列表中收集了一些語言的相關包/模塊/庫的鏈接:
如果您在應用程序中使用Go,請查看tylerb / graceful軟件包,該軟件包會自動響應SIGINT或SIGTERM信號而正常關閉http.Handler服務器。
4. 接收信號
編寫應用程序以響應特定信號而正常關閉是一個不錯的第一步,但是您還需要確保應用程序的打包方式使其有機會接收Docker命令發送的信號。如果您不小心啟動應用程序,則它可能永遠不會收到docker stop
或發送的任何信號docker kill
。
為了演示,讓我們創建一個將在Docker容器中運行的簡單應用程序:
#!/usr/bin/env bash trap 'exit 0' SIGTERM while true; do :; done
這個瑣碎的bash腳本只是進入無限循環,但是如果收到SIGTERM,它將以0狀態退出。
我們將使用以下Dockerfile將其打包到Docker映像中:
FROM ubuntu:trusty COPY loop.sh / CMD /loop.sh
這將簡單地將loop.sh bash腳本復制到基於Ubuntu的映像中,並將其設置為運行容器的默認命令。
現在,讓我們構建此映像,啟動一個容器,然后立即停止它。
$ docker build -t loop . Sending build context to Docker daemon 3.072 kB Sending build context to Docker daemon Step 0 : FROM ubuntu:trusty ---> 07f8e8c5e660 Step 1 : COPY loop.sh / ---> 161f583a7028 Removing intermediate container e0988f66358a Step 2 : CMD /loop.sh ---> Running in 6d6664be02da ---> 18b3feccee90 Removing intermediate container 6d6664be02da Successfully built 18b3feccee90 $ docker run -d loop 64d39c3b49147f847722dbfd0c7976315533a729d9453c34cb6cbdaa11d46c21 $ docker stop 64d39c3b
如果繼續進行,您可能已經注意到docker stop
上面的命令花費了大約10秒鍾來完成-這通常表明您的容器沒有對SIGTERM做出響應,並且必須以SIGKILL強制終止。
我們可以通過查看容器的退出狀態來驗證這一點。
$ docker inspect -f '{{.State.ExitCode}}' 64d39c3b 137
基於我們在應用程序中設置的處理程序,如果我們的容器收到SIGTERM,則應該看到0退出狀態,而不是137。實際上,退出狀態大於128通常表示該進程由於以下原因而終止:未處理的信號。137 = 128 + 9-表示該進程由於信號編號9(SIGKILL)而終止。
那么,這里發生了什么?我們的應用程序被編碼為捕獲SIGTERM並正常退出。我們知道docker stop
將SIGTERM發送到容器進程。但似乎信號從未傳到我們的應用程序中。
要了解這里發生的情況,讓我們啟動另一個容器並看一看正在運行的進程。
$ docker run -d loop 512c36b5b517b3a43246b519bc5cdb756cdbc4d2ff1e0a3984e83b094f3db136 $ docker exec 512c36b5 ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 16:03 ? 00:00:00 /bin/sh -c /loop.sh root 13 1 61 16:03 ? 00:00:10 bash /loop.sh root 14 0 0 16:03 ? 00:00:00 ps -ef
在上面的輸出中要注意的重要一點是我們的loop.sh腳本未在容器內作為PID 1運行。該腳本實際上是作為運行在PID 1 的/ bin / sh進程的子進程運行的
當您使用docker stop
或docker kill
向容器發出信號時,該信號僅發送到以PID 1運行的容器進程。
由於/ bin / sh不會將信號轉發給任何子進程,因此我們發送的SIGTERM從未到達我們的腳本。顯然,如果我們希望我們的應用程序能夠接收來自主機的信號,則需要找到一種將其作為PID 1運行的方法。
為此,我們需要返回到Dockerfile,並查看用於啟動腳本的CMD指令。實際上,CMD指令可以采用幾種不同的形式。在上面的Dockerfile中,我們使用了如下的shell形式:
CMD command param1 param2
使用shell形式時,指定的命令與/bin/sh -c
shell一起執行。如果您回顧一下我們容器的進程列表,您將看到PID 1處的進程顯示命令字符串“ / bin / sh -c /loop.sh”。因此,/ bin / sh作為PID 1運行,然后派生/執行我們的腳本。
幸運的是,Docker還支持CMD指令的exec形式,如下所示:
CMD ["executable","param1","param2"]
請注意,在這種情況下,出現在CMD指令之后的內容被格式化為JSON數組。
當使用CMD指令的exec形式時,該命令將在沒有shell的情況下執行。
讓我們更改Dockerfile來查看實際效果:
FROM ubuntu:trusty COPY loop.sh / CMD ["/loop.sh"]
重建映像並查看容器中運行的進程:
$ docker build -t loop . [truncated] $ docker run -d loop 4dda905ee902c91d1f56082d1092d6d72ef54b3d4582fe6b453cba90777554e2 $ docker exec 4dda905e ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 30 16:42 ? 00:00:04 bash /loop.sh root 13 0 0 16:42 ? 00:00:00 ps -ef
現在,我們的腳本以PID 1的身份運行。讓我們向容器發送SIGTERM並查看退出狀態:
$ docker stop 4dda905e $ docker inspect -f '{{.State.ExitCode}}' 4dda905e 0
這正是我們所期望的結果!我們的腳本收到docker stop
命令發送的SIGTERM,並以0狀態干凈退出。
最重要的是,您應該審核容器中的進程,以確保它們能夠接收要發送的信號。在您的Dockerfile中使用CMD(或ENTRYPOINT)指令的exec形式是一個好的開始。
結論
使用docker kill
命令終止Docker容器非常容易,但是如果您實際上想以有序的方式關閉應用程序,則需要進行更多的工作。現在,您應該了解如何向容器發送信號,如何在自定義應用程序中處理這些信號以及如何確保應用程序甚至可以首先接收到這些信號。
文章原文鏈接:https://www.ctl.io/developers/blog/post/gracefully-stopping-docker-containers/