當前位置: 妍妍網 > 碼農

Docker 容器優雅結束

2024-07-01碼農

原文連結:https://www.cnblogs.com/zhangmingcheng/p/18252004

1、概述

不論是什麽型別的套用,都會希望在服務停止前能夠收到停止通知,有一定的時間做結束前的釋放資源、關閉連線、不再接收外部請求等工作。比如你的套用正在處理HTTP請求,你希望在停止前能完成所有未完成的請求;如果你的套用正在寫入檔,你也許希望在停止容器前能夠正確的重新整理數據到磁盤並關閉檔。

2、Linux常見訊號

要了解套用優雅停止方法,我們先回顧一下與容器相關的Linux常見訊號。

訊號是一種行程間通訊的形式。一個訊號就是內核發送給行程的一個訊息,告訴行程發生了某種事件。行程需要為自己感興趣的訊號註冊處理常式,舉例:

  • 為了能讓程式優雅的結束(接到結束的訊號後,對資源進行清理),一般程式都會處理SIGTERM訊號。與SIGTERM訊號不同,SIGKILL訊號會粗暴的結束一個行程;

  • 許多守護行程會透過處理SIGHUP訊號實作熱載入配置檔。

  • 使用 kill -l 命令會顯示Linux支持的訊號列表。其中編號為1 ~ 31的訊號為傳統UNIX支持的訊號,是不可靠訊號(非即時的),編號為32 ~ 63的訊號是後來擴充的,稱做可靠訊號(即時訊號)。

    下面對常用的訊號進行說明:

  • SIGHUP(1)

  • 當使用者終端連線結束時,系統會像所有執行中的行程發出這個訊號;通常在熱載入配置檔時候也會使用該訊號。wget命令就註冊了SIGHUP(1)訊號,這樣就算你結束了Linux登入,wget也能繼續下載檔。同樣的,如Docker/Nginx/LVS等服務也會註冊SIGHUP(1)訊號,實作服務的熱載入配置檔功能。

  • SIGINT(2)

  • 程式終止(interrupt)訊號,在使用者鍵入INTR字元(通常是Ctrl+C)時發出,用於通知前台行程組終止行程。

  • SIGQUIT(3)

  • 和SIGINT類似,但由QUIT字元(通常是Ctrl+反斜杠)來控制。Nginx就是透過註冊這個訊號來實作優雅停止服務的。

  • SIGKILL(9)

  • 立刻結束程式。該訊號不能被阻塞、處理和忽略,不能在程式中被獲取到。

  • SIGTERM(15)

  • 程式結束(Terminate)訊號,又叫請求結束訊號, 與SIGKILL不同的是該訊號可以被阻塞和處理,我們可以透過在程式中註冊該訊號來實作服務的優雅停止。 使用kill命令缺省會發出這個訊號。

  • SIGCHLD(17)

  • 子行程結束時,一般會向父行程發送這個訊號。Nginx是個多行程程式,master行程和worker行程通訊就使用的這個訊號。

    3、Docker容器常見結束碼

    Exit Code 1

  • 程式錯誤,或者Dockerfile中參照不存在的檔,如 entrypoint中參照了錯誤的包

  • 程式錯誤可以很簡單,例如「除以0」,也可以很復雜,比如空參照或者其他程式 crash

  • Exit Code 137

  • 表明容器收到了SIGKILL訊號,行程被殺掉,對應kill -9

  • 引發SIGKILL的是docker kill。這可以由使用者或由docker守護程式來發起,手動執行:docker kill

  • 137 比較常見,如果 pod 中的limit 資源設定較小,會執行記憶體不足導致OOMKilled,此時state 中的」OOMKilled」值為true,你可以在系統的 dmesg 中看到 oom 日誌

  • Exit Code 139

  • 表明容器收到了SIGSEGV訊號,無效的記憶體參照,對應kill -11

  • 一般是程式碼有問題,或者 docker 的基礎映像有問題

  • Exit Code 143

  • 表明容器收到了SIGTERM訊號,終端關閉,對應kill -15

  • 一般對應docker stop 命令

  • 有時docker stop也會導致Exit Code 137。發生在與程式碼無法處理SIGTERM的情況下,docker行程等待十秒鐘然後發出SIGKILL強制結束。

  • 不常用的一些 Exit Code

  • Exit Code 126: 許可權問題或命令不可執行

  • Exit Code 127: Shell指令碼中可能出現錯字且字元無法辨識的情況

  • Exit Code 1 或 255:因為很多程式設計師寫異常結束時習慣用 exit(1) 或 exit(-1),-1 會根據轉換規則轉成 255。這個一般是自訂 code,要看具體邏輯

  • 結束狀態碼的區間

  • 必須在 0-255 之間,0 表示正常結束

  • 外界將程式中斷結束,狀態碼在 129-255

  • 程式自身異常結束,狀態碼一般在 1-128

  • 假如寫程式碼指定的結束狀態碼時不在 0-255 之間,例如: exit(-1),這時會自動做一個轉換,最終呈現的狀態碼還是會在 0-255 之間。我們把狀態碼記為 code,當指定的結束時狀態碼為負數,那麽轉換公式如下:256 – (|code| % 256)

  • 對於被訊號終止的行程,作業系統會將訊號編號加上128 ,作為行程的結束狀態碼。這是因為在Unix系統中,訊號編號的範圍通常是1到31,而行程結束狀態碼的範圍是0到255,為了區分正常的結束狀態碼和訊號終止導致的結束狀態碼,就將訊號編號加上128。

    具體來說:

  • 如果一個行程接收到了訊號編號為9(SIGKILL),那麽它的結束狀態碼就是 128 + 9 = 137。

  • 如果一個行程接收到了訊號編號為15(SIGTERM),那麽它的結束狀態碼就是 128 + 15 = 143。

  • 這種設計保證了在獲取行程結束狀態碼時,可以區分出是正常結束(0到127範圍內)還是訊號終止(128到255範圍內)。在Docker等容器環境中,這種區分特別重要,因為容器的結束狀態碼可以幫助使用者或管理程式了解容器是如何結束的,從而采取相應的處理措施。

    4、Docker容器服務對訊號的支持

    Docker對Linux Signal也做了很多的支持。

    4.1 docker stop命令訊號支持

    當我們用docker stop命令來停掉容器的時候, docker預設會允許容器中的應用程式有10秒的時間用以終止執行 我們可以透過在執行docker stop命令時手動指定--time/-t參數來自訂一個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的行程(main process)發送系統訊號SIGTERM,然後等待容器中的應用程式終止執行,如果等待時間達到設定的超時時間,如預設的10秒,會繼續發送SIGKILL的系統訊號強行kill掉行程。在容器中的應用程式,可以選擇忽略和不處理SIGTERM訊號,不過一旦達到超時時間,程式就會被系統強行kill掉。

    4.2 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訊號,我們可以這樣來實作:

    1

    docker kill --signal=SIGINT container_name

    與docker stop命令不一樣的地方在於,docker kill沒有任何的超時時間設定,它會直接發送SIGKILL訊號,或者使用者指定的其他訊號。

    4.3 docker rm命令訊號支持

    docker rm命令用於刪除已經停止執行的容器,我們可以添加--force或-f參數強行刪除正在執行的容器。使用這個參數後,docker會先給執行中的容器發送SIGKILL訊號,強制停掉容器,然後再做刪除。

    例如,強制刪除正在執行的名稱為web容器。

    1

    docker rm -fv web

    4.4 docker daemon行程對訊號支持( 常用功能

    docker daemon行程會接收SIGHUP訊號,接收後會重新reload daemon.json配置檔。

    我們為dockerd行程發送一個SIGHUP訊號:

    root@vm10-1-1-28:~# kill -SIGHUP $(pidof dockerd)root@vm10-1-1-28:~# 或者root@vm10-1-1-28:~# systemctl reload docker

    檢視docker daemon的日誌可以看到,docker daemon接收這個訊號並重新reload daemon.json配置檔

    root@vm10-1-1-28:~# journalctl -u docker.service -f-- Logs begin at Sun 2018-01-07 09:17:01 CST. --Jan 1816:20:11 vm10-1-1-28.ksc.com dockerd[26668]: time="2018-01-18T16:20:11.262904839+08:00" level=info msg="Got signal to reload configuration, reloading from: /etc/docker/daemon.json"Jan 1816:21:41 vm10-1-1-28.ksc.com systemd[1]: Reloading Docker Application Container Engine.

    所以在你修改完/etc/docker/daemon.json檔後,可以直接給Docker發送一個SIGHUP訊號實作配置檔的reload,而不需要重新開機docker daemon。

    註意:systemctl reload docker 命令通常不會導致機器上的容器重新開機。這個命令的作用是讓 Docker 守護行程重新載入其配置檔,而不會中斷正在執行的容器。它和 systemctl restart docker 是不同的,後者會停止並重新啟動 Docker 服務,從而導致所有容器重新開機。

    5、業務服務容器優雅停止案例

    不論什麽服務,要想實作優雅停止,都是希望在停止前告訴程式,讓程式能有一定的時間處理、保存程式執行現場,優雅的結束程式。下面我們準備了一個通用案例,演示如何在程式中接收並處理TERM訊號。

    透過了解上面Docker容器服務對訊號的支持我們知道,docker kill命令適用於強行終止程式並實作快速停止容器。而如果希望程式能夠gracefully shutdown的話,docker stop才是不二之選,這樣我們可以讓程式在接收到SIGTERM訊號後,有一定的時間處理(預設10秒)、保存程式執行現場,優雅的結束程式。

    接下來我們寫一個簡單的Go程式來實作訊號的接收與處理。程式在啟動過後,會一直阻塞並監聽系統訊號,直到監測到對應的系統訊號後,輸出到控制台並結束執行。

    // main.gopackage mainimport ("fmt""os""os/signal""syscall")funcmain() { fmt.Println("Program started…") ch := make(chan os.Signal, 1)// notify signal SIGTERM(15) signal.Notify(ch, syscall.SIGTERM)// notify signal SIGINT(2) signal.Notify(ch, syscall.SIGINT) s := <-chswitch {case s == syscall.SIGINT: fmt.Println("SIGINT received!")//Do something…case s == syscall.SIGTERM: fmt.Println("SIGTERM received!")//Do something… } fmt.Println("Exiting…")}

    接下來使用交叉編譯的方式來編譯程式,讓程式可以在Linux下執行:

    1

    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o graceful

    編譯好之後。我們做測試:

    1) 測試接收SIGTNT訊號。在前台啟動行程,然後輸入Ctrl + C發送SIGINT(2)訊號。

    zmc@ubuntu ~/r/g/s/edit> ./gracefulProgram started…^CSIGINT received!Exiting…zmc@ubuntu ~/r/g/s/edit>

    2) 測試接收SIGTERM訊號

    zmc@ubuntu ~/r/g/s/edit> ./graceful &Program started…zmc@ubuntu ~/r/g/s/edit> ps -ef | grep gracefulzmc 2122321082015:57 pts/2100:00:00 ./gracefulzmc 2128721082015:57 pts/2100:00:00 grep --color=auto gracefulzmc@ubuntu ~/r/g/s/edit> kill 21223SIGTERM received!Exiting…「./graceful &」 has endedzmc@ubuntu ~/r/g/s/edit>

    3) 將上面程式打包到容器中執行。

    Dockerfile

    1

    2

    3

    FROM alpine:latest

    ADD graceful /graceful

    CMD ["/graceful"]

    在處理SIGTERM訊號常見的一個坑

    我們都知道,透過在Dockerfle中使用CMD、ENTRYPOINT命令可以定義容器啟動命令,關於這兩個命令的區別這裏就不講了,我們只講在使用時候一定要註意的問題。

    這兩個命令都支持下面幾種格式:

  • shell 格式:CMD <命令>

  • exec 格式:CMD ["可執行檔", "參數1", "參數2"...]

  • 參數列格式:CMD ["參數1", "參數2"...]。在指定了 ENTRYPOINT 指令後,用 CMD 指定具體的參數。

  • 一般推薦使用 exec格式,這類格式在解析時會被解析為 JSON 陣列,因此一定要使用雙引號 ",而不要使用單引號'。

    如果使用 shell 格式的話,實際的命令會被包裝為 sh -c 的參數的形式進行執行。比如:

    1

    CMD echo $HOME

    在實際執行中,會將其變更為:

    1

    CMD [ "sh", "-c", "echo $HOME" ]

    因此容器的主行程是sh,當給容器發送訊號,接收訊號的是sh行程,sh行程收到訊號後會直接結束,自然就會令容器結束。我們的程式永遠收不到訊號。

    註意:exec 格式這種寫法避免了 Docker 自動將 CMD 轉換為 sh -c 形式的操作,因為 JSON 陣列格式的 CMD 已經明確指定了要執行的命令路徑或檔。上面範例,docker在容器啟動時會直接執行 /graceful(不包裝任何參數)。

    映像打包過程:

    zmc@ubuntu ~/r/g/s/edit> docker build -t graceful-golang-case:1.0.0 .Sending build context to Docker daemon 1.953 MBStep 1/4 : FROM alpine:latest---> 3fd9065eaf02Step 2/4 : LABEL maintainer "[email protected]"---> Using cache---> 6cc05b3f0ed0Step 3/4 : ADD graceful /graceful---> Using cache---> 4a47b371a124Step 4/4 : CMD /graceful---> Using cache---> f1841c0035afSuccessfully built f1841c0035afzmc@ubuntu ~/r/g/s/edit>

    4) 啟動容器:

    zmc@ubuntu ~/r/g/s/edit> docker run -d --name graceful graceful-golang-case:1.0.008d871007b58e55e9552cff23960c80faf51bf8637014a745dec060b80ac9a6fzmc@ubuntu ~/r/g/s/edit> docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES08d871007b58 graceful-golang-case:1.0.0"/graceful"10 seconds ago Up 9 seconds gracefulzmc@ubuntu ~/r/g/s/edit>

    5) 檢視容器輸出,能看到程式已經正常啟動:

    1

    2

    3

    zmc@ubuntu ~/r/g/s/edit> docker logs graceful

    Program started…

    zmc@ubuntu ~/r/g/s/edit>

    6) 接著我們要使用docker stop看程式能否響應SIGTERM訊號。

    我們都知道 docker stop預設在發出SIGTERM訊號後的10秒鐘,再發送SIGKILL訊號強制停掉容器內所有行程,刪除容器,假如我的程式處理很復雜,10秒內幹不完清理工作,所以我在執行docker stop時自訂讓2分鐘後再強制kill掉我的容器

    zmc@ubuntu ~/r/g/s/edit> docker stop --time=120 gracefulgracefulzmc@ubuntu ~/r/g/s/edit> docker logs gracefulProgram started…SIGTERM received!Exiting…zmc@ubuntu ~/r/g/s/edit>

    檢視上面日誌,我們可以看到,我們程式確實可以對Docker發來的SIGTERM訊號進行處理。

    6、總結

    在Docker中,為了實作容器的優雅結束,確保你的應用程式能夠接收和處理 SIGTERM 訊號是至關重要的。推薦使用 docker stop 命令來停止容器,因為這會發送 SIGTERM 訊號給容器內的行程,允許它們有機會完成未完成的任務並正常結束。相比之下,直接使用 docker rm -f 或 docker kill 可能會強制終止行程,導致數據遺失或不一致的狀態。此外,在 Dockerfile 中設定 ENTRYPOINT 或 CMD 時,應使用 exec 格式,以確保訊號正確傳遞給應用程式。

    Q&A

    Q1:在程式裏註冊了SIGTERM訊號,如何確定是否執行了程式中訊號註冊函式?

    A:可以直接檢視容器執行日誌。

    往期推薦


    點亮,伺服器三年不宕機