📢 注意,该文本非最终版本,正在更新中,版权所有,请勿转载!!

前言

你有没有在 k8s 的 node 上敲过 docker ps 这个命令,我就干过。而出现的结果大概会是这样的:

1
2
3
4
root@10.0.10.102:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5aa88e8d16ac xxxx "/entrypoint.sh" 3 days ago Up 3 days k8s_xxx-0
4e40566baa09 google_containers/pause:3.4.1 "/pause" 3 days ago Up 3 days k8s_POD_xxxx

你有没有好奇过这个 google_containers/pause 是什么来路?为什么会有一个这个容器,并且和应用总是成对出现的?我就好奇,于是今天就来叭叭一下 pause 是做什么的。
最早以前 pause 在一些教程里面叫作 infra,我也是当时受众之一,所以第一次看到 pause 有点奇怪它与 infra 的关系,其实是一个东西。

前置知识

  • Linux namespace
  • pod
  • cri

码前提问

  1. pause 什么时候被创建的?
  2. pause 是谁创建的?
  3. pause 的作用是什么?

心路历程

作为第一章节的最后一小结,将在这里说明另一个源码阅读要注意的方式方法:先原理,再源码。有时候,仅仅只是使用某个工具或项目,一些细节的地方是没有办法在使用中被了解的,比如我们使用了很久 k8s 知道了 pod 的作用以及能力,但我们依旧对 pause 毫无感知,因为它是那种背后默默无闻的东西。对于这些技术的实现,如果直接去看源码会有两个问题,一个是难以理解,另一个则是容易误入歧途,看着看着看叉了。所以,对于 pause 与之前不同的是,我们需要先去弄懂它的原理,了解了大概之后再回去看源码。

如果不了解的请看 https://www.ianlewis.org/en/almighty-pause-container

当然,你也可以不看,我直接帮你总结为了一句话:paues 让 pod 中的多个容器可以 sharing namespaces(共享命名空间)。
因为我们知道一个 pod 可以包含多个容器,这些容器可以共享网络资源,并且重要的是 namespace 是隔离的基础,也是运行的保证,如果让任意其他的业务容器去当作主容器被别人共享,那么主容器的安危就决定了整个 pod 的生死,那显然有些不合理,于是找到了中间商 pause 来帮助我们先 hold 所需要的 namespace,然后做共享,这也就是 pause 存在的意义了。
你可以根据文中的指令来在本机上运行一个 pause 容器来使用 --net=container:pause 类似的参数来共享,并测试。

而 pause 在 k8s 中是如何被创建,并且做了哪些事情呢?这就需要到源码中寻找答案了。

源码分析

当你想要你 k8s 的源码中寻找 pause 的时候,你就会发现,你能找到一些蛛丝马迹,但是毫无头绪,一开始我也是的,我在源码中搜索了所有有关 pause 的内容,发现并没有看到真正创建这个容器的地方。(此时我还没懂 pause 的原理)于是乎,我回头弄清楚的原理(先原理再源码),发现 pause 的作用是共享命名空间,那么它的创建一定是在 pod 创建的比较前面步骤,至少要在其他容器创建之前

于是就回到了我们第一节里面,说 pod 创建的时候有一个 SyncPod 的方法

1
2
3
4
5
6
7
8
9
10
// SyncPod syncs the running pod into the desired pod by executing following steps://
// 1. Compute sandbox and container changes.
// 2. Kill pod sandbox if necessary.
// 3. Kill any containers that should not be running.
// 4. Create sandbox if necessary.
// 5. Create ephemeral containers.
// 6. Create init containers.
// 7. Resize running containers (if InPlacePodVerticalScaling==true)
// 8. Create normal containers.
func (m *kubeGenericRuntimeManager) SyncPod(

我就发现当时有一个 sandbox 容器我们没有管它,难道是它?于是我带着目标去追源码 createPodSandbox 这个方法就是在 SyncPod 里面的第 4 步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// pkg/kubelet/kuberuntime/kuberuntime_sandbox.go:40
// createPodSandbox creates a pod sandbox and returns (podSandBoxID, message, error).
func (m *kubeGenericRuntimeManager) createPodSandbox(ctx context.Context, pod *v1.Pod, attempt uint32) (string, string, error) {
// ...
podSandboxConfig, err := m.generatePodSandboxConfig(pod, attempt)

// ...
err = m.osInterface.MkdirAll(podSandboxConfig.LogDirectory, 0755)

// ...
runtimeHandler, err = m.runtimeClassManager.LookupRuntimeHandler(pod.Spec.RuntimeClassName)

// ...
podSandBoxID, err := m.runtimeService.RunPodSandbox(ctx, podSandboxConfig, runtimeHandler)

return podSandBoxID, "", nil
}

其中就是创建了 podSandboxConfig 然后就是 RunPodSandbox 也就是使用必要的配置去启动 Sandbox,接下来要注意,别跟错了

1
2
3
4
5
6
7
8
9
10
11
12
// pkg/kubelet/cri/remote/remote_runtime.go:176
func (r *remoteRuntimeService) RunPodSandbox(ctx context.Context, config *runtimeapi.PodSandboxConfig, runtimeHandler string) (string, error) {
// ...
resp, err := r.runtimeClient.RunPodSandbox(ctx, &runtimeapi.RunPodSandboxRequest{
Config: config,
RuntimeHandler: runtimeHandler,
})

// ...
podSandboxID := resp.PodSandboxId
return podSandboxID, nil
}

最后终于到了关键了 runtimeClient 调用的 RunPodSandbox

1
2
3
4
5
6
7
8
9
// kubernetes/vendor/k8s.io/cri-api/pkg/apis/runtime/v1/api.pb.go
func (c *runtimeServiceClient) RunPodSandbox(ctx context.Context, in *RunPodSandboxRequest, opts ...grpc.CallOption) (*RunPodSandboxResponse, error) {
out := new(RunPodSandboxResponse)
err := c.cc.Invoke(ctx, "/runtime.v1.RuntimeService/RunPodSandbox", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}

到此,如果你不知道原理,你肯定就懵了。哈?怎么到了一个 pb 里面,并且一个 Invoke 就结束了?此时源码已经追不下去了。这也是读源码最容易遇到的一个问题,由于源码本身会依赖外部的一些实现,导致阅读源码本身并不能理解全部,此时也是原理发挥作用的时候了。让我们来仔细分析一下:

  1. 这个是在一个叫 cri-api 的包下面
  2. pb 是 Protocol Buffer 也就是 grpc 的一个调用

所以:得到结论这一定是在调用一个 CRI 的接口,也就是有其他人在实现这个接口,kubelet 负责调用。OK,这里我就不讨论 dockershim 和 containerd 的关系,让我们先来直接看看 containerd 对于 CRI 的实现吧。不要怕,让我们去 containerd 的源码里面看看。

原来是你 containerd

于是我直接去 containerd 源码里面搜索 RuntimeServiceRunPodSandbox 实现。

https://github.com/containerd/containerd/blob/b693d137ed5f905d04bf955b185054011e25880c/internal/cri/server/sandbox_run.go#L51

1
2
3
4
5
6
7
8
9
10
// RunPodSandbox creates and starts a pod-level sandbox. Runtimes should ensure
// the sandbox is in ready state.
func (c *criService) RunPodSandbox(ctx context.Context, r *runtime.RunPodSandboxRequest) (_ *runtime.RunPodSandboxResponse, retErr error) {
// ...
if err := c.sandboxService.CreateSandbox(ctx, sandboxInfo, sb.WithOptions(config), sb.WithNetNSPath(sandbox.NetNSPath)); err != nil {
return nil, fmt.Errorf("failed to create sandbox %q: %w", id, err)
}
// ...
ctrl, err := c.sandboxService.StartSandbox(ctx, sandbox.Sandboxer, id)
}

CreateSandbox 创建,嗯。StartSandbox 启动,嗯。然后我就找,那镜像是哪个,于是让我发现了一个常量

https://github.com/containerd/containerd/blob/2adae6093e52028580f72c6f8c4f2f06c9d57648/internal/cri/config/config.go#L73

1
DefaultSandboxImage = "registry.k8s.io/pause:3.9"

好家伙,还得是你啊。目前我们就知道了是谁创建的这个 pause 容器,那么这个容器是干嘛的呢?于是乎,我去找找这个容器的镜像是如何构建的,让我们回到 k8s 源码里面看看。

pause 镜像

dockerfile 在 kubernetes/build/pause/Dockerfile,非常容易,就是启动一个二进制 /pause

1
2
3
4
5
6
ARG BASE
FROM ${BASE}
ARG ARCH
ADD bin/pause-linux-${ARCH} /pause
USER 65535:65535
ENTRYPOINT ["/pause"]

这个二进制的源码在 kubernetes/build/pause/linux/pause.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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) {
int i;
for (i = 1; i < argc; ++i) {
if (!strcasecmp(argv[i], "-v")) {
printf("pause.c %s\n", VERSION_STRING(VERSION));
return 0;
}
}

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;
}

就这?没错,这就是全部了。里面做了什么事情呢?

  1. 如果有 -v 打印版本号
  2. 看看自己是不是第一个进程 pid 是不是 1
  3. 处理 SIGINT、SIGTERM、SIGCHLD 三个信号
  4. 死循环等着吧

其实也不过如此是吧,当这个容器创建之后,就如同最开始说的,比如 docker 就可以通过 --net=container:pause 共享你需要的 namespace 了。

码后解答

  1. pause 什么时候被创建的?
    1. pod 创建的第一个步骤被创建的
  2. pause 是谁创建的?
    1. CRI 的实现者,可以是 containerd、docker
  3. pause 的作用是什么?
    1. 成为 pid 为 1 也就是第一个进程从而 “hold 住” namespace

总结提升

pause 作为 pod 创建的最后一块拼图,已经拼上了,至此我觉得 pod 本身的原理应该已经明确了。这一节的代码不复杂,主要是想让你明白,有时候需要明确里面的设计原理和思路再去看代码,否则很容易看不懂或者掉入怪圈里面。在遇到一些外部调用和扩展的时候也不用慌张,努力去发现一些蛛丝马迹,结合已有的知识点大胆假设,小心求证,你总能在源码中找到属于你的真相。