docker容器优雅停机
我们部署在docker中的springboot程序在docker停止的时候并没有执行shutdownHook的操作,正常在本地idea停止springboot服务会看到一系列的shutdownHook操作日志
日志如下:
2021-05-13 11:12:29.253 INFO 218 [,] [SpringContextShutdownHook] o.s.s.c.ThreadPoolTaskScheduler Shutting down ExecutorService 'catalogWatchTaskScheduler'
2021-05-13 11:12:29.261 INFO 96 [,] [Thread-694] c.x.j.core.server.EmbedServer >>>>>>>>>>> xxl-job remoting server stop.
2021-05-13 11:12:29.272 INFO 87 [,] [xxl-job, executor ExecutorRegistryThread] c.x.j.c.t.ExecutorRegistryThread >>>>>>>>>>> xxl-job registry-remove success, registryParam:RegistryParam{registryGroup='EXECUTOR', registryKey='shop', registryValue='http://192.168.1.88:9999/'}, registryResult:ReturnT [code=200, msg=null, content=null]
2021-05-13 11:12:29.272 INFO 105 [,] [xxl-job, executor ExecutorRegistryThread] c.x.j.c.t.ExecutorRegistryThread >>>>>>>>>>> xxl-job, executor registry thread destory.
2021-05-13 11:12:29.272 INFO 125 [,] [SpringContextShutdownHook] c.x.j.core.server.EmbedServer >>>>>>>>>>> xxl-job remoting server destroy success.
2021-05-13 11:12:29.273 INFO 99 [,] [xxl-job, executor JobLogFileCleanThread] c.x.j.c.t.JobLogFileCleanThread >>>>>>>>>>> xxl-job, executor JobLogFileCleanThread thread destory.
2021-05-13 11:12:29.273 INFO 97 [,] [xxl-job, executor TriggerCallbackThread] c.x.j.c.t.TriggerCallbackThread >>>>>>>>>>> xxl-job, executor callback thread destory.
2021-05-13 11:12:29.273 INFO 127 [,] [Thread-693] c.x.j.c.t.TriggerCallbackThread >>>>>>>>>>> xxl-job, executor retry callback thread destory.
2021-05-13 11:12:29.361 INFO 94 [,] [SpringContextShutdownHook] o.s.c.c.s.ConsulServiceRegistry Deregistering service with consul: shop-192-168-1-88-8883
2021-05-13 11:12:29.379 INFO 218 [,] [SpringContextShutdownHook] o.s.s.c.ThreadPoolTaskExecutor Shutting down ExecutorService
2021-05-13 11:12:29.379 INFO 350 [,] [SpringContextShutdownHook] c.z.hikari.HikariDataSource HikariCP - Shutdown initiated...
2021-05-13 11:12:29.387 INFO 352 [,] [SpringContextShutdownHook] c.z.hikari.HikariDataSource HikariCP - Shutdown completed.
启动一个docker容器来验证问题
编写一个Dockerfile
FROM openjdk:8-jre-alpine
COPY ./build/libs/app.jar /app/app.jar
COPY ./docker/bootapp.sh /app/bootapp.sh
WORKDIR /app
EXPOSE 8080
ENTRYPOINT ["./bootapp.sh"]
创建启动脚本bootapp.sh
#!/bin/sh
echo 'Do something'
java -jar app.jar
docker stop 做了什么
docker官网对docker stop的描述:
The main process inside the container will receive SIGTERM
, and after a grace period, SIGKILL
. The first signal can be changed with the STOPSIGNAL
instruction in the container’s Dockerfile, or the --stop-signal
option to docker run
.
在docker stop
命令执行的时候,会先向容器中PID为1的进程发送系统信号SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间,或者默认的10秒,会继续发送SIGKILL的系统信号强行kill掉进程。在容器中的应用程序,可以选择忽略和不处理SIGTERM信号,不过一旦达到超时时间,程序就会被系统强行kill掉,因为SIGKILL信号是直接发往系统内核的,应用程序没有机会去处理它。在使用docker stop
命令的时候,我们唯一能控制的是超时时间,比如设置为20秒超时:
当我们在 shell
中给进程发送 SIGTERM
和 SIGINT
信号的时候,这些进程往往都能正确的处理。但是在 docker
中却不灵了。这是因为在 docker
中,只会将 SIGTERM
等所有的 signal
信号发送给 PID 为 1 的进程,当我们 docker
中运行的进程的PID不是 1 时,就不会收到这样的信号。
如何优雅的关闭容器
通过前面的内容,我们已经了解了容器没有被优雅关闭的原因和可能导致的问题,接下来,我们来看看如何解决。
使目标进程成为 PID 1
既然 docker
只会将 sigal
发送给 PID 1 的进程,那就让我们的进程成为 PID 1 的进程就好了。
docker 的 exec 与 shell 模式
Dockerfile
的 ENTRYPOINT
有两种写法,即 exec
和 shell
:
# exec form
ENTRYPOINT ["command", "param"]
# shell form
ENTRYPOINT command param
两者的区别在于:
exec
形式的命令会使用 PID 1 的进程shell
形式的命令会被执行为/bin/sh -c <command>
,PID为1的进程是“/bin/sh”,command不会执行在 PID 1 上,也就不会收到signal
所以,我们应该选择 exec
模式,让我们的程序成为 PID 1 进程。
ENTRYPOINT ["java", "-jar", "app.jar"]
exec 命令
exec
形式的 ENTRYPOINT
只能解决 无需任何准备工作就启动进程 的场景,而不能解决一些需要准备工作的复杂场景。
在这样的场景中,我们的 ENTRYPOINT
往往需要执行一个 shell
脚本:
ENTRYPOINT ["./bootapp.sh"]
然后在这个脚本中执行我们的准备工作,完成后再启动真正的进程。
比如上面的例子,做完准备后,启动 java
进程。
这时候,我们的 java
进程就无法成为 PID 1 进程。
我们可以看到,java
进程的 PID 是 45,也就无法优雅退出了。
为了解决这个问题,我们可以使用 exec
命令来解决。这个命令的作用就是使用新的进程替代原有的进程,并保持 PID 不变。
这就意味着我们可以在执行 java
命令的时候使用它,从而替换掉 PID 1 的 shell 脚本:
#!/bin/sh
echo "Do something"
exec java -jar app.jar
shell的内建命令exec将并不启动新的shell,而是用要被执行命令替换当前的shell进程,并且将老进程的环境清理掉,而且exec命令后的其它命令将不再执行。
我们再来看一下容器中的进程:
使用 exec
命令之后,我们无论是使用 ctrl+c
还是 docker stop
都能让进程接收到信号,执行相应的操作后退出:
日志表明,java
进程收到了 docker stop
发送的 SIGTERM
信号,并且正确的触发了相关操作,最后退出程序。