在一个运行 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。

在查阅了网上的一些资料以后,我总结了一下它大概有两个作用,

  1. 它是 Pod 中第一个启动的 container ,由它创建新的 linux namespace,其他 container 启动后再加入到这些 namespace 中。
  2. 在 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 共享,有两个潜在的问题:

  1. 像 systemd 这种默认启动 PID 为 1 的程序会有问题。
  2. Pod 中有多个 container,比如一个是你的 app,另一个是类似 envoy 这样的 sidecar,那么默认是能看到 /proc/{pid}/ 下的信息的

总之,默认开启还是关闭,是一个仁者见仁智者见智的事情,也不一定谁对谁错,但是我们要记住,需要 PID 共享的时候记得加 shareProcessNamespace

References