前言
在Docker大行其道的今天,我們能夠非常方便的使用容器打包我們的應用程式,並且將它在我們的伺服器上部署並運行起來。但是,談論到如何停掉運行中的docker容器並正確的終止其中的程式,這就成為一個非常值得討論的話題了。
事實上,在我們日常的項目當中,這是我們經常需要面對和處理的問題:
情境A:假如我們打包在容器中的程式,提供HTTP方式的服務,負責處理各種HTTP requests並返回結果,我們必然希望在容器被停掉的時候,能夠讓程式有時間把已經在處理中的請求繼續處理完畢,並返回結果給用戶端。
情境B:又比如我們打包在容器中的程式,負責寫入資料到某個資料檔案中,我們希望程式能夠在容器被停掉的時候,有時間把記憶體中緩衝的資料持久化到存放裝置中,以防資料丟失。
情境C:再比如現在流行的微服務架構中,一般會有服務發現的機制,也即每一個微服務在啟動之後,都會主動把自己的地址資訊註冊到服務發現模組當中,讓其他的服務可以知道自己的存在。而在容器被停掉的時候,微服務需要即時從服務發現模組中登出自己,以防止從API Gateway而來的請求被錯誤的路由到了已經被停止掉的微服務。
如上的各種情境中,都要求打包在容器中的應用程式能夠被優雅的終止(也即gracefully shutdown),這種gracefully shutdown的方式,允許程式在容器被停止的時候,有一定時間做一些後續處理操作,這也是我們需要進一步探討的話題。
docker stop 與 docker kill 的區別
Docker本身提供了兩種終止容器啟動並執行方式,即docker stop與docker kill。
docker stop
先來說說docker stop吧,當我們用docker stop命令來停掉容器的時候,docker預設會允許容器中的應用程式有10秒的時間用以終止運行。所以我們查看docker stop命令協助的時候,會有如下的提示:
→ docker stop --helpUsage: docker stop [OPTIONS] CONTAINER [CONTAINER...]Stop one or more running containersOptions: --help Print usage -t, --time int Seconds to wait for stop before killing it (default 10)
在docker stop命令執行的時候,會先向容器中PID為1的進程發送系統訊號SIGTERM,然後等待容器中的應用程式終止執行,如果等待時間達到設定的逾時時間,或者預設的10秒,會繼續發送SIGKILL的系統訊號強行kill掉進程。在容器中的應用程式,可以選擇忽略和不處理SIGTERM訊號,不過一旦達到逾時時間,程式就會被系統強行kill掉,因為SIGKILL訊號是直接發往系統核心的,應用程式沒有機會去處理它。在使用docker stop命令的時候,我們唯一能控制的是逾時時間,比如設定為20秒逾時:
docker stop --time=20 container_name
docker kill
接著我們來看看docker kill命令,預設情況下,docker kill命令不會給容器中的應用程式有任何gracefully shutdown的機會。它會直接發出SIGKILL的系統訊號,以強行終止容器中程式的運行。通過查看docker kill命令的協助,我們可以看到,除了預設發送SIGKILL訊號外,還允許我們發送一些自訂的系統訊號:
→ docker kill --helpUsage: docker kill [OPTIONS] CONTAINER [CONTAINER...]Kill one or more running containersOptions: --help Print usage -s, --signal string Signal to send to the container (default "KILL")
比如,如果我們想向docker中的程式發送SIGINT訊號,我們可以這樣來實現:
docker kill --signal=SIGINT container_name
與docker stop命令不一樣的地方在於,docker kill沒有任何的逾時時間設定,它會直接發送SIGKILL訊號,以及使用者通過signal參數指定的其他訊號。
其實不難看出,docker stop命令,更類似於Linux系統中的kill命令,二者都是發送系統訊號SIGTERM。而docker kill命令,更像是Linux系統中的kill -9或者是kill -SIGKILL命令,用來發送SIGKILL訊號,強行終止進程。
在程式中接收並處理訊號
瞭解了docker stop與docker kill的區別,我們能夠知道,docker kill適合用來強行終止程式並實現快速停止容器。而如果希望程式能夠gracefully shutdown的話,docker stop才是不二之選。這樣,我們可以讓程式在接收到SIGTERM訊號後,有一定的時間處理、儲存程式執行現場,優雅的退出程式。
接下來我們可以寫一個簡單的Go程式來實現訊號的接收與處理,程式在啟動過後,會一直阻塞並監聽系統訊號,直到監測到對應的系統訊號後,輸出控制台並退出執行。
// main.gopackage mainimport ( "fmt" "os" "os/signal" "syscall")func main() { fmt.Println("Program started...") ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGTERM) s := <-ch if s == syscall.SIGTERM { fmt.Println("SIGTERM received!") //Do something... } fmt.Println("Exiting...")}
接下來使用交叉編譯的方式來編譯器,讓程式可以在Linux下運行:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o graceful
編譯好之後,我們還需要打包程式到容器中運行。於是,我們還得有個Dockerfile。在這裡,我們選擇使用體積小又輕盈的alpine鏡像作為基礎鏡像,打包這個Go程式:
from alpine:latestMAINTAINER TimothyADD graceful /gracefulCMD ["/graceful"]
這裡需要避開的一個坑,是Dockerfile中CMD命令的用法。
CMD命令有兩種方式:
使用 CMD command param1 param2 這種方式,其實是以shell的方式運行程式。最終程式被執行時,類似於/bin/sh -c的方式運行了我們的程式,這樣會導致/bin/sh以PID為1的進程運行,而我們的程式只不過是它fork/execs出來的子進程而已。前面我們提到過docker stop的SIGTERM訊號只是發送給容器中PID為1的進程,而這樣,我們的程式就沒法接收和處理到訊號了。
使用 CMD [“executable”,”param1”,”param2”] 這種方式啟動程式,才是我們想要的,這種方式執行和啟動時,我們的程式會被直接啟動執行,而不是以shell的方式,這樣我們的程式就能以PID=1的方式開始執行了。
話題轉回來,我們開始執行容器構建操作,打包程式:
docker build -t registry.xiaozhou.net/graceful:latest .
打包過後的鏡像,才6MB左右:
λ Timothy [workspace/src/graceful] → docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEregistry.xiaozhou.net/graceful latest b2210a85ca55 20 hours ago 6.484 MB
啟動並運行容器:
λ Timothy [workspace/src/graceful] → docker run -d --name graceful b2210a85
查看容器運行狀態:
λ Timothy [workspace/src/graceful] → docker ps -aCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESfd18eedafd16 b221 "/graceful" 3 seconds ago Up 2 seconds graceful
查看容器輸出,能看到程式已經正常啟動:
λ Timothy [workspace/src/graceful] → docker logs gracefulStarted...
接著我們要使用docker stop大法,看程式能否響應SIGTERM訊號:
λ Timothy [workspace/src/graceful] → docker stop gracefulgraceful
最後,查看容器的日誌,檢驗輸出:
λ Timothy [workspace/src/graceful] → docker logs gracefulStarted...SIGTERM received!Exiting...
總結
以上就是這篇文章的全部內容了,用docker kill命令,可以簡單粗暴的終止docker容器中啟動並執行程式,但是想要優雅的終止掉的話,我們需要使用docker stop命令,並且在程式中多花一些功夫來處理系統訊號,這樣能保證程式不被粗暴的終止掉,從而實現gracefully shutdown。希望本文的內容對大家的學習或者工作能有所協助,如果有疑問大家可以留言交流。