在一个运行 kubernetes 的节点上,我们能看到很多名叫 “pause” 的 container。比如
$ sudo docker ps | grep pause
a4218d1d379b k8s.gcr.io/pause:3.1 "/pause"
a2109bf3f0db k8s.gcr.io/pause:3.1 "/pause"
57cfa42e95d3 k8s.gcr.io/pause:3.1 "/pause"
仔细观察一下不难发现,每一个 Pod 都会对应一个 pause container。
在查阅了网上的一些资料以后,我总结了一下它大概有两个作用,
- 它是 Pod 中第一个启动的 container ,由它创建新的 linux namespace,其他 container 启动后再加入到这些 namespace 中。
- 在 Pod 的环境中充当 init process 的角色,它的 PID 是 1,负责回收所有僵尸进程。
说个题外话,在 docker 中,一个 container 启动时,Dockerfile 的 ENTRYPOINT 中指定的命令会成为这个 container 的 init process,PID 为 1.
顺便来看一下 pause 容器的实现,一共只有几十行 C 语言代码
static void sigdown(int signo) {
psignal(signo, "Shutting down, got signal");
exit(0);
}
static void sigreap(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
int main(int argc, char **argv) {
if (getpid() != 1)
/* Not an error because pause sees use outside of infra containers. */
fprintf(stderr, "Warning: pause should be the first process\n");
if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 1;
if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 2;
if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
.sa_flags = SA_NOCLDSTOP}, NULL) < 0)
return 3;
for (;;)
pause();
fprintf(stderr, "Error: infinite loop terminated\n");
return 42;
}
函数 sigreap(int signo)
就是在子进程退出的时候进行回收,防止产生僵尸进程,所以是 “reap”,非常形象生动。
可以看到 sigreap 函数的作用就是无限循环检测有没有子进程退出,如果有就调用 waitpid() 处理,防止产生僵尸进程。
- -1 代表监听所有进程的 SIGCHLD 信号
- 一般来说,waitpid收到 SIGCHLD 信号后,会一直等待子进程的结束,通过设置 WNOHANG 告诉 waitpid为非阻塞,但这样的话就需要不断的调用waitpid来探测子进程是否结束。
pause 的 main() 捕捉三类进程的退出信号,
- SIGINT,一般就是用户输入 Ctrl-C 后进程 “收到” 的信号。
- SIGTERM, kill默认的终止信号,也是目标进程收到的信号。
- SIGCHLD
注意,上面前两个信号都是指 pause 程序收到了发给自己的信号后调用信号处理函数 sigdown。
只有 SIGCHLD 信号才是 pause 真正监听子进程退出的信号,处理函数是 sigreap。
看到这里,已经大概清楚了 pause 容器的来龙去脉了,于是我随便找了一个 k8s cluster,登录到 Pod 内部看看是不是 pause 的容器 PID 为 1.
然而结果却很奇怪,
/home# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2388 764 ? Ss Aug24 0:00 /bin/sh -c python flask_server.py
root 6 0.0 0.1 60640 47404 ? S Aug24 0:00 python flask_server.py
root 8 0.9 0.1 356080 52724 ? Sl Aug24 265:12 /usr/local/bin/python /home/flask_server.py
在这个 container 内部根本没有 PID = 1 的 pause 进程。
于是,退出 container,在 Host 主机上 docker ps 罗列了所有的 container,证实确实存在一个对应的 pause 正在运行。
继续在网上搜索之后,发现 Kubernetes 1.8 版本之前,默认是启用 PID namespace 共享的,也就是收 Pod 内的 container 共享 PID。而到了 1.8 版本以后,情况刚好相反,默认情况下 kubelet 启动时 --docker-disable-shared-pid=true
。
用户在使用 YAML file deploy 时,可以指定 shareProcessNamespace
开启 PID namespace 的共享。
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: default
labels:
app: my-app
spec:
replicas: 2
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
shareProcessNamespace: true
containers:
- name: my-app
image: gcr.io/my-app:v1
imagePullPolicy: Always
我试着 apply 了上面的 YAML,然后进入 container,确实看到了 pause 进程,而且 PID = 1.
~# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 1024 4 ? Ss 00:49 0:00 /pause
root 6 0.1 0.3 1162780 15396 ? Ssl 00:49 0:25 /root/server
root 17 0.0 0.0 18508 3372 pts/0 Ss+ 00:49 0:00 bash
那么为什么 Kubernetes 1.8 之后要默认关闭 PID 共享呢? 既然默认关闭,是不是意味着每个随着 Pod 启动而启动 pause 容器就是多余的呢?
第一个问题似乎可以解答: pause 容器解决的是 Pod 内应用程序产生了子进程,并且应用程序自己没有很好的处理子进程退出问题,继而产生了僵尸进程。说到底,这是用户自己的锅。 而如果一个应用程序不会产生子进程,那么就没有僵尸进程的问题了。
如果默认开启 PID 共享,有两个潜在的问题:
- 像 systemd 这种默认启动 PID 为 1 的程序会有问题。
- Pod 中有多个 container,比如一个是你的 app,另一个是类似 envoy 这样的 sidecar,那么默认是能看到
/proc/{pid}/
下的信息的
总之,默认开启还是关闭,是一个仁者见仁智者见智的事情,也不一定谁对谁错,但是我们要记住,需要 PID 共享的时候记得加 shareProcessNamespace
。