1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        容器應(yīng)用優(yōu)雅關(guān)閉的終極大招

        共 12636字,需瀏覽 26分鐘

         ·

        2021-08-01 13:08


        概述

        優(yōu)雅關(guān)閉:在關(guān)閉前,執(zhí)行正常的關(guān)閉過程,釋放連接和資源,如我們操作系統(tǒng)執(zhí)行 shutdown。

        目前業(yè)務(wù)系統(tǒng)組件眾多,互相之間調(diào)用關(guān)系也比較復(fù)雜,一個(gè)組件的下線、關(guān)閉會(huì)涉及到多個(gè)組件 對(duì)于任何一個(gè)線上應(yīng)用,如何保證服務(wù)更新部署過程中從應(yīng)用停止到重啟恢復(fù)服務(wù)這個(gè)過程中不影響正常的業(yè)務(wù)請(qǐng)求,這是應(yīng)用開發(fā)運(yùn)維團(tuán)隊(duì)必須要解決的問題。傳統(tǒng)的解決方式是通過將應(yīng)用更新流程劃分為手工摘流量、停應(yīng)用、更新重啟三個(gè)步驟,由人工操作實(shí)現(xiàn)客戶端不對(duì)更新感知。這種方式簡單而有效,但是限制較多:不僅需要使用借助網(wǎng)關(guān)的支持來摘流量,還需要在停應(yīng)用前人工判斷來保證在途請(qǐng)求已經(jīng)處理完畢。

        同時(shí),在應(yīng)用層也有一些保障應(yīng)用優(yōu)雅停機(jī)的機(jī)制,目前 Tomcat、Spring Boot、Dubbo 等框架都有提供相關(guān)的內(nèi)置實(shí)現(xiàn),如 SpringBoot 2.3 內(nèi)置 graceful shutdown 可以很方便的直接實(shí)現(xiàn)優(yōu)雅停機(jī)時(shí)的資源處理,同時(shí)一個(gè)普通的 Java 應(yīng)用也可以基于 Runtime.getRuntime().addShutdownHook()來自定義實(shí)現(xiàn),它們的實(shí)現(xiàn)原理都基本一致,通過等待操作系統(tǒng)發(fā)送的 SIGTERM 信號(hào),然后針對(duì)監(jiān)聽到該信號(hào)做一些處理動(dòng)作。優(yōu)雅停機(jī)是指在停止應(yīng)用時(shí),執(zhí)行的一系列保證應(yīng)用正常關(guān)閉的操作。這些操作往往包括等待已有請(qǐng)求執(zhí)行完成、關(guān)閉線程、關(guān)閉連接和釋放資源等,優(yōu)雅停機(jī)可以避免非正常關(guān)閉程序可能造成數(shù)據(jù)異?;騺G失,應(yīng)用異常等問題。優(yōu)雅停機(jī)本質(zhì)上是 JVM 即將關(guān)閉前執(zhí)行的一些額外的處理代碼。

        現(xiàn)狀分析

        現(xiàn)階段,業(yè)務(wù)容器化后業(yè)務(wù)啟動(dòng)是通過 shell 腳本啟動(dòng)業(yè)務(wù),對(duì)應(yīng)的在容器內(nèi) PID 為 1 的進(jìn)程為 shell 進(jìn)程但 shell 程序不轉(zhuǎn)發(fā) signals,也不響應(yīng)退出信號(hào)。所以在容器應(yīng)用中如果應(yīng)用容器中啟動(dòng) shell,占據(jù)了 pid=1 的位置,那么就無法接收 k8s 發(fā)送的 SIGTERM 信號(hào),只能等超時(shí)后被強(qiáng)行殺死了。

        案例分析

        go 開發(fā)的一個(gè) Demo

        package main

        import (
            "fmt"
            "os"
            "os/signal"
            "syscall"
            "time"
        )

        func main()  {
            c := make(chan os.Signal)
            signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
            go func() {
                for s := range c {
                    switch s {
                    case syscall.SIGINT, syscall.SIGTERM:
                        fmt.Println("退出", s)
                        ExitFunc()
                    default:
                        fmt.Println("other", s)
                    }
                }
            }()

            fmt.Println("進(jìn)程啟動(dòng)...")
            time.Sleep(time.Duration(200000)*time.Second)
        }

        func ExitFunc()  {
            fmt.Println("正在退出...")
            fmt.Println("執(zhí)行清理...")
            fmt.Println("退出完成...")
            os.Exit(0)
        }

        代碼參考:https://www.jianshu.com/p/ae72ad58ecb6

        1、Signal.Notify 會(huì)監(jiān)聽括號(hào)內(nèi)指定的信號(hào),若沒有指定,則監(jiān)聽所有信號(hào)。2、通過 switch 對(duì)監(jiān)聽到信號(hào)進(jìn)行判斷,如果是 SININT 和 SIGTERM 則條用 Exitfunc 函數(shù)執(zhí)行退出。

        SHELL 模式和 CMD 模式帶來的差異性

        編寫應(yīng)用 Dockerfile 文件

        概述 在 Dockerfile 中 CMD 和 ENTRYPOINT 用來啟動(dòng)應(yīng)用,有 shell 模式和 exec 模式,對(duì)應(yīng)的使用 shell 模式,PID 為 1 的進(jìn)程為 shell,使用 exec 模式 PID 為 1 的進(jìn)程為業(yè)務(wù)本身。SHELL 模式

        FROM golang as builder
        WORKDIR /go/
        COPY app.go    .
        RUN go build app.go
        FROM ubuntu
        WORKDIR /root/
        COPY --from=builder /go/app .
        CMD ./app

        構(gòu)建鏡像

        $ docker build -t app:v1.0-shell ./

        運(yùn)行查看

        $ docker exec -it app-shell ps aux
        USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
        root           1  0.7  0.0   2608   548 pts/0    Ss+  03:22   0:00 /bin/sh -c ./
        root           6  0.0  0.0 704368  1684 pts/0    Sl+  03:22   0:00 ./app
        root          24  0.0  0.0   5896  2868 pts/1    Rs+  03:23   0:00 ps aux

        可以看見 PID 為 1 的進(jìn)程是 sh 進(jìn)程

        此時(shí)執(zhí)行 docker stop,業(yè)務(wù)進(jìn)程是接收不到 SIGTERM 信號(hào)的,要等待一個(gè)超時(shí)時(shí)間后被 KILL

        日志沒有輸出 SIGTERM 關(guān)閉指令

        $ docker stop app-shell
        app-shell

        $ docker logs app-shell
        進(jìn)程啟動(dòng)...

        EXEC 模式

        FROM golang as builder
        WORKDIR /go/
        COPY app.go    .
        RUN go build app.go
        FROM ubuntu
        WORKDIR /root/
        COPY --from=builder /go/app .
        CMD ["./app"]

        構(gòu)建鏡像

        $ docker build -t app:v1.0-exec ./

        運(yùn)行查看

        $ docker exec -it app-exec ps aux
        USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
        root           1  2.0  0.0 703472  1772 pts/0    Ssl+ 03:33   0:00 ./app
        root          14  0.0  0.0   5896  2908 pts/1    Rs+  03:34   0:00 ps aux

        可以看見 PID 為 1 的進(jìn)程是應(yīng)用進(jìn)程

        此時(shí)執(zhí)行 docker stop,業(yè)務(wù)進(jìn)程是可以接收 SIGTERM 信號(hào)的,會(huì)優(yōu)雅退出

        $ docker stop app-exec
        app-exec

        $ docker logs app-exec
        進(jìn)程啟動(dòng)...
        退出 terminated
        正在退出...
        執(zhí)行清理...
        退出完成...

        注意:1、以下測試在 ubuntu 做為應(yīng)用啟動(dòng) base 鏡像測試成功,在 alpine 做為應(yīng)用啟動(dòng) base 鏡像時(shí) shell 模式和 exec 模式都一樣,都是應(yīng)用進(jìn)程為 PID 1 的進(jìn)程。

        直接啟動(dòng)應(yīng)用和通過腳本啟動(dòng)區(qū)別

        在實(shí)際生產(chǎn)環(huán)境中,因?yàn)閼?yīng)用啟動(dòng)命令后會(huì)接很多啟動(dòng)參數(shù),所以通常我們會(huì)使用一個(gè)啟動(dòng)腳本來啟動(dòng)應(yīng)用,方便我們啟動(dòng)應(yīng)用。對(duì)應(yīng)的在容器內(nèi) PID 為 1 的進(jìn)程為 shell 進(jìn)程但 shell 程序不轉(zhuǎn)發(fā) signals,也不響應(yīng)退出信號(hào)。所以在容器應(yīng)用中如果應(yīng)用容器中啟動(dòng) shell,占據(jù)了 pid=1 的位置,那么就無法接收 k8s 發(fā)送的 SIGTERM 信號(hào),只能等超時(shí)后被強(qiáng)行殺死了。啟動(dòng)腳本 start.sh

        $ cat > start.sh<< EOF
        #!/bin/sh
        sh -c /root/app
        EOF
        FROM golang as builder
        WORKDIR /go/
        COPY app.go    .
        RUN go build app.go
        FROM alpine
        WORKDIR /root/
        COPY --from=builder /go/app .
        ADD start.sh /root/
        CMD ["/bin/sh","/root/start.sh"]

        構(gòu)建應(yīng)用

        $ docker build -t app:v1.0-script ./

        查看

        $ docker exec -it app-script ps aux
        PID   USER     TIME  COMMAND
        1     root     0:00  /bin/sh /root/start.sh
        6     root     0:00  /root/app
        19    root     0:00  ps aux

        docker stop 關(guān)閉應(yīng)用

        $ docker stop app-script

        是登待超時(shí)后被強(qiáng)行 KILL

        $ docker logs app-script
        進(jìn)程啟動(dòng)...

        容器應(yīng)用優(yōu)雅關(guān)閉方案介紹

        方案介紹

        正常的優(yōu)雅停機(jī)可以簡單的認(rèn)為包括兩個(gè)部分:

        • 應(yīng)用:應(yīng)用自身需要實(shí)現(xiàn)優(yōu)雅停機(jī)的處理邏輯,確保處理中的請(qǐng)求可以繼續(xù)完成,資源得到有效的關(guān)閉釋放,等等。針對(duì)應(yīng)用層,不管是 Java 應(yīng)用還是其他語言編寫的應(yīng)用,其實(shí)現(xiàn)原理基本一致,都提供了類似的監(jiān)聽處理接口,根據(jù)規(guī)范要求實(shí)現(xiàn)即可。
        • 平臺(tái):平臺(tái)層要能夠?qū)?yīng)用從負(fù)載均衡中去掉,確保應(yīng)用不會(huì)再接受到新的請(qǐng)求連接,并且能夠通知到應(yīng)用要進(jìn)行優(yōu)雅停機(jī)處理。在傳統(tǒng)的部署模式下,這部分工作可能需要人工處理,但是在 K8s 容器平臺(tái)中,K8s 的 Pod 刪除默認(rèn)就會(huì)向容器中的主進(jìn)程發(fā)送優(yōu)雅停機(jī)命令,并提供了默認(rèn) 30s 的等待時(shí)長,若優(yōu)雅停機(jī)處理超出 30s 以后就會(huì)強(qiáng)制終止。同時(shí),有些應(yīng)用在容器中部署時(shí),并不是通過容器主進(jìn)程的形式進(jìn)行部署,那么 K8s 也提供了 PreStop 的回調(diào)函數(shù)來在 Pod 停止前進(jìn)行指定處理,可以是一段命令,也可以是一個(gè) HTTP 的請(qǐng)求,從而具備了較強(qiáng)的靈活性。通過以上分析,理論上應(yīng)用容器化部署以后仍然可以很好的支持優(yōu)雅停機(jī),甚至相比于傳統(tǒng)方式實(shí)現(xiàn)了更多的自動(dòng)化操作,本文檔后面會(huì)針對(duì)該方案進(jìn)行詳細(xì)的方案驗(yàn)證。
        • 容器應(yīng)用中第三方 Init:在構(gòu)建應(yīng)用中使用第三方 init 如 tini 或 dumb-init

        方案一:通過 k8s 的 prestop 參數(shù)調(diào)用容器內(nèi)進(jìn)程關(guān)閉腳本,實(shí)現(xiàn)優(yōu)雅關(guān)閉。

        方案二:通過第三方 init 進(jìn)程傳遞 SIGTERM 到進(jìn)程中。

        方案驗(yàn)證

        方案一:通過 k8s Prestop 參數(shù)調(diào)用

        在前面腳本啟動(dòng)的 dockerfile 基礎(chǔ)上,定義一個(gè)優(yōu)雅關(guān)閉的腳本,通過 k8s-prestop 在關(guān)閉 POD 前調(diào)用優(yōu)雅關(guān)閉腳本,實(shí)現(xiàn) pod 優(yōu)雅關(guān)閉。

        啟動(dòng)腳本 start.sh

        $ cat > start.sh<< EOF
        #!/bin/sh
        ./app
        EOF

        stop.sh 優(yōu)雅關(guān)閉腳本

        #!/bin/sh
        ps -ef|grep app|grep -v grep|awk '{print $1}'|xargs kill -15
        FROM golang as builder
        WORKDIR /go/
        COPY app.go    .
        RUN go build app.go
        FROM alpine
        WORKDIR /root/
        COPY --from=builder /go/app .
        ADD start.sh /root/
        CMD ["/bin/sh","/root/start.sh"]

        構(gòu)建鏡像

        $ docker build -t app:v1.0-prestop ./

        通過 yaml 部署到 k8s 中

        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: app-prestop
          labels:
            app: prestop
        spec:
          replicas: 1
          selector:
            matchLabels:
              app: prestop
          template:
            metadata:
              labels:
                app: prestop
            spec:
              containers:
              - name: prestop
                image: 172.16.1.31/library/app:v1.0-prestop
                lifecycle:
                  preStop:
                    exec:
                      command:
                      - sh
                      - /root/stop.sh

        查看 POD 日志,然后刪除 pod 副本

        $ kubectl get pod
        NAME                            READY   STATUS    RESTARTS   AGE
        app-prestop-847f5c4db8-mrbqr    1/1     Running   0          73s

        查看日志

        $ kubectl logs app-prestop-847f5c4db8-mrbqr -f
        進(jìn)程啟動(dòng)...

        另外窗口刪除 POD

        $ kubectl logs app-prestop-847f5c4db8-mrbqr -f
        進(jìn)程啟動(dòng)...


        退出 terminated
        正在退出...
        執(zhí)行清理...
        退出完成...

        可以看見執(zhí)行了 Prestop 腳本進(jìn)行優(yōu)雅關(guān)閉。同樣的可以將 yaml 文件中的 Prestop 腳本取消進(jìn)行對(duì)比測試可以發(fā)現(xiàn)就會(huì)進(jìn)行強(qiáng)制刪除。

        方案二:shell 腳本修改為 exec 執(zhí)行

        修改start.sh腳本

        #!/bin/sh
        exec ./app

        shell 中添加一個(gè) exec 即可讓應(yīng)用進(jìn)程替代當(dāng)前 shell 進(jìn)程,可將 SIGTERM 信號(hào)傳遞到業(yè)務(wù)層,讓業(yè)務(wù)實(shí)現(xiàn)優(yōu)雅關(guān)閉。

        可使用上面例子,進(jìn)行修改測試。

        方案三:通過第三 init 工具啟動(dòng)

        使用 dump-init 或 tini 做為容器的主進(jìn)程,在收到退出信號(hào)的時(shí)候,會(huì)將退出信號(hào)轉(zhuǎn)發(fā)給進(jìn)程組所有進(jìn)程。,主要適用應(yīng)用本身無關(guān)閉信號(hào)處理的場景。docker –init 本身也是集成的 tini。

        FROM golang as builder
        WORKDIR /go/
        COPY app.go    .
        RUN go build app.go
        FROM alpine
        WORKDIR /root/
        COPY --from=builder /go/app .
        ADD start.sh tini /root/
        RUN chmoad a+x start.sh && apk add --no-cache tini
        ENTRYPOINT ["/sbin/tini""--"]
        CMD ["/root/tini""--", /root/start.sh"]

        構(gòu)建鏡像

        $ docker build -t app:v1.0-tini ./

        測試運(yùn)行

        $ docker run -itd --name app-tini app:v1.0-tini

        查看日志

        $ docker logs app-tini

        進(jìn)程啟動(dòng)...

        發(fā)現(xiàn)容器快速停止了,但沒有輸出應(yīng)用關(guān)閉和清理的日志

        后面查閱相關(guān)資料發(fā)現(xiàn)

        使用 tini 或 dump-init 做為應(yīng)用啟動(dòng)的主進(jìn)程。tini 和 dumb-init 會(huì)將關(guān)閉信號(hào)向子進(jìn)程傳遞,但不會(huì)等待子進(jìn)程完全退出后自己在退出。而是傳遞完后直接就退出了。

        相關(guān) issue:https://github.com/krallin/tini/issues/180

        后面又查到另外一個(gè)第三方的組件 smell-baron 能實(shí)現(xiàn)等待子進(jìn)程優(yōu)雅關(guān)閉后在關(guān)閉本身功能。但這個(gè)項(xiàng)目本身熱度不是特別高,并且有很久沒有維護(hù)了。

        FROM golang as builder
        WORKDIR /go/
        COPY app.go    .
        RUN go build app.go
        FROM ubuntu
        WORKDIR /root/
        COPY --from=builder /go/app .
        ADD start.sh /root/
        ADD smell-baron /bin/smell-baron
        RUN chmod a+x /bin/smell-baron  && chmod a+x start.sh
        ENTRYPOINT ["/bin/smell-baron"]
        CMD ["/root/start.sh"]

        構(gòu)建鏡像

        $ docker build -t app:v1.0-smell-baron ./

        測試

        $ docker run -itd --name app-smell-baron app:v1.0-smell-baron

        $ docker stop  app-smell-baron

        進(jìn)程啟動(dòng)...
        退出 terminated
        正在退出...
        執(zhí)行清理...
        退出完成...

        總結(jié):

        1、對(duì)于容器化應(yīng)用啟動(dòng)命令建議使用 EXEC 模式。2、對(duì)于應(yīng)用本身代碼層面已經(jīng)實(shí)現(xiàn)了優(yōu)雅關(guān)閉的業(yè)務(wù),但有 shell 啟動(dòng)腳本,容器化后部署到 k8s 上建議使方案一和方案二。3、對(duì)于應(yīng)用本身代碼層面沒有實(shí)現(xiàn)優(yōu)雅關(guān)閉的業(yè)務(wù),建議使用方案三。

        項(xiàng)目地址:

        • https://github.com/insidewhy/smell-baron[1]
        • https://github.com/Yelp/dumb-init[2]
        • https://github.com/krallin/tini[3]

        腳注

        [1]

        https://github.com/insidewhy/smell-baron: https://github.com/insidewhy/smell-baron

        [2]

        https://github.com/Yelp/dumb-init: https://github.com/Yelp/dumb-init

        [3]

        https://github.com/krallin/tini: https://github.com/krallin/tini


        原文鏈接:https://www.bladewan.com/2021/05/26/graceful_close/


        你可能還喜歡

        點(diǎn)擊下方圖片即可閱讀

        為什么Pod突然就不見了?

        云原生是一種信仰 ??

        關(guān)注公眾號(hào)

        后臺(tái)回復(fù)?k8s?獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!



        點(diǎn)擊 "閱讀原文" 獲取更好的閱讀體驗(yàn)!


        發(fā)現(xiàn)朋友圈變“安靜”了嗎?

        瀏覽 65
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产福利在线不卡 | 熟女嗷嗷叫高潮合集91 | 乱爱性全过程免费视频 | 日本免费黄色电影网站 | 隔壁老王av | 色吧五月天 | 日本a在线观看 | 国产视频999 | 夜夜春精品视频 | 我把老师日出水了 |