目 录CONTENT

文章目录

信号与优雅停机

FatFish1
2025-03-25 / 0 评论 / 0 点赞 / 37 阅读 / 0 字 / 正在检测是否收录...

docker优雅停机原理

Linux的信号

docker环境优雅停机是基于Linux信号实现的,常见的Linux信号包括:

  • SIGHUP(1):当用户终端连接结束时,系统会像所有运行中的进程发出这个信号;通常在热加载配置文件时候也会使用该信号。wget命令就注册了SIGHUP(1)信号,这样就算你退出了Linux登录,wget也能继续下载文件。同样的,如Docker/Nginx/LVS等服务也会注册SIGHUP(1)信号,实现服务的热加载配置文件功能。

  • SIGINT(2):程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl+C)时发出,用于通知前台进程组终止进程。

  • SIGQUIT(3):和SIGINT类似,但由QUIT字符(通常是Ctrl+反斜杠)来控制。Nginx就是通过注册这个信号来实现优雅停止服务的。

  • SIGKILL(9):立刻结束程序。该信号不能被阻塞、处理和忽略,不能在程序中被获取到

  • SIGTERM(15):程序结束(Terminate)信号,又叫请求退出信号,与SIGKILL不同的是该信号可以被阻塞和处理,我们可以通过在程序中注册该信号来实现服务的优雅停止。使用kill命令缺省会发出这个信号

  • SIGCHLD(17):子进程结束时,一般会向父进程发送这个信号。Nginx是个多进程程序,master进程和worker进程通信就使用的这个信号。

通过Linux的kill命令就看可以向JVM发送信号,使用kill -l可以查看所有信号列表,然后执行kill -9 <PID> ,就可以模拟SIGKILL信号发送的过程了

docker stop与退出码

docker命令参考如下链接:

http://www.chymfatfish.cn/archives/dockercommand#docker%E5%91%BD%E4%BB%A4

docker stop命令本质上就是利用了Linux信号实现容器停止,当我们用docker stop命令来停掉容器的时候,docker默认会允许容器中的应用程序有10秒的时间用以终止运行。我们可以通过在执行docker stop --timedocker stop -t参数来自定义一个stop时间长度

在docker stop命令执行的时候,会先向容器中PID为1的进程(main process)发送系统信号SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间,如默认的10秒,会继续发送SIGKILL的系统信号强行kill掉进程。在容器中的应用程序,可以选择忽略和不处理SIGTERM信号,不过一旦达到超时时间,程序就会被系统强行kill掉

因为根据信号做了不同的容器响应,在退出后容器上的退出码也会有不同的展示。docker容器的退出码值=128+信号值(Exit Code 1除外),举例如下:

  • Exit Code 1 :程序错误,pod直接就没起来,例如Dockerfile中引用不存在的文件,或entrypoint.sh中引用了错误的包

  • Exit Code 137:128+9,即响应SIGKILL信号或kill -9的结果,手动docker kill会得到这个退出码,或者pod中limit设置过小,导致pod产生OOMKilled,也会间接产生Exit Code 137退出码

  • Exit Code 139:128+11,即SIGSEGV信号或kill -11,对应无效的内存引用,一般是代码问题或docker基础镜像问题

  • Exit Code 143:128+15,即SIGTERM或kill -15,意味着pod是优雅停机的,比如执行docker stop。但是如果服务无法处理SIGTERM信号,也会让docker stop等待10s无响应后,产生137退出码

  • 其他信号:126是权限问题或容器启动时执行设置的bash命令不可执行,比如docker file中的ENTRYPOINT命令或CMD命令;127是shell脚本中的错字或无法识别的语句;255是1在某些场景下转换过来的,和1是一个意思

其他响应信号的docker命令

其他的docker命令,例如docker kill ,则是直接发送SIGKILL信号,不给任何优雅停机的机会;docker rm 用于删除已经停机的容器,如果不停机,则无法删除,但一旦使用docker rm -f ,则会先发送SIGKILL信号,再删除容器

docker daemon命令比较特殊,它接收SIGHUP信号,接收后会重新reload daemon.json配置文件

例如执行以下命令:

root@vm10-1-1-28:~# kill -SIGHUP $(pidof dockerd)
# 或者
root@vm10-1-1-28:~# systemctl reload docker

查看docker daemon的日志可以看到,docker daemon接收这个信号并重新reload daemon.json配置文件

root@vm10-1-1-28:~# journalctl -u docker.service -f
-- Logs begin at Sun 2018-01-07 09:17:01 CST. --
Jan 18 16:20:11 vm10-1-1-28.ksc.com dockerd[26668]: time="2018-01-18T16:20:11.262904839+08:00" level=info msg="Got signal to reload configuration, reloading from: /etc/docker/daemon.json"
Jan 18 16:21:41 vm10-1-1-28.ksc.com systemd[1]: Reloading Docker Application Container Engine.

所以修改完/etc/docker/daemon.json文件后,可以直接给Docker发送一个SIGHUP信号实现配置文件的reload,而不需要重启docker daemon

而systemctl reload docker 命令通常不会导致机器上的容器重启。这个命令的作用是让 Docker 守护进程重新加载其配置文件,而不会中断正在运行的容器。它和 systemctl restart docker 是不同的,后者会停止并重新启动 Docker 服务,从而导致所有容器重启。

JAVA程序对SIGTERM信号的响应

想要真正优雅退出,需要java服务对SIGTERM信号作出响应

Java 中的 Shutdown Hook 提供了比较好的方案。我们可以通过 Java.Runtime.addShutdownHook(Thread hook) 方法向 JVM 注册关闭钩子,在 JVM 退出之前会自动调用执行钩子方法,做一些结尾操作,从而让进程平滑优雅的退出,保证了业务的完整性。

Spring提供了默认的ShutdownHook实现,可以参考AbstractApplicationContext#registerShutdownHook

public void registerShutdownHook() {
    if (this.shutdownHook == null) {
       // No shutdown hook registered yet.
       this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
          @Override
          public void run() {
             synchronized (startupShutdownMonitor) {
                doClose();
             }
          }
       };
       Runtime.getRuntime().addShutdownHook(this.shutdownHook);
    }
}

该方法设置成员变量this.shutdownHook ,然后在执行该方法的时候调用Runtime.getRuntime().addShutdownHook(this.shutdownHook); 添加进去

那么registerShutdownHook什么时候调用呢?

这个就看使用什么框架了,例如自行搭建基于SpringFramework的e2e框架时,可以使用SpringTestListener的实现:

@Override
public void beforeTestClass(TestContext testContext) throws Exception {
        ApplicationContext applicationContext = testContext.getApplicationContext();
        SpringTestListener.applicationContext = applicationContext;
        ……
}

参考如下链接:

http://www.chymfatfish.cn/archives/e2econstructor#%E5%AE%9E%E7%8E%B0spring%E7%8E%AF%E5%A2%83%E5%90%AF%E5%8A%A8%E5%90%8E%E7%BD%AE%E5%A4%84%E7%90%86%E5%99%A8

再比如华为贡献的CSE框架,则是监听了上下文启动的event,直接就注册进去的:

// org.apache.servicecomb.core.CseApplicationListener#onApplicationEvent
public void onApplicationEvent(ApplicationEvent event) {
  if (initEventClass.isInstance(event)) {
    if (applicationContext instanceof AbstractApplicationContext) {
      ((AbstractApplicationContext) applicationContext).registerShutdownHook();
    }
   ……
  }
}

然后就是看下spring在ShutdownHook里面都做什么,可以看到AbstractApplicationContext#doClose

protected void doClose() {
    // Check whether an actual close attempt is necessary...
    if (this.active.get() && this.closed.compareAndSet(false, true)) {
       ……
       // 发布关闭事件,调用方可以自行监听
       try {
          // Publish shutdown event.
          publishEvent(new ContextClosedEvent(this));
       }
       ……
       // Stop all Lifecycle beans, to avoid delays during individual destruction.
       // 这里停止声明周期bean
       if (this.lifecycleProcessor != null) {
          try {
             this.lifecycleProcessor.onClose();
          }
          catch (Throwable ex) {
             logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
          }
       }
       // Destroy all cached singletons in the context's BeanFactory.
       // 这里关闭所有单例bean
       destroyBeans();
       // Close the state of this context itself.
       // 然后关闭bean工厂
       closeBeanFactory();
       // 抽象方法,给子类自行实现
       onClose();
       // Reset common introspection caches to avoid class reference leaks.
       resetCommonCaches();
       // Reset local application listeners to pre-refresh state.
       if (this.earlyApplicationListeners != null) {
          this.applicationListeners.clear();
          this.applicationListeners.addAll(this.earlyApplicationListeners);
       }
       // Switch to inactive.
       this.active.set(false);
    }
}

k8s优雅停机

k8s在docker的再上层,优雅停机涉及到pod层面和网络层面

网络层面:

  1. Pod 被删除,状态置为 Terminating。

  2. Endpoint Controller 将该 Pod 的 ip 从 Endpoint 对象中删除。

  3. Kube-proxy 根据 Endpoint 对象的改变更新 iptables/ipvs 规则,不再将流量路由到被删除的 Pod。

  4. 如果还有其他 Gateway 依赖 Endpoint 资源变化的,也会改变自己的配置(比如 Nginx Ingress Controller)。

Pod 层面:

  1. Pod 被删除,状态置为 Terminating。

  2. Kubelet 捕获到 ApiServer 中 Pod 状态变化,执行 syncPod 动作。

  3. 如果 Pod 配置了 preStop Hook ,将会执行。

  4. kubelet 对 Pod 中各个 container 发送调用 cri 接口中 StopContainer 方法,向 dockerd 发送 stop -t 指令,用 SIGTERM 信号以通知容器内应用进程开始优雅停止。

  5. 等待容器内应用进程完全停止,如果容器在 gracePeriod 执行时间内还未完全停止,就发送 SIGKILL 信号强制杀死应用进程(容器运行时处理)。

  6. 所有容器进程终止,清理 Pod 资源。

参考文档

  1. https://www.cnblogs.com/zhangmingcheng/p/18254613

  2. https://www.cnblogs.com/luciochn/p/14878160.html

0

评论区