《一起读 kubernetes 源码》pause 你在哪里?
📢 注意,该文本非最终版本,正在更新中,版权所有,请勿转载!!
前言
你有没有在 k8s 的 node 上敲过 docker ps
这个命令,我就干过。而出现的结果大概会是这样的:
1 | root@10.0.10.102:~# docker ps |
你有没有好奇过这个 google_containers/pause
是什么来路?为什么会有一个这个容器,并且和应用总是成对出现的?我就好奇,于是今天就来叭叭一下 pause 是做什么的。
最早以前 pause 在一些教程里面叫作 infra,我也是当时受众之一,所以第一次看到 pause 有点奇怪它与 infra 的关系,其实是一个东西。
前置知识
- Linux namespace
- pod
- cri
码前提问
- pause 什么时候被创建的?
- pause 是谁创建的?
- 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 | // SyncPod syncs the running pod into the desired pod by executing following steps:// |
我就发现当时有一个 sandbox 容器我们没有管它,难道是它?于是我带着目标去追源码 createPodSandbox
这个方法就是在 SyncPod
里面的第 4 步骤:
1 | // pkg/kubelet/kuberuntime/kuberuntime_sandbox.go:40 |
其中就是创建了 podSandboxConfig
然后就是 RunPodSandbox
也就是使用必要的配置去启动 Sandbox,接下来要注意,别跟错了
1 | // pkg/kubelet/cri/remote/remote_runtime.go:176 |
最后终于到了关键了 runtimeClient 调用的 RunPodSandbox
1 | // kubernetes/vendor/k8s.io/cri-api/pkg/apis/runtime/v1/api.pb.go |
到此,如果你不知道原理,你肯定就懵了。哈?怎么到了一个 pb 里面,并且一个 Invoke 就结束了?此时源码已经追不下去了。这也是读源码最容易遇到的一个问题,由于源码本身会依赖外部的一些实现,导致阅读源码本身并不能理解全部,此时也是原理发挥作用的时候了。让我们来仔细分析一下:
- 这个是在一个叫 cri-api 的包下面
- pb 是 Protocol Buffer 也就是 grpc 的一个调用
所以:得到结论这一定是在调用一个 CRI 的接口,也就是有其他人在实现这个接口,kubelet 负责调用。OK,这里我就不讨论 dockershim 和 containerd 的关系,让我们先来直接看看 containerd 对于 CRI 的实现吧。不要怕,让我们去 containerd 的源码里面看看。
原来是你 containerd
于是我直接去 containerd 源码里面搜索 RuntimeService
的 RunPodSandbox
实现。
1 | // RunPodSandbox creates and starts a pod-level sandbox. Runtimes should ensure |
CreateSandbox
创建,嗯。StartSandbox
启动,嗯。然后我就找,那镜像是哪个,于是让我发现了一个常量
1 | DefaultSandboxImage = "registry.k8s.io/pause:3.9" |
好家伙,还得是你啊。目前我们就知道了是谁创建的这个 pause 容器,那么这个容器是干嘛的呢?于是乎,我去找找这个容器的镜像是如何构建的,让我们回到 k8s 源码里面看看。
pause 镜像
dockerfile 在 kubernetes/build/pause/Dockerfile
,非常容易,就是启动一个二进制 /pause
1 | ARG BASE |
这个二进制的源码在 kubernetes/build/pause/linux/pause.c
1 | static void sigdown(int signo) { |
就这?没错,这就是全部了。里面做了什么事情呢?
- 如果有 -v 打印版本号
- 看看自己是不是第一个进程 pid 是不是 1
- 处理 SIGINT、SIGTERM、SIGCHLD 三个信号
- 死循环等着吧
其实也不过如此是吧,当这个容器创建之后,就如同最开始说的,比如 docker 就可以通过 --net=container:pause
共享你需要的 namespace 了。
码后解答
- pause 什么时候被创建的?
- pod 创建的第一个步骤被创建的
- pause 是谁创建的?
- CRI 的实现者,可以是 containerd、docker
- pause 的作用是什么?
- 成为 pid 为 1 也就是第一个进程从而 “hold 住” namespace
总结提升
pause 作为 pod 创建的最后一块拼图,已经拼上了,至此我觉得 pod 本身的原理应该已经明确了。这一节的代码不复杂,主要是想让你明白,有时候需要明确里面的设计原理和思路再去看代码,否则很容易看不懂或者掉入怪圈里面。在遇到一些外部调用和扩展的时候也不用慌张,努力去发现一些蛛丝马迹,结合已有的知识点大胆假设,小心求证,你总能在源码中找到属于你的真相。