Docker Graceful Shutdown

kyaa111 1年前 ⋅ 384 阅读

有几个前提

  1. 操作系统层面: 提供了 kill -9 (SIGKILL)和 kill -15(SIGTERM) 两种停机策略. SIGKILL 信号是一个不能被阻塞、处理或忽略的信号,它会立即终止目标进程. SIGTERM 信号是一个可以被阻塞、处理或忽略的信号,它也可以通知目标进程终止,但是它相对于 SIGKILL 信号来说更加温和,目标进程可以在接收到 SIGTERM 信号时进行一些清理操作,例如保存数据、关闭文件、释放资源等,然后再终止进程
  2. 语言层面: 在Java中, Runtime 类的 addShutdownHook 方法注册 shutdown hook. spring-boot已经实现了. 我们只要找个类实现java.io.Closeable接口的close方法, 再将其注册到容器中即可
  3. 在 Docker 中,执行 docker stop 命令时,它会向容器中的主进程 (pid=1)发送 SIGTERM 信号. 如果容器中的进程不响应 SIGTERM 信号,Docker 会等待一定的时间(默认为 10 秒),然后向容器中的所有进程发送 SIGKILL 信号,以强制结束容器中的进程. 如果我们需要修改 SIGTERM 信号等待的时间,可以在 docker run 命令中使用 --stop-timeout 参数来更改默认的停止超时时间(单位: s)

  1. 当使用kill, stop等命令时, 需要发送SIGTERM信号
  2. 你的进程要正常接收到信号
  3. 你的应用要正常处理信号

应用要正常处理信号

SpringBoot已经做了相关处理, 我们只要实现接口即可

实现org.springframework.context.SmartLifecycle接口, 实现getPhase/start/stop/isRunning方法, 通过getPhase方法定义优先级

@Override
public int getPhase() {
    //在 WebServerGracefulShutdownLifecycle 那一组之后
    return SmartLifecycle.DEFAULT_PHASE - 1;
}

进程要正常接收到信号

容器中只有pid=1的进程才能接收到信号, 所以要保证java的pid=1, dockerfile如下

...
ENV JAVA_OPTS=""
ENV APP_OPTS=""
# 如果用这个格式 sh -c start.sh 会导致pid不为1
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar $APP_OPTS /root/user-web.jar"]

这样, 你的java在容器内的pid就是为1了

日志如下

08:59:04.373 [main    ] INFO  com.thy.backend.user.service.user.web.UserWebApplication     : Starting UserWebApplication v1.0-SNAPSHOT using Java 17.0.2 with PID 1 (/root/user-web.jar started by root in /)

还可以使用https://github.com/krallin/tini/, 将java进程当成tini的子进程执行并执行信号转发

执行docker stop xxx

@Override
public void stop() {
    // 输出
    log.info("stop");
}