• 容器化服务信号量传递


    背景

    在Kubernetes中,Pod 停止时 kubelet 会先给容器中的主进程也就是PID为1的进程发SIGTERM 信号来通知进程进行shutdown 以实现优雅停止,如果超时进程还未完全停止则会使用SIGKILL 来强行终止。比如,容器内运行一个 Java 程序, 那么信号传递给 JVM 后 Java 相关的框架比如 Spring Boot 等就会检测到此信号, 然后开始执行一些关闭前的清理工作, 这被称之为 “优雅关闭(Graceful shutdown)”。
    但有时我们会遇到一种情况: 业务逻辑处理了 SIGTERM 信号,但 Pod 停止时好像没收到信号导致优雅停止逻辑不生效。比如,我们容器化 Java 应用时没有正确的让信号传递给 JVM, 那么调度程序如 Kubernetes 在等待容器关闭超时以后就会进行强制关闭, 这很可能导致一些 Java 程序无法正常释放资源, 比如数据库连接没有关闭、注册中心没有反注册等。
    通常是因为我们的业务进程是在脚本中启动的,容器的启动入口使用了脚本,所以容器中的主进程并不是我们所希望的业务进程而是 shell 进程,导致业务进程收不到 SIGTERM 信号。

    正确的信号传递

    直接运行方式

    要解决信号传递这个问题其实很简单, 也有很多方法; 比如常见的直接使用 CMD 或 ENTRYPOINT 指令运行 java 程序:

    FROM alpine-oraclejdk8:latest
    COPY target/xxx-0.0.1-SNAPSHOT.jar /
    CMD ["java", "-jar", "/xxx-0.0.1-SNAPSHOT.jar"]
    
    • 1
    • 2
    • 3

    间接 Exec 方式

    在 Dockerfile 里直接运行命令无法解析环境变量,但是有些时候我们又依赖脚本进行变量解析, 这时候我们可以先在脚本内解析完成, 并采用 exec 的方式进行最终执行; 这种方式也可以保证信号传递。在 shell 中启动二进制的命令前加一个 exec 即可让该二进制启动的进程代替当前 shell 进程,即让新启动的进程成为主进程:

    #! /bin/bash
    ...
    
    exec /bin/yourapp # 脚本中执行二进制
    
    • 1
    • 2
    • 3
    • 4

    然后业务进程就可以正常接收所有信号了。

    exec命令解释

    shell的内建命令exec将并不启动新的shell,而是将要被执行命令替换当前的shell进程,并且将老进程的环境清理掉,而且exec命令后的其它命令将不再执行。
    因此,如果你在一个shell里面,执行exec ls;那么,当列出了当前目录后,这个shell就自己退出了,因为这个shell进程已被替换为仅仅执行ls命令的一个进程,执行结束自然也就退出了。为了避免这个影响我们的使用,一般将exec命令放到一个shell脚本里面,用主脚本调用这个脚本,调用点处可以用bash a.sh,(a.sh就是存放该命令的脚本),这样会为a.sh建立一个sub shell去执行,当执行到exec后,该子脚本进程就被替换成了相应的exec的命令。source命令或者”.”,不会为脚本新建shell,而只是将脚本包含的命令在当前shell执行。不过,要注意一个例外,当exec命令来对文件描述符操作的时候,就不会替换shell,而且操作完成后,还会继续执行接下来的命令。

    Bash-c 方式

    除了直接执行和 exec 方式,还有一个是使用bash -c 来执行命令; 在使用 bash -c 执行一些简单命令时, 其行为会跟 exec 很相似, 也会把子进程命令替换到父进程从而让 -c 后的命令直接接受到系统信号; 但需要注意的是, 这种方式不一定百分百成功, 比如当 -c 后面的命令中含有管道、重定向等可能仍会触发 fork, 这时子命令仍然无法完成优雅关闭。

    FROM alpine-oraclejdk8:latest
    
    COPY entrypoint.sh /
    COPY target/xxx-0.0.1-SNAPSHOT.jar /
    CMD ["bash", "-c", "java -jar /xxx-0.0.1-SNAPSHOT.jar"]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    比较完善的方案:使用 init 系统

    前面几种方案实际是用脚本实现了一个极简的 init 系统 (或 supervisor) 来管理所有子进程,只不过它的逻辑很简陋,仅仅简单的透传指定信号给子进程,其实社区有更完善的方案,这两个工具是大部分人都熟知的利器, 甚至连 Docker 本身都集成了; dumb-init 和 tini 都可以作为 init 进程,作为主进程 (PID 1) 在容器中启动,然后它再运行 shell 来执行我们指定的脚本 (shell 作为子进程),shell 中启动的业务进程也成为它的子进程,当它收到信号时会将其传递给所有的子进程,从而也能完美解决 SHELL 无法传递信号问题,并且还有回收僵尸进程的能力。
    这里以tini为例制作镜像,下面是 Dockerfile 示例:

    FROM alpine-oraclejdk8:latest
    COPY entrypoint.sh /
    
    ENTRYPOINT ["/sbin/tini", "--","/entrypoint.sh"]
    
    • 1
    • 2
    • 3
    • 4

    参考博客:https://www.cnblogs.com/bulh/articles/12760617.html

  • 相关阅读:
    0031力扣191题---位1的个数
    MySQL 经验集总结(更新ing)
    项目管理之人力资源管理
    guava常见用法整理(不定期更新)
    线下保薪班开启
    pytorch CV入门4-使用MobileNet解决视觉相关问题
    第3章 docker容器管理
    Django分页功能的使用和自定义分装
    CIPU落地专有云:是“小众需求”还是“机会之门”?
    supOS APP开发者课程练习册
  • 原文地址:https://blog.csdn.net/qq_34939308/article/details/127902534