由於應用鏡像是由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進程作為主進程在容器中運行。
-
在Dockerfile中通過
CMD
作為容器啟動的默認命令,如:FROM openjdk:8u212-jdk-alpine ADD ***.jar /home WORKDIR /home CMD java -jar ***.jar
-
在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 param2
和ENTRYPOINT ["/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
-
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.
-
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 信號)
- 在preStop這個hook中,通過
補充
-
當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
-
如果想讓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進程能收到停止信號有以下三種方式
- 通過
CMD java -jar
直接運行 - 以exec格式啟動
ENTRYPOINT
,在ENTRYPOINT
對應的腳本中,以exec java -jar
形式啟動java進程 - 以 exec 形式啟動ENTRYPOINT,command用
tini
,在ENTRYPOINT
對應的腳本中,以exec java -jar
形式啟動java進程
- 其中前兩種都是讓Java進程作為一號進程運行,第三種以tini作為一號進程,Java作為tini的子進程