前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《一起读 kubernetes 源码》pause 你在哪里?

《一起读 kubernetes 源码》pause 你在哪里?

作者头像
LinkinStar
发布2024-04-13 09:29:07
760
发布2024-04-13 09:29:07
举报
文章被收录于专栏:LinkinStar's BlogLinkinStar's Blog

前言

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

代码语言:javascript
复制
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 的方法

代码语言:javascript
复制
// 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 步骤:

代码语言:javascript
复制
// 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,接下来要注意,别跟错了

代码语言:javascript
复制
// 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

代码语言:javascript
复制
// 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

代码语言:javascript
复制
// 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

代码语言:javascript
复制
DefaultSandboxImage = "registry.k8s.io/pause:3.9"

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

pause 镜像

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

代码语言:javascript
复制
ARG BASE
FROM ${BASE}
ARG ARCH
ADD bin/pause-linux-${ARCH} /pause
USER 65535:65535
ENTRYPOINT ["/pause"]

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

代码语言:javascript
复制
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 本身的原理应该已经明确了。这一节的代码不复杂,主要是想让你明白,有时候需要明确里面的设计原理和思路再去看代码,否则很容易看不懂或者掉入怪圈里面。在遇到一些外部调用和扩展的时候也不用慌张,努力去发现一些蛛丝马迹,结合已有的知识点大胆假设,小心求证,你总能在源码中找到属于你的真相。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-04-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 前置知识
  • 码前提问
  • 心路历程
  • 源码分析
    • 原来是你 containerd
      • pause 镜像
      • 码后解答
      • 总结提升
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档