背景

随着云原生技术的流行,越来越多的应用选择容器化,容器化的话题自然离不开 Kubernetes 。Pod 是 Kubernetes 中创建和管理的、最小的可部署的计算单元,一个 Pod 中有多个容器,容器是一组进程的集合。当然,容器本质上是 Linux 的 Namespace 和 Ggroups 技术的应用,Namespace 负责资源隔离,Cgroups 负责资源限制。

在使用 Kubernetes 部署应用的过程中,是否有产生这样的疑问:为什么有的pod删除很快,有的pod删除要等很久?容器退出时,以为进程会收到 SIGTERM 信号做优雅退出,结果反而被 SIGKILL 给杀死了?这也是本文想和大家探讨的几个问题。

环境

Ubuntu 20.04.2、Kernel 5.4.0-73-generic 、Kubernetes 1.20.7

实验

实验代码如下:

main.go

package main

import (
"fmt"
"os"
"os/signal"
"syscall"
) func main() {
fmt.Println("app running...")
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGTERM)
sig := <-sc
fmt.Printf("接收到信号[%s]\n", sig.String())
switch sig {
case syscall.SIGTERM:
// 释放资源
fmt.Println("优雅退出")
}
}

编译生成二进制用于下面例子中的Dockerfile

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o demoapp main.go

start.sh

#!/bin/sh
echo "do something before start"
./demoapp

Dockerfile

FROM alpine
WORKDIR /app
COPY demoapp start.sh ./
RUN chmod +x ./start.sh
CMD ["./start.sh"]

pod.yaml

apiVersion: v1
kind: Pod
metadata:
name: demoapp
spec:
containers:
- name: demoapp
imagePullPolicy: IfNotPresent
image: demoapp:v1

执行下面的命令

# 打包镜像
$ docker build -t demoapp:v1 -f Dockerfile . # 创建pod
$ kubectl apply -f pod.yaml # 查看容器里的进程
$ kubectl exec -it demoapp -c demoapp -- ps aux
PID USER TIME COMMAND
1 root 0:00 {start.sh} /bin/sh ./start.sh
8 root 0:00 ./demoapp
35 root 0:00 ps aux $ kubectl exec -it demoapp -c demoapp -- pstree
start.sh---demoapp---6*[{demoapp}]

从上面可以看到start.sh是容器里的init进程(1号进程),dempapp是它的子进程。查看容器的实时日志

$ kubectl logs -f demoapp -c demoapp
do something before start
app running...

执行下面的删除pod命令

kubectl delete pod demoapp

pod 的状态变成 Terminating,并持续了30s左右pod才真正消失,同时容器日志并没有看到"优雅退出"的输出,证明demoapp进程确实没收到 SIGTERM 信号。到底哪里出了问题?

于是在删除pod的过程,观察容器里的 init 进程(1号进程)和子进程 demoapp 到底收到了什么信号。在容器里面,我们看到是容器所在的 PID Namespace 下的进程PID,PID编号从1开始。而在宿主机上的 Host PID Namespace,它是其他 Namespace 的父亲 Namespace,可以看到在这台机器上的所有进程,不过进程 PID 编号不是容器所在 PID Namespace 里的编号了,而是把所有在宿主机运行的进程放在一起,再进行编号。在宿主机中我们通过 ps 命令找出它们并用 strace 工具观察它们收到的信号

$ ps -ef | grep start.sh
root 72433 72412 0 20:36 ? 00:00:00 /bin/sh ./start.sh
chen 74415 43973 0 20:40 pts/4 00:00:00 grep --color=auto start.sh $ ps -ef | grep demoapp
root 72463 72433 0 20:36 ? 00:00:00 ./demoapp
chen 74492 43973 0 20:40 pts/4 00:00:00 grep --color=auto demoapp $ strace -p 72433
strace: Process 72433 attached
wait4(-1, 0x7ffc512a367c, 0, NULL) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=0, si_uid=0} ---
wait4(-1, <unfinished ...>) = ?
+++ killed by SIGKILL +++ $ strace -p 72463
strace: Process 72463 attached
futex(0x554bd0, FUTEX_WAIT_PRIVATE, 0, NULL) = ?
+++ killed by SIGKILL +++

执行删除 pod 那一刻,容器的init进程 start.sh 先收到了 SIGTERM ,过了30s收到 SIGKILL ,紧接着demoapp进程收到了SIGKILL。

从上面的结果来看,有两大疑问:

  • 为什么容器里的init进程收到 SIGTERM 信号不响应,而是等到30s后收到 SIGKILL 信号才被杀掉呢,信号是谁发给它的?
  • 为什么容器里的demoapp进程收到是 SIGKILL 信号,信号是谁发给它的?

为了回答第一个疑问,重做实验并进入容器查看 1 号进程状态中 SigCgt Bitmap(在Host PID Namespace下查看也是一样的)

$ kubectl exec -it demoapp -c demoapp -- /bin/sh

$ cat /proc/1/status |grep -i SigCgt
SigCgt: 0000000000010002

上面的十六进制转换成二进制是 1 0000 0000 0000 0010 ,可以看到 start.sh 注册了两个信号handler,bit 2和bit 17,也就是 SIGNIT(2) 和 SIGCHLD(17),但是没有注册 SIGTERM(15)。

进程对每种信号的处理,包括三个选择: 调用系统缺省行为、捕获、忽略。两个特权信号 SIGKILL 和 SIGSTOP不能被捕获和忽略。

缺省就是如果我们在代码中对某个信号,比如 SIGTERM 信号,不做任何 signal() 相关的系统调用,那么在进程运行的时候,如果接收到信号 SIGTERM,进程就会执行内核中 SIGTERM 信号的缺省代码。对于 SIGTERM 这个信号来说,它的缺省行为就是进程退出(terminate)。在Linux下可以通过 man 7 signal 查看每个信号的缺省行为。

捕获指的就是我们在代码中为某个信号,调用 signal() 注册自己的 handler。这样进程在运行的时候,一旦接收到信号,就不会再去执行内核中的缺省代码,而是会执行通过 signal() 注册的 handler。

忽略就是通过 signal() 这个系统调用,为这个信号注册一个特殊的 handler,也就是 SIG_IGN。在程序运行的时候,如果收到 SIGTERM 信号,什么反应也没有,就像完全没有收到这个信号一样。

注意的是: D state (uninterruptible) 进程还有 Zombie 进程都是不能接受任何信号的。

回答这两个疑问:

容器的 init 进程start.sh收到的 SIGTERM 是 containerd 调用runc发送给它的,它收到后因为没有注册 SIGTERM 的handler,按道理是缺省行为(terminate)。

不使用容器,直接在宿主机执行 start.sh,进程起来后执行 kill ${pid} 命令发送SIGTERM给 start.sh 进程,start.sh进程收到后直接退出,demoapp变成孤儿进程被1号进程收养。

使用容器后,在容器外面(host namespace下)或者进入容器里面发送SIGTERM信号给容器的 init 进程都没有响应,是因为在linux内核代码中有对init进程的的保护逻辑,毕竟init进程随便就能杀死的话会让系统混乱和难以调试。

没有注册SIGTERM信号的handler所以没有响应,containerd 过了30秒(时间是由pod.spec.terminationGracePeriodSeconds 这个字段决定,默认是30),发送 SIGKILL 给 init 进程,init 进程退出是do_exit(),退出的时候同样给子进程 demoapp 发送了 SIGKILL 而不是 SIGTERM。哪怕 demoapp 的代码里注册了 SIGTERM 的handler,也没有机会使用。

疑问2的补充资料:

Linux 内核对处理进程退出的入口点就是 do_exit() 函数,do_exit() 函数中会释放进程的相关资源,比如内存,文件句柄,信号量等等。

在做完这些工作之后,它会调用一个 exit_notify() 函数,用来通知和这个进程相关的父子进程等。

对于容器来说,还要考虑 Pid Namespace 里的其他进程。这里调用的就是 zap_pid_ns_processes() 这个函数,而在这个函数中,如果是处于退出状态的 init 进程,它会向 Namespace 中的其他进程都发送一个 SIGKILL 信号。

看完上面,怎么让demoapp进程收到SIGTERM,最简单的方案就是demoapp成为容器里的 init 进程。

方法一

修改start.sh

#!/bin/sh
echo "do something before start"
exec ./demoapp

和原来的 start.sh 相比,多了 exec 。exec是以新的进程去代替原来的进程,但进程的PID保持不变。可以这样认为,exec系统调用并没有创建新的进程,只是替换了原来进程上下文的内容。

按照上面的步骤重新打包成 demoapp:v2 镜像,并修改 pod.yaml 中的image,重新创建pod,进入容器查看,可以看到 1 号进程不再是 start.sh,而是demoapp。

$ kubectl exec -it demoapp -c demoapp -- ps aux
PID USER TIME COMMAND
1 root 0:00 ./demoapp
13 root 0:00 ps aux

删除pod的时候根据下面容器的日志确实看到了优雅退出。

$ kubectl logs -f demoapp -c demoapp
do something before start
app running...
接收到信号[terminated]
优雅退出

因为 demeapp 进程能响应 SIGTERM 并快速退出,pod 在 Terminating 状态持续很短的时间就消失了,根本不用等待30s给pod里面的容器的init进程发送 SIGKILL。

见过有人在容器里面用shell脚本去启动supervisord,然后supervisord去管理应用进程,如果实在非要这么做,可以用exec命令让supervisord成为容器里的init进程,supervisord会转发信号给子进程,原因下面有说到。

方法二(推荐)

除了修改 start.sh 让 demoapp 变成容器里的 init 进程以外,如果方便的话把start.sh里面的准备工作放进代码里,去掉start.sh,直接启动demoapp,这样生来就是init进程了。

Dockerfile

FROM alpine
WORKDIR /app
COPY demoapp ./
CMD ["./demoapp"]

方案三

上面的两种方法是让 demoapp 成为容器的 init 进程从而收到 SIGTERM,不成为 init 进程能不能收到 SIGTERM 呢?其实是可以的,init 进程转发收到的信号给子进程。这里使用 tini 作为容器的 init 进程。tini 的代码中就会调用 sigtimedwait() 这个函数来查看自己收到的信号,然后调用 kill() 把信号发给子进程。

tini安装参考: https://github.com/krallin/tini

tinti的用法如下

# tini -h
tini (tini version 0.19.0)
Usage: tini [OPTIONS] PROGRAM -- [ARGS] | --version Execute a program under the supervision of a valid init process (tini) Command line options: --version: Show version and exit.
-h: Show this help message and exit.
-s: Register as a process subreaper (requires Linux >= 3.4).
-p SIGNAL: Trigger SIGNAL when parent dies, e.g. "-p SIGKILL".
-v: Generate more verbose output. Repeat up to 3 times.
-w: Print a warning when processes are getting reaped.
-g: Send signals to the child's process group.
-e EXIT_CODE: Remap EXIT_CODE (from 0 to 255) to 0.
-l: Show license and exit. Environment variables: TINI_SUBREAPER: Register as a process subreaper (requires Linux >= 3.4).
TINI_VERBOSITY: Set the verbosity level (default: 1).
TINI_KILL_PROCESS_GROUP: Send signals to the child's process group.

start.sh

#!/bin/sh
echo "do something before start"
./demoapp

Dockerfile

FROM alpine
WORKDIR /app
RUN apk add --no-cache tini
COPY demoapp start.sh ./
RUN chmod +x ./start.sh
ENTRYPOINT ["tini", "--"]
CMD ["./start.sh"]

重新打包创建pod,容器里面是这样的

# kubectl exec -it demoapp -c demoapp -- ps aux
PID USER TIME COMMAND
1 root 0:00 tini -- ./start.sh
7 root 0:00 {start.sh} /bin/sh ./start.sh
8 root 0:00 ./demoapp
15 root 0:00 ps aux # kubectl exec -it demoapp -c demoapp -- pstree
tini---start.sh---demoapp---6*[{demoapp}]

执行删除pod命令,用 strace 工具发现 start.sh 进程收到了 SIGTERM,demoapp 收到的却是 SIGKILL,到底哪里出了问题?

不难发现,原因就是 tini 把 SIGTERM 转发给它的子进程 start.sh,而 demoapp 是 start.sh 的子进程。tini 没把信号转发给 demoapp,start.sh 则是没能力把收到的 SIGTERM 转发给它的子进程 demoapp。那怎么办?一种做法就是在start.sh里面使用exec,demoapp 直接变成了 tini 的子进程。那不想改start.sh怎么办?还记得 tini 有一个 -g 的的参数吗?

-g: Send signals to the child's process group.

字面意思是发送信号到子进程所属的进程组,也就是发送信号到 start.sh 所属的进程组。

在宿主机查看 tini、start.sh、demoapp 这几个进程所属的进程组。start.sh 和 demoapp 同属一个进程组,进程组id正是 start.sh 进程的 pid。tini 属另一个进程组,进程组id是它本身的pid。

再次修改Dockerfile如下

FROM alpine
WORKDIR /app
RUN apk add --no-cache tini
COPY demoapp start.sh ./
RUN chmod +x ./start.sh
ENTRYPOINT ["tini", "-g", "--"]
CMD ["./start.sh"]

重新打包创建pod,容器里面的 tini 进程多了 -g 的参数

$ kubectl exec -it demoapp -c demoapp -- ps aux
PID USER TIME COMMAND
1 root 0:00 tini -g -- ./start.sh
6 root 0:00 {start.sh} /bin/sh ./start.sh
7 root 0:00 ./demoapp
20 root 0:00 ps aux

执行删除pod命令,这次同属一个进程组的 start.sh 和 demoapp 进程都收到了 SIGTERM。

k8s在删除pod时优雅关闭sigterm信号传输失败的更多相关文章

  1. k8s中删除pod后仍然存在问题

    分析: 是因为删除了pod,但是没有删除对应的deployment,删除对应的deployment即可 实例如下: 删除pod [root@test2 ~]# kubectl get pod -n j ...

  2. k8s删除pod时,docker服务出现挂载点泄漏问题的解决

    k8s更新版本后,老的POD一直出现Terminating,多久都不能删除. 然后,进入具体的节点机器之后,查看日志输出如下类似: ERROR: driver "overlay" ...

  3. k8s强制删除pod

    有时候pod一直在Terminating kubectl delete pod xxx --force --grace-period=

  4. kubernetes删除pod失败

    一.概述 k8s中删除pod失败,可能是该pod有rc,rs上层控制,而且很有可能,所以删除上层对应的rc,rs,deployment即可: 删除的方法: 1.直接删除rc,rs,deployment ...

  5. kubernetes/k8s CRI分析-kubelet删除pod分析

    关联博客<kubernetes/k8s CRI 分析-容器运行时接口分析> <kubernetes/k8s CRI分析-kubelet创建pod分析> 之前的博文先对 CRI ...

  6. k8s学习 - 概念 - Pod

    k8s学习 - 概念 - Pod 这篇继续看概念,主要是 Pod 这个概念,这个概念非常重要,是 k8s 集群的最小单位. 怎么才算是理解好 pod 了呢,基本上把 pod 的所有 describe ...

  7. 利用 trap 在 docker 容器优雅关闭前执行环境清理

    当一个运行中的容器被终止时,如何能够执行一些预定义的操作,比如在容器彻底退出之前清理环境.这是一种类似于 pre stop 的钩子体验.但 docker 本身无法提供这种能力,本文结合 Linux 内 ...

  8. Kubernetes 无法删除pod实例的排查过程

    今天在k8s集群创建pod时,执行了如下命令: #kubectl run busybox-service --image=busybox --replicas=3 但是在创建过程中pod既然失败了, ...

  9. 7.Go退出向Consuk反注册服务,优雅关闭服务

    注册和反注册代码 package utils import ( consulapi "github.com/hashicorp/consul/api" "log" ...

  10. 如何为k8s中的pod配置QoS等级?

    1.概述 本文介绍如何为pod分配特定的QoS等级. 我们知道,在k8s的环境中,通过使用QoS等级来做决定,在资源紧张的时候,将哪些的pod进行驱逐,或者说如何对pod进行调度. OK,话不多说,让 ...

随机推荐

  1. SpringBootAdmin_监控

    监控的意义 监控服务状态是否宕机 监控服务运行指标(内存.虚拟机.线程.请求等) 监控日志 管理服务(服务下线) 监控的实施方式 大部分监控平台都是主动拉取监控信息,而不是被动地等待应用程序传递信息 ...

  2. 专为小白打造—Kafka一篇文章从入门到入土

    一.什么是Kafka MQ消息队列作为最常用的中间件之一,其主要特性有:解耦.异步.限流/削峰. Kafka 和传统的消息系统(也称作消息中间件)都具备系统解耦.冗余存储.流量削峰.缓冲.异步通信.扩 ...

  3. 深入解析 C++ 中的 ostringstream、istringstream 和 stringstream 用法

    引言: 在 C++ 中,ostringstream.istringstream 和 stringstream 是三个非常有用的字符串流类,它们允许我们以流的方式处理字符串数据.本文将深入探讨这三个类的 ...

  4. 炫酷转换:Java实现Excel转换为图片的方法

    摘要:本文由葡萄城技术团队原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言 在实际开发过程中,经常会有这样的需求:将Excel表格或特定区域 ...

  5. 自学一周python做的一个小游戏《大球吃小球》

    需求 1,显示一个窗口. 2,我们要做到的功能有鼠标点击屏幕生成小球. 3,生成的小球大小随机,颜色随机,向随机方向移动,速度也随机. 4,大的球碰到小球时可以吃掉小球,吃掉后会变大. 5,球碰到边界 ...

  6. Chromium GPU资源共享

    资源共享指的是在一个 Context 中的创建的 Texture 资源可以被其他 Context 所使用.一般来讲只有相同 share group Context 创建的 Texture 才可以被共享 ...

  7. Chromium Command Buffer原理解析

    Command Buffer 是支撑 Chromium 多进程硬件加速渲染的核心技术之一.它基于 OpenGLES2.0 定义了一套序列化协议,这套协议规定了所有 OpenGLES2.0 命令的序列化 ...

  8. 如何去掉桌面快捷方式左下角的小箭头(Win11)

    在对系统重命名之后,在快捷方式的左下角莫名的出现了小图标 如果想要去掉这个小图标 (1)首先在桌面上创建一个txt文件 (2)打开后输入指令 reg add "HKEY_LOCAL_MACH ...

  9. MongoDB-SQL语法

    MongoDB-SQL语法 可视化软件:Navicat 1. MongoDB-查询 db.getCollection('表名').find({}); db.getCollection('表名').fi ...

  10. Go 接口:Go中最强大的魔法,接口应用模式或惯例介绍

    Go 接口:Go中最强大的魔法,接口应用模式或惯例介绍 目录 Go 接口:Go中最强大的魔法,接口应用模式或惯例介绍 一.前置原则 二.一切皆组合 2.1 一切皆组合 2.2 垂直组合 2.2.1 第 ...