Kubernetes pod的SIGTERM问题

这篇文章是ZOZO Advent Calendar 2023系列5的第10天的文章。

Pod被意外地强制关闭了。

我在Kubernetes上运行的Java应用程序的Pod在结束时未能执行预期的终止处理并被强制终止。我想分享一下原因和解决方法。

导致这个情况的原因是什么?

通常,当pod进入Terminating状态时,会向容器的主进程(pid=1)发送SIGTERM信号。
本次事件中,SIGTERM信号从kubelet成功地发送到了Java应用程序容器,但由于后面将提到的原因,SIGTERM信号没有达到Java应用程序。

SIGTERM无法传递给Java应用程序的原因

在相关的Dockerfile中,有关Java进程启动的描述如下。

ENTRYPOINT java -Xmx${JAVA_HEAP_XMX} -Xms${JAVA_HEAP_XMS} -XX:FlightRecorderOptions=stackdepth=256 -javaagent:/dd-java-agent.jar -jar app.jar

这种记法被称为shell形式,实际的命令会在/bin/sh -c中执行。
在正在运行的容器中使用ps命令来确认进程时,它被启动如下。

root@worker-5dc87f4dd7-mzxpm:/# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 07:19 ?        00:00:00 /bin/sh -c java -Xmx${JAVA_HEAP_XMX} -Xms${JAVA_HEAP_XMS} -XX:FlightRecorderOptions=stackdepth=256 -javaagent:/dd-java-agent.jar -jar app.jar
root         7     1 44 07:19 ?        00:00:21 java -Xmx6000m -Xms6000m -XX:FlightRecorderOptions=stackdepth=256 -javaagent:/dd-java-agent.jar -jar app.jar
root        35     0  0 07:19 pts/0    00:00:00 bash
root       351    35  0 07:20 pts/0    00:00:00 ps -ef
root@worker-5dc87f4dd7-mzxpm:/#

我想你可能已经注意到,/bin/sh -c进程作为根进程(pid=1)启动,然后Java进程作为其子进程启动。
在Pod终止时,会发送SIGTERM信号给主进程(pid=1)的/bin/sh进程。
然而,接收到SIGTERM的/bin/sh进程并没有聪明到将信号传递给子进程的处理程序。
如果想要进行传递,就需要使用trap命令等来处理。
因此,如果/bin/sh没有处理信号的话,SIGTERM就会被忽略。
结果就是,Java进程没有收到SIGTERM信号,无法执行结束处理,最终被强制终止了。

解决方案 (jiě jué àn)

用exec形式编写ENTRYPOINT

如果通过exec形式来编写ENTRYPOINT,则java命令会直接执行,因此在Pod结束时,SIGTERM信号会被发送给java进程,而不会使/bin/sh成为根进程。
然而,由于exec形式不会对命令中的变量进行展开,所以在这种情况下,无法从环境变量中获取与堆相关的配置并在命令中使用。
※变量展开是shell的功能,因此如果不通过exec包含shell,则无法进行展开。

将启动命令制作为Shell脚本,并使用trap命令。
将”起动命令变为 shell 脚本然后使用 trap 命令”翻译成中文。

如果要在exec形式中编写并展开变量,可以将启动命令设置为类似于entrypoint.sh的shell脚本,并使用trap命令来处理SIGTERM信号的方式。

#!/bin/sh
signalhandler() {
    kill -TERM ${pid}
    wait ${pid}
}
trap signalhandler SIGTERM
java -Xmx${JAVA_HEAP_XMX} -Xms${JAVA_HEAP_XMS} -XX:FlightRecorderOptions=stackdepth=256 -javaagent:/dd-java-agent.jar -jar app.jar &
pid=$!
wait ${pid}

Dockerfile的内容如下所示。

ENTRYPOINT ["./entrypoint.sh"]

可以使用 Deployment 的 container.lifecycle.preStop。

Kubernetes的容器中存在一种称为PreStop的容器钩子,它在终止处理之前被调用。
由于以shell格式编写时,/bin/sh将成为根进程不可避免,因此可以通过PreStop钩子获取子进程的PID,并直接发送SIGTERM信号来成功停止java进程。
通过在相应的java应用部署中定义以下PreStop钩子,我们成功实现了正常终止java应用。

lifecycle:
  preStop:
    exec:
      command:
        - sh
        - -c
        - kill -TERM $(pidof java)

以上是关于pod终止时的SIGTERM问题的原因和解决方法。
有时候即使被强制终止,也不会出现明确的错误,因此可能会意外地陷入这种状态而不自知。
我建议您确认一下您管理的应用程序是否能够正确地正常结束。

广告
将在 10 秒后关闭
bannerAds