docker容器優雅停止


由於應用鏡像是由spring boot制作的,在關注docker 容器停止之前,先看下Java應用是如何處理程序停止的。

java shutdownhook

在java程序停止前,我們可能會需要一些清理工作,如關閉數據庫連接池,執行一些反注冊等。Runtime的addShutdownHook方法給我們提供了這樣一個機制,通過這個方法,我們可以告訴JVM,在收到停止信號時,執行一些我們自定義的邏輯

 /**
     * Registers a new virtual-machine shutdown hook.
     *
     * <p> The Java virtual machine <i>shuts down</i> in response to two kinds
     * of events:
     *
     *   <ul>
     *
     *   <li> The program <i>exits</i> normally, when the last non-daemon
     *   thread exits or when the <tt>{@link #exit exit}</tt> (equivalently,
     *   {@link System#exit(int) System.exit}) method is invoked, or
     *
     *   <li> The virtual machine is <i>terminated</i> in response to a
     *   user interrupt, such as typing <tt>^C</tt>, or a system-wide event,
     *   such as user logoff or system shutdown.
     *
     *   </ul>
     *
     * <p> A <i>shutdown hook</i> is simply an initialized but unstarted
     * thread.  When the virtual machine begins its shutdown sequence it will
     * start all registered shutdown hooks in some unspecified order and let
     * them run concurrently.  When all the hooks have finished it will then
     * run all uninvoked finalizers if finalization-on-exit has been enabled.
     * Finally, the virtual machine will halt.  Note that daemon threads will
     * continue to run during the shutdown sequence, as will non-daemon threads
     * if shutdown was initiated by invoking the <tt>{@link #exit exit}</tt>
     * method.
     * 
     * <p> In rare circumstances the virtual machine may <i>abort</i>, that is,
     * stop running without shutting down cleanly.  This occurs when the
     * virtual machine is terminated externally, for example with the
     * <tt>SIGKILL</tt> signal on Unix or the <tt>TerminateProcess</tt> call on
     * Microsoft Windows.  The virtual machine may also abort if a native
     * method goes awry by, for example, corrupting internal data structures or
     * attempting to access nonexistent memory.  If the virtual machine aborts
     * then no guarantee can be made about whether or not any shutdown hooks
     * will be run. <p>
     * 
     * @see #removeShutdownHook
     * @see #halt(int)
     * @see #exit(int)
     * @since 1.3
     */
     public void addShutdownHook(Thread hook)
  • 此方法在程序正常終止或者jvm收到中斷interrupt、停止信號 terminate時被觸發
  • 一個程序可以注冊多個shutdown hook,當JVM開始停止時,這些shutdown hooks會同時執行,相互之間沒有次序
序號 執行命令 結果 說明
1 kill -9 不能觸發 發送的是SIGKILL
2 kill 觸發 默認的是kill -15 發送SIGERM
3 ctrl+c 觸發 發送的是SIGINT
4 正常結束 觸發
5 oom 觸發

docker 容器內部

將java程序做成docker鏡像,以容器形式執行時,我們不能直接給容器內部的java進程發送信號,此時只能通過docker命令來操作正在運行的容器。根據docker stop命令的描述,

The main process inside the container will receive SIGTERM, and after a grace period, SIGKILL.

docker只會給容器內的主進程發送信號,所以為了使java進程能收到停止信號,觸發shutdown hooks,java 進程在容器內只能作為主進程(1號進程)運行。

可以通過以下方式讓Java進程作為主進程在容器中運行。

  1. 在Dockerfile中通過 CMD作為容器啟動的默認命令,如:

    FROM openjdk:8u212-jdk-alpine
    ADD ***.jar /home
    
    
    WORKDIR /home
    
    CMD java -jar ***.jar
     
    
  2. 在Dockerfile中用exec格式的ENTRYPOINT作為容器啟動的默認命令,在ENTRYPOINT對應的腳本內部,用exec啟動java程序,如:

    Dockerfile:

    FROM openjdk:8u212-jdk-alpine
    
    ADD spring-boot-shutdownhook-1.0-SNAPSHOT.jar /home
    
    COPY docker-entrypoint.sh /home/docker-entrypoint.sh
    WORKDIR /home
    
    RUN chmod +x docker-entrypoint.sh
    ENTRYPOINT ["./docker-entrypoint.sh"]
    
    

    docker-entrypoint.sh:

    #!/bin/sh
    
    #do something
    
    exec java -jar spring-boot-shutdownhook-1.0-SNAPSHOT.jar
    
    
    • linux exec 命令的意思是在當前進程內執行,並且exec命令后面的 指令就不在執行了
    • ENTRYPOINT command param1 param2ENTRYPOINT ["/bin/sh", "param1"] 都是shell模式,pid 為1的進程都是shell,不能使Java進程收到停止信號
序號 執行命令 結果 說明
1. docker rm -f 不能觸發 直接發送SIGKILL
2. docker stop 觸發
3. docker stack rm 觸發
4. docker service rm 觸發
5. docker service scale 觸發 縮減實例個數的情況下
6. docker service update 觸發 造成實例停止的更新

kubernetes pod Termination of Pods

  1. If one of the Pod's containers has defined a preStop hook, the kubelet runs that hook inside of the container. If the preStop hook is still running after the grace period expires, the kubelet requests a small, one-off grace period extension of 2 seconds.

    Note: If the preStop hook needs longer to complete than the default grace period allows, you must modify terminationGracePeriodSeconds to suit this.

  2. The kubelet triggers the container runtime to send a TERM signal to process 1 inside each container.

    Note: The containers in the Pod receive the TERM signal at different times and in an arbitrary order. If the order of shutdowns matters, consider using a preStop hook to synchronize.

  • 只有容器中的1號進程能收到SIGTERM信號,所以為了在k8s環境下,使Java進程執行shutdown hooks,需保證在容器中的Java進程是主進程

  • 在k8s環境下,還可以通過preStop這個hook來在主進程收到TERM之前做一些事情,如果我們的Java進程在容器中不是主進程,在k8s環境下,我們可以通過如下的preStop來觸發Java進程的shutdown hook

    
    ***
    containers:
    - image: myimage:test
      lifecycle:
        preStop:
          exec:
            command: ["/bin/sh","-c","ps|grep java|grep -v grep| awk '{ print $1 }' | xargs -I{} kill {}]
    ***
    
    
    • 在preStop這個hook中,通過kill ${java 進程PID (kill 默認發送 15 SIGTERM 信號)

補充

  1. 當Java進程在容器中是1號進程時,雖然能收到SIGTERM信號,自動執行shutdown hooks,但是,利用 jmap、jstack等工具對1號進程(Java進程)進行分析時,會出現如下錯誤

    chengaofeng@chengaofeng target % docker exec -it b6a45781b81f sh
    	
    /home # ps
    PID   USER     TIME  COMMAND
        1 root      0:22 java -jar spring-boot-shutdownhook-1.0-SNAPSHOT.jar
       37 root      0:00 sh
       42 root      0:00 ps
    /home # jmap -dump:format=b,file=dump.bin 1
    1: Unable to get pid of LinuxThreads manager thread
    
    
  2. 如果想讓Java進程既不是1號進程,也要能收到信號,可以利用tini來實現 ,通過讓tini運行在1號進程,Java作為tini的子進程來實現

    Dockerfile:

    FROM openjdk:8u212-jdk-alpine
    
    ADD spring-boot-shutdownhook-1.0-SNAPSHOT.jar /home
    
    COPY docker-entrypoint.sh /home/docker-entrypoint.sh
    WORKDIR /home
    
    RUN chmod +x docker-entrypoint.sh
    RUN apk add --no-cache tini
    ENTRYPOINT ["/sbin/tini", "--","./docker-entrypoint.sh"]
    
    

    docker-entrypoint.sh:

    #!/bin/sh
    
    echo "hello"
    exec java -jar spring-boot-shutdownhook-1.0-SNAPSHOT.jar
    
    

    啟動后進入容器

    chengaofeng@chengaofeng target % docker exec -it 7b04dd056973 sh
    /home # ps
    PID   USER     TIME  COMMAND
        1 root      0:00 /sbin/tini -- ./docker-entrypoint.sh
        7 root      0:21 java -jar spring-boot-shutdownhook-1.0-SNAPSHOT.jar
       37 root      0:00 sh
       43 root      0:00 ps
    /home # jstack 7
    2020-10-21 03:31:36
    Full thread dump OpenJDK 64-Bit Server VM (25.212-b04 mixed mode):
    
    "Attach Listener" #30 daemon prio=9 os_prio=0 tid=0x000056519338b800 nid=0x38 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    

    執行docker stop 命令停止容器對應的日志

    2020-10-21 03:34:16.845  INFO 7 --- [       Thread-0] o.example.shutdownhook.ShutdownHookApp   : app shutdown hook executed
    
    

    對應的代碼

    @SpringBootApplication
    @Slf4j
    public class ShutdownHookApp {
    
        public static void main(String[] args) {
            Runtime.getRuntime().addShutdownHook(new Thread(()->{
    
               log.info("app shutdown hook executed");
            }));
    
            SpringApplication.run(ShutdownHookApp.class, args);
        }
    }
    
    • 需要讓Java進程時tini的直接子進程

總結

使容器內Java進程能收到停止信號有以下三種方式

  1. 通過 CMD java -jar 直接運行
  2. 以exec格式啟動 ENTRYPOINT,在 ENTRYPOINT對應的腳本中,以 exec java -jar 形式啟動java進程
  3. 以 exec 形式啟動ENTRYPOINT,command用tini,在 ENTRYPOINT對應的腳本中,以 exec java -jar 形式啟動java進程
  • 其中前兩種都是讓Java進程作為一號進程運行,第三種以tini作為一號進程,Java作為tini的子進程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM