k8s里的pause镜像

在kubernetes的Pod启动时,会有一个叫做pause的容器先启动,然后才会轮到有真正服务的pod容器启动。这个pod的作用是什么呢?

pause容器的Dockerfile的内容很少

1
2
3
4
FROM scratch
ARG ARCH
ADD bin/pause-${ARCH} /pause
ENTRYPOINT ["/pause"]

可以看到,这里就启动了一个pause程序,再来看下pause程序

https://github.com/kubernetes/kubernetes/blob/master/build/pause/pause.c

只看main函数里的几行代码

1
2
3
4
5
6
7
8
9
10
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();

sigaction函数指定本程序在接收到SIGINT, SIGTERM, SIGCHLD信号之后做什么处理。
设置完后就会通过pause()暂停本进程,一直等待信号,而信号到来后仍然继续循环等待,所以该进程只能通过接收信号后,从信号处理函数里退出了。

信号处理函数就sigdown和sigreap两个函数。

1
2
3
4
5
6
7
8
9
static void sigdown(int signo) {
psignal(signo, "Shutting down, got signal");
exit(0);
}

static void sigreap(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}

结合上面的代码可以看到,进程终止的信号SIGINT, SIGTERM,就是调用psignal输出了信号信息,然后就退出了。集合main函数里的逻辑,pause进程只能从这两个信号退出。

SIGCHLD信号的处理比较特殊。

1
2
while (waitpid(-1, NULL, WNOHANG) > 0)
;

这段代码,一般用父进程有多个子进程时使用,让父进程等待子进程结束。这样做可以防止产生僵尸进程。

1
2
进程可以使用fork和exec syscalls启动其他进程。当启动了其他进程,新进程的父进程就是调用fork syscall的进程。fork用于启动正在运行的进程的另一个副本,而exec则用于启动不同的进程。每个进程在OS进程表中都有一个条目。这将记录有关进程的状态和退出代码。当子进程运行完成,它的进程表条目仍然将保留直到父进程使用wait syscall检索其退出代码将其退出。这被称为“收割”僵尸进程。
僵尸进程是已停止运行但进程表条目仍然存在的进程,因为父进程尚未通过wait syscall进行检索。从技术层面来说,终止的每个进程都算是一个僵尸进程,尽管只是在很短的时间内发生的,但只要不终止他们就可以存活更久。

总结下,pause进程启动后,也就干了两件事

  • 重复循环等待信号,只有SIGTERM和SIGINT时才会退出进程
  • 负责回收僵尸子进程

在Kubernetes中,pause容器的功能会有所扩展

  • pause容器在创建pod时第一个启动,创建的命名空间作为基础和pod中的其他容器共享
  • pause容器在PID namespace中是PID为1的init进程,也是Pod中其他容器的父进程,负责回收pod内的僵尸子进程

如果不使用pause容器呢,一个是命名空间并不好统一管理,一个是容器将会只能自己收集僵尸进程本身,这对内存会是一个隐患。

参考文章:
https://kubernetes.io/docs/concepts/workloads/pods/pod/
http://dockone.io/article/2785
http://dockone.io/article/2682