containerd如何创建Pod

前言

之前看了kubelet侧如何管理Pod,本章分析CRI的默认实现:containerd。

  1. kubelet:watch apiserver获取分配给当前Node的Pod,调用CRI接口,管理Pod生命周期

    • RunPodSandbox/PodSandboxStatus,创建Sandbox容器,获取Sandbox容器详情,包含PodIP;

    • ImageStatus/PullImage,查询容器镜像是否存在,不存在拉取镜像;

    • CreateContainer/StartContainer,创建和启动业务容器;

    • ListPodSandbox/ListContainers,PLEG循环每秒扫描容器状态,更新内存缓存,发现容器下线重新拉起;

  2. CRI:Container Runtime Interface,kubelet对接Runtime的接口定义,见github.com/kubernetes/...

  3. CNI:Container Network Interface,用于在容器中配置网络接口,见github.com/containerne...

  4. containerd:默认CRI实现,同类还有CRI-Ogithub.com/cri-o/cri-o

  5. shim:一个Pod对应一个shim常驻进程,containerd定义协议。默认containerd使用runc,如gVisor要接入自己的Runtime,需要自行实现如github.com/google/gvis...

  6. OCI Runtime Spec:OCI 运行时规范旨在定义容器的配置、执行环境和生命周期,github.com/opencontain...

  7. Runtime:容器运行时,如runc是github.com/opencontain...,gVisor是github.com/google/gvis...

注:

  1. kubelet:1.36.1

  2. containerd:2.3.1

一、启动 Sandbox 容器

kubelet 通过 gRPC over UDS(默认 unix:///run/containerd/containerd.sock)发起 RunPodSandbox 请求,创建sandbox容器。

1.1. 时序

整体时序如下:

1)kubelet发送PodSandboxConfig给containerd,包含:cgroup路径、Pod级资源限制、SecurityContext安全上下文、Pod主机名、Pod日志目录、Pod名称/Namespace/UID等等;

2)containerd调用CNI插件,CNI插件返回PodIP;

3)containerd启动containerd-shim-runc-v2进程,收到一个ttrpc地址,如unix:///run/containerd/s/<64位hex>;(ttrpc 是 containerd 自研的 轻量 RPC,接口像 gRPC,协议更轻)

4)containerd连接shim的ttrpc地址,用于后续通讯,包括启动和停止容器;

5)containerd调用shim创建和启动sandbox容器(pause容器),shim底层调用runc命令;

如启动一个Pod后,可以看到如下进程树,shim会是一个独立进程,shim下挂着Pod内的N个容器,包括sandbox,即/pause。

Shell 复制代码
144998       1  /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io ...
145021  144998   \_ /pause
145061  144998   \_ nginx: master process nginx -g daemon off;

1.2. RunPodSandbox 主流程

internal/cri/server/sandbox_run.go

1)非hostNetwork,配置网络环境,containerd 负责创建 netns,将 netns 路径传递给 CNI,CNI 返回 PodIP;

2)c.sandboxService.StartSandbox,containerd 创建并启动 sandbox 容器;

3)c.client.SandboxStore(),sandbox元数据持久化到本地boltdb,数据文件位置为:/var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db,containerd重启后从这里恢复到内存c.sandboxStore

4)sandbox信息会缓存到c.sandboxStore,后面如kubelet调用CRIListPodSandbox,也是从内存里取数据;

Go 复制代码
func (c *criService) RunPodSandbox(ctx context.Context, r *runtime.RunPodSandboxRequest) (_ *runtime.RunPodSandboxResponse, retErr error) {
	// 1. 生成 sandbox id/name,Reserve占用名称(防止并发 RunPodSandbox 冲突)。
	id := util.GenerateID()
	metadata := config.GetMetadata()
	name := makeSandboxName(metadata)
	err := c.sandboxNameIndex.Reserve(name, id)
	var (
		err         error
		sandboxInfo = sb.Sandbox{ID: id}
	)
	// 2. 按 runtime_handler 解析 OCI runtime / Sandboxer。
	ociRuntime, err := c.config.GetSandboxRuntime(config, r.GetRuntimeHandler())
	// 3. 构造内存中的 sandbox 元数据,sandboxInfo写入 boltdb。
	sandbox := sandboxstore.NewSandbox(
		sandboxstore.Metadata{
			ID:             id,
			Name:           name,
			Config:         config,
			RuntimeHandler: r.GetRuntimeHandler(),
		},
		sandboxstore.Status{
			State:     sandboxstore.StateUnknown,
			CreatedAt: time.Now().UTC(),
		},
	)
	sandbox.Sandboxer = ociRuntime.Sandboxer
	sandboxInfo.AddExtension(podsandbox.MetadataKey, &sandbox.Metadata)
	c.client.SandboxStore().Create(ctx, sandboxInfo)
	// 4. 配置网络(hostNetwork 则跳过)
	if !hostNetwork(config) {
		var netnsMountDir = "/var/run/netns"
		// 这里创建netns
		sandbox.NetNS, err = netns.NewNetNS(netnsMountDir)
		sandbox.NetNSPath = sandbox.NetNS.GetPath()
		sandboxInfo.AddExtension(podsandbox.MetadataKey, &sandbox.Metadata)
		sandboxInfo, err = c.client.SandboxStore().Update(ctx, sandboxInfo, "extensions")
		// 这里调用CNI,CNI返回PodIP,写到&sandbox
		c.setupPodNetwork(ctx, &sandbox)
		sandboxInfo.AddExtension(podsandbox.MetadataKey, &sandbox.Metadata)
		// 更新sandboxInfo到boltdb
		c.client.SandboxStore().Update(ctx, sandboxInfo, "extensions")
	}
	// 5. 通过 controller CreateSandbox(仅存入内存)。
	c.sandboxService.CreateSandbox(ctx, sandboxInfo, sb.WithOptions(config), sb.WithNetNSPath(sandbox.NetNSPath))
	// 6. 确保 pause 镜像存在(本地解析;没有则 Pull)
	c.ensurePauseImageExists(ctx, r.GetConfig(), r.GetRuntimeHandler())
	// 7. StartSandbox:创建并启动 pause 容器
	ctrl, err := c.sandboxService.StartSandbox(ctx, sandbox.Sandboxer, id)
	// 8. 内存缓存sandbox信息,后面kubelet调用ListPodSandbox,就是从这里拿
	c.sandboxStore.Add(sandbox)
	return &runtime.RunPodSandboxResponse{PodSandboxId: id}, nil
}

internal/cri/server/podsandbox/sandbox_run.go:创建并启动 sandbox 容器

1)c.config.GetSandboxRuntime,根据请求入参的RuntimeHandler(kubelet解析Pod上的runtimeClassName得到)匹配containerd配置的runtime实现,得到实际runtime配置,默认是runc;

2)c.sandboxContainerSpec,构造OCI Runtime Spec;

3)NewContainer,制作rootfs,构造Container对象,包括Spec,持久化到本地boltdb;

4)container.NewTask,启动shim进程,containerd与shim建立连接,创建sandbox;

5)task.Start,调用shim,启动sandbox;

Go 复制代码
func (c *Controller) Start(ctx context.Context, id string) (cin sandbox.ControllerInstance, retErr error) {
	// 1. CreateSandbox时写入内存sandbox,这里取出sandboxInfo
	podSandbox := c.store.Get(id)
	metadata := podSandbox.Metadata
	var (
		config = metadata.Config
		labels = map[string]string{}
	)
	// 2. 获取 pause 镜像 及其 OCI Image Spec
	sandboxImage := c.getSandboxImageName()
	pauseImage, err := c.client.GetImage(ctx, sandboxImage)
	imageSpec, err := pauseImage.Spec(ctx)
	// 3. 按 RuntimeHandler 解析 sandbox 使用的 OCI runtime。
	ociRuntime, err := c.config.GetSandboxRuntime(config, metadata.RuntimeHandler)
	// 4. 创建 sandbox 持久目录与 volatile 目录
	sandboxRootDir := c.getSandboxRootDir(id)
	if err := c.os.MkdirAll(sandboxRootDir, 0755)
	volatileSandboxRootDir := c.getVolatileSandboxRootDir(id)
	if err := c.os.MkdirAll(volatileSandboxRootDir, 0755)
	// 5. 生成 pause 容器 OCI Runtime Spec
	spec, err := c.sandboxContainerSpec(id, config, &imageSpec.Config, metadata.NetNSPath, ociRuntime.PodAnnotations)
	specOpts, err := c.sandboxContainerSpecOpts(config, &imageSpec.Config)
	snapshotterOpt = append(snapshotterOpt, extraSOpts...)
    // 默认overlayfs
	sandboxSnapshotter := c.imageConfig.Snapshotter
	// 6. WithNewSnapshot制作rootfs,container配置持久化到boltdb
	opts := []containerd.NewContainerOpts{
		containerd.WithSnapshotter(sandboxSnapshotter),
		customopts.WithNewSnapshot(id, pauseImage, !c.imageConfig.DisableSnapshotAnnotations, snapshotterOpt...),
		containerd.WithSpec(spec, specOpts...),
		containerd.WithContainerLabels(sandboxLabels),
		containerd.WithContainerExtension(crilabels.SandboxMetadataExtension, &metadata),
		containerd.WithRuntime(ociRuntime.Type, podSandbox.Runtime.Options),
	}
	container, err := c.client.NewContainer(ctx, id, opts...)
	podSandbox.Container = container
	// 7. 创建 sandbox 所需文件(如 resolv.conf 等)
	err = c.setupSandboxFiles(id, config)
	// 8. 创建shim任务,启动shim进程,并与shim建立连接,创建sandbox
	task, err := container.NewTask(ctx, containerdio.NullIO, taskOpts...)
	// 9. 调用shim,启动sandbox
	err := task.Start(ctx)
	pid := task.Pid()
	return
}

下面主要分析:

1)setupPodNetwork:CNI初始化网络;

2)container.NewTask:创建shim和sandbox;

3)task.Start:通过shim启动sandbox;

1.3. CNI 网络初始化

pkg/netns/netns_linux.go:containerd 侧 创建网络命名空间(netns)。

1)随机创建一个挂载点 /var/run/netns/cni-xxx

2)在 containerd 进程里开一个专用 OS 线程,Unshare 出新的 netns(如 /proc/<containerd_pid>/task/<tid>/ns/net);

3)将该 netns bind-mount 到 1,靠挂载点持有引用,线程退出后 netns 仍持久存在;

Go 复制代码
func newNS(baseDir string, pid uint32) (nsPath string, err error) {
	b := make([]byte, 16)
	_, err = rand.Read(b)
	nsName := fmt.Sprintf("cni-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
    // /var/run/netns/cni-xxx
	nsPath = path.Join(baseDir, nsName)
	mountPointFd, err := os.OpenFile(nsPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
	mountPointFd.Close()
	var wg sync.WaitGroup
	wg.Add(1)
	go (func() {
		defer wg.Done()
        // 把当前 goroutine 钉死在某一个 OS 线程上,不再被调度到别的线程
		runtime.LockOSThread()
		// 当前线程 创建新的netns unshare(2)系统调用
		err = unix.Unshare(unix.CLONE_NEWNET)
        // mount /proc/186812/task/186921/ns/net -> /var/run/netns/cni-xxx
		err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "")
	})()
	wg.Wait()
	return nsPath, nil
}

internal/cri/server/sandbox_run.gosetupPodNetwork 从kubelet传入的 PodSandboxConfig 提取 网络配置 + netns路径,调用CNI返回IP。

Go 复制代码
func (c *criService) setupPodNetwork(ctx context.Context, sandbox *sandboxstore.Sandbox) (retErr error) {
	var (
		id        = sandbox.ID
		config    = sandbox.Config
		path      = sandbox.NetNSPath
		// 按 RuntimeClass 选 CNI,缺省回落到默认插件
		netPlugin = c.getNetworkPlugin(sandbox.RuntimeHandler)
		err       error
		result    *cni.Result
	)
	// 1. 从 PodSandboxConfig 组装 CNI 能力参数(端口映射、DNS等)。
	opts, err := cniNamespaceOpts(id, config)
	// 2. 在 sandbox 的 netns(path)里执行 CNI Setup
    result, err = netPlugin.Setup(ctx, id, path, opts...)
	// 3. 从默认网卡 defaultIfName = eth0 取 IP
	if configs, ok := result.Interfaces[defaultIfName]; ok && len(configs.IPConfigs) > 0 {
		sandbox.IP, sandbox.AdditionalIPs = selectPodIPs(ctx, configs.IPConfigs, c.config.IPPreference)
		sandbox.CNIResult = result
		return nil
	}
	return fmt.Errorf("failed to find network info for sandbox %q", id)
}

vendor/github.com/containerd/go-cni/cni.go:CNI调用:1)循环网络接口;2)循环每个网络接口的插件。

Go 复制代码
func (c *libcni) attachNetworks(ctx context.Context, ns *Namespace) ([]*types100.Result, error) {
	var wg sync.WaitGroup
	var firstError error
	results := make([]*types100.Result, len(c.networks))
	rc := make(chan asynchAttachResult)
	// 循环网络接口名 lo(containerd写死) + eth0(CNI插件配置)
	for i, network := range c.networks {
		wg.Add(1)
		go asynchAttach(ctx, i, network, ns, &wg, rc)
	}
}
func (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
	var err error
	var result types.Result
    // 对于每个网络接口,再循环各自的Plugin
	for _, net := range list.Plugins {
		result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt)
	}
	return result, nil
}

一般这里有两个networks,一个是containerd写死的loopback,用于配置lo。

JSON 复制代码
{
    "cniVersion": "0.3.1",
    "name": "cni-loopback",
    "plugins": [{
        "type": "loopback"
    }]
}

一个是CNI实现,containerd默认从/etc/cni/net.d下找配置文件,如10-calico.conflist

JSON 复制代码
{
    "name": "k8s-pod-network",
    "cniVersion": "0.3.1",
    "plugins": [
        {
            "calico_api_group": "",
            "container_settings": {
                "allow_ip_forwarding": false
            },
            "datastore_type": "kubernetes",
            "endpoint_status_dir": "/var/run/calico/endpoint-status",
            "ipam": {
                "assign_ipv4": "true",
                "assign_ipv6": "false",
                "type": "calico-ipam"
            },
            "kubernetes": {
                "k8s_api_root": "https://10.96.0.1:443",
                "kubeconfig": "/etc/cni/net.d/calico-kubeconfig"
            },
            "log_file_max_age": 30,
            "log_file_max_count": 10,
            "log_file_max_size": 100,
            "log_file_path": "/var/log/calico/cni/cni.log",
            "log_level": "Info",
            "mtu": 0,
            "nodename_file_optional": false,
            "policy": {
                "type": "k8s"
            },
            "policy_setup_timeout_seconds": 0,
            "type": "calico"
        },
        {
            "capabilities": {
                "portMappings": true
            },
            "snat": true,
            "type": "portmap"
        }
    ]
}

vendor/github.com/containernetworking/cni/libcni/api.go:调用CNI插件。

Go 复制代码
func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *PluginConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) {
    // 构造插件可执行程序路径 如/opt/cni/bin/calico
	pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
    // 构造配置,包括上一个插件执行的结果,和CNI自己的配置
	newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
    // c.args = CRI侧的部分配置
	return invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec)
}
func ExecPluginWithResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) (types.Result, error) {
    // 插件调用,获取标准输出,作为结果
	stdoutBytes, err := exec.ExecPlugin(ctx, pluginPath, netconf, args.AsEnv())
}

1)FindInPath/opt/cni/bin目录下,找到插件type对应的可执行程序。

Shell 复制代码
# ls /opt/cni/bin/
calico	calico-ipam  flannel  host-local  loopback  portmap  ptp  tuning

2)netconf:CNI配置,通过标准输入传入,和/etc/cni/net.d中插件对应的字段基本一致。

3)args.AsEnv():CRI侧数据,通过ENV传入,比如:

CNI_COMMAND=ADD:子命令,代表创建网络;

CNI_NETNS=/var/run/netns/cni-xxx:netns目录;

CNI_ARGS=K8S_POD_NAME=x;K8S_POD_INFRA_CONTAINER_ID=y;K8S_POD_UID=z;IgnoreUnknown=1;K8S_POD_NAMESPACE=default:Pod信息;

CNI_IFNAME=eth0:网络接口名;

CNI_CONTAINERID=y:容器id;

4)stdoutBytes 通过标准输出,获取CNI插件的返回;

JSON 复制代码
{
  "cniVersion": "0.3.1",
  "interfaces": [
    {
      "name": "calic440f455693"
    },
    {
      "name": "eth0",
      "mac": "e6:88:a5:cf:97:1e",
      "sandbox": "/var/run/netns/cni-5eaa3464-47bd-71d5-d1cc-815e5763d321"
    }
  ],
  "ips": [
    {
      "version": "4",
      "interface": 1,
      "address": "192.168.43.21/32"
    }
  ],
  "routes": [
    {
      "dst": "192.168.43.21/26"
    }
  ],
  "dns": {}
}

1.4. 创建 Sandbox 容器(NewTask)

core/runtime/v2/task_manager.go:containerd

1)创建Bundle,包含容器运行时配置和rootfs;

2)创建shim进程,建立连接;

3)发送CreateTaskRequest给shim,创建Sandbox容器(runc create);

Go 复制代码
func (m *TaskManager) Create(ctx context.Context, taskID string, opts runtime.CreateOpts) (_ runtime.Task, retErr error) {
    // 创建Bundle
	bundle, err := NewBundle(ctx, m.root, m.state, taskID, opts.Spec)
    // 创建shim,建立连接
	shim, err := m.manager.Start(ctx, taskID, bundle, opts)
	shimTask, err := newShimTask(shim)
    // 发送CreateTaskRequest给shim
	t, err := func() (runtime.Task, error) {
		t, err := shimTask.Create(ctx, opts)
	}()
	return t, nil
}

Bundle

Bundle 由 OCI Runtime Spec 定义,是一组按特定方式组织的文件,Runtime按照这个标准运行容器,包含: 1)config.json:运行容器的配置文件,json-schema见github.com/opencontain...; 2)rootfs:启动容器所需的目录结构,这个是containerd负责创建,在config.json中通过root.path告知Runtime;

NewBundle containerd 为 Sandbox 在磁盘上建好这个目录结构。后续containerd只传递Bundle=/run/containerd/io.containerd.runtime.v2.task/k8s.io/sandbox容器id给shim。

Shell 复制代码
ls /run/containerd/io.containerd.runtime.v2.task/k8s.io/sandbox容器id
config.json  rootfs  work

示例config.json:root.path是基于工作目录的相对路径,宿主节点上在/run/containerd/io.containerd.runtime.v2.task/k8s.io/容器id/rootfs

JSON 复制代码
{
  "ociVersion": "1.3.0",
  "process": {
    "user": { "uid": 65535, "gid": 65535 },
    "args": ["/pause"],
    "cwd": "/",
    "capabilities": { "...": "默认 capability 集合" },
    "noNewPrivileges": true,
    "oomScoreAdj": -998
  },
  "root": {
    "path": "rootfs",
    "readonly": true
  },
  "hostname": "nginx",
  "mounts": [
    { "destination": "/proc", "type": "proc", "source": "proc" },
    { "destination": "/dev",  "type": "tmpfs", "source": "tmpfs" },
    { "destination": "/sys",  "type": "sysfs", "source": "sysfs", "options": ["ro"] },
    {
      "destination": "/dev/shm",
      "type": "bind",
      "source": "/run/containerd/.../sandboxes/<id>/shm",
      "options": ["rbind", "ro"]
    },
    {
      "destination": "/etc/resolv.conf",
      "type": "bind",
      "source": "/var/lib/containerd/.../sandboxes/<id>/resolv.conf",
      "options": ["rbind", "ro"]
    }
  ],
  "annotations": {
    "io.kubernetes.cri.container-type": "sandbox",
    "io.kubernetes.cri.sandbox-name": "nginx",
    "io.kubernetes.cri.sandbox-namespace": "default",
    "io.kubernetes.cri.sandbox-id": "<sandbox-id>"
  },
  "linux": {
    "cgroupsPath": "kubepods-...slice:cri-containerd:<sandbox-id>",
    "resources": { "cpu": { "shares": 2 } },
    "namespaces": [
      { "type": "pid" },
      { "type": "ipc" },
      { "type": "uts" },
      { "type": "mount" },
      {
        "type": "network",
        "path": "/var/run/netns/cni-xxx"
      }
    ],
    "seccomp": { "...": "默认 seccomp 策略,略" },
    "maskedPaths": ["..."],
    "readonlyPaths": ["..."]
  }
}

启动shim(containerd侧)

shim 是 containerd 定义的规范和协议,不是OCI Runtime Spec的一部分。

core/runtime/v2/shim_manager.go:默认Runtime是io.containerd.runc.v2resolveRuntimePath会解析最后两段,和containerd-shim-固定前缀拼接,最后是containerd-shim-runc-v2,底层是runc。

Go 复制代码
func (m *ShimManager) Start(ctx context.Context, id string, bundle *Bundle, opts runtime.CreateOpts) (_ ShimInstance, retErr error) {
    // 启动shim
	shim, err := m.startShim(ctx, bundle, id, opts)
    // 缓存shim
	m.shims.Add(ctx, shim)
	return shim, nil
}
func (m *ShimManager) startShim(ctx context.Context, bundle *Bundle, id string, opts runtime.CreateOpts) (*shim, error) {
	ns, err := namespaces.NamespaceRequired(ctx)
    // 这里根据Runtime配置(io.containerd.runsc.v1),找shim可执行程序
	runtimePath, err := m.resolveRuntimePath(opts.Runtime)
	b := shimBinary(bundle, shimBinaryConfig{
		runtime:      runtimePath,
		address:      m.containerdAddress,
		ttrpcAddress: m.containerdTTRPCAddress,
		socketDir:    m.socketDir,
		env:          m.env,
	})
	shim, err := b.Start(ctx, typeurl.MarshalProto(topts)
	return shim, nil
}

core/runtime/v2/binary.go:containerd fork-exec 出 shim 进程,shim进程独立于containerd。

启动 shim 命令如containerd-shim-runc-v2 -namespace k8s.io -address /run/containerd/containerd.sock -id <sandbox容器ID> start,shim通过标准输出返回shim的Unix Socket连接地址unix:///run/containerd/s/xxx

Go 复制代码
func (b *binary) Start(ctx context.Context, opts *types.Any, onClose func()) (_ *shim, err error) {
	cmd, err := client.Command(
		ctx,
		&client.CommandConfig{
            // sandbox容器ID
			ID:           b.bundle.ID,
			RuntimePath:  b.runtime,
			GRPCAddress:  b.containerdAddress,
			TTRPCAddress: b.containerdTTRPCAddress,
            // shim进程的工作目录是sandbox bundle路径
			WorkDir:      b.bundle.Path,
			Opts:         opts,
			Env:          b.env,
			LogLevel:     log.GetLevel(),
            // start子命令
			Action:       "start",
			SocketDir:    b.socketDir,
		})
    // fork-exec 出 containerd-shim-runc-v2 进程
	out, err := cmd.CombinedOutput()
	response := bytes.TrimSpace(out)
	os.WriteFile(filepath.Join(b.bundle.Path, "shim-binary-path"), []byte(b.runtime), 0600)
    // shim返回连接地址
	params, err := parseStartResponse(response)
    // 与shim建立连接
	conn, err := makeConnection(ctx, b.bundle.ID, params, onCloseWithShimLog, client.AnonDialer)
    // 持久化shim地址,containerd重启可重新与shim建立连接
	writeBootstrapParams(filepath.Join(b.bundle.Path, "bootstrap.json"), params)
	address := fmt.Sprintf("%s+%s", params.Protocol, params.Address)
	return &shim{
		bundle:  b.bundle,
		client:  conn,
		address: address,
		version: int(params.Version),
	}, nil
}

启动shim(shim侧)

pkg/shim/shim.go:shim侧实际上分成两步

1)shim start:启动常驻shim进程,返回socket地址(/run/containerd/s/xxx)给containerd,自己退出;

2)shim:被1启动,提供ttrpc服务,用于后续容器管理;

Go 复制代码
func run(ctx context.Context, manager Shim, config Config) error {
	switch action {
	case "delete":
        // ...
		return nil
	case "start":
        // 进程一走这里:由containerd启动,带start指令
        // 标准输入 请求
		input, err := io.ReadAll(io.LimitReader(os.Stdin, 10<<20))
		var params bootapi.BootstrapParams
		if len(input) == 0 || proto.Unmarshal(input, &params) != nil {
		}
        // start,返回bootapi.BootstrapResult
		result, err := manager.Start(ctx, &params)
        // 标准输出 响应
		data, err := proto.Marshal(result)
		os.Stdout.Write(data)
        // start进程退出
		return nil
	}
    // 进程二走这里:由进程一启动 常驻进程 提供ttrpc服务
    server, err := newServer(...)
	for _, srv := range ttrpcServices {
		srv.RegisterTTRPC(server)
	}
    serve(ctx, server, signals, sd.Shutdown, pprofHandler)
}

cmd/containerd-shim-runc-v2/manager/manager_linux.go:start构建并启动常驻shim进程。

Go 复制代码
func (manager) Start(ctx context.Context, opts *bootapi.BootstrapParams) (_ *bootapi.BootstrapResult, retErr error) {
	var params bootapi.BootstrapResult
	params.Version = 3
	params.Protocol = "ttrpc"
	id := opts.GetInstanceID()
	// 子进程命令,没有start
	cmd, err := newCommand(ctx, id, opts.GetContainerdGrpcAddress(), opts.GetContainerdTtrpcAddress(), debugLog)
	grouping := id
	spec, err := readSpec()
	var sockets []*shimSocket
	// /run/containerd/s
	socketDir := opts.GetSocketDir()
	// 创建socket=/run/containerd/s/sha256(containerd_socket,containerd_namespace,sandbox_id)
	s, err := newShimSocket(ctx, socketDir, opts.GetContainerdGrpcAddress(), grouping, false)
	sockets = append(sockets, s)
	// 传递socket文件(s.f)描述符给子进程
	cmd.ExtraFiles = append(cmd.ExtraFiles, s.f)
    // 启动子进程
	err := cmd.Start()
	params.Address = sockets[0].addr
	return &params, nil
}
func newCommand(ctx context.Context, id, containerdAddress, containerdTTRPCAddress string, debug bool) (*exec.Cmd, error) {
	ns, err := namespaces.NamespaceRequired(ctx)
    // self=containerd-shim-runc-v2
	self, err := os.Executable()
	cwd, err := os.Getwd()
	args := []string{
		"-namespace", ns,
		"-id", id,
		"-address", containerdAddress,
	}
	cmd := exec.Command(self, args...)
	cmd.Dir = cwd
	cmd.Env = append(os.Environ(), "GOMAXPROCS=4")
	cmd.Env = append(cmd.Env, "OTEL_SERVICE_NAME=containerd-shim-"+id)
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Setpgid: true,
	}
	return cmd, nil
}

runc create

core/runtime/v2/shim.go:shim启动完毕后,containerd 发送 CreateTaskRequest 给shim,其中包含Bundle路径和rootfs配置。

Go 复制代码
func (s *shimTask) Create(ctx context.Context, opts runtime.CreateOpts) (runtime.Task, error) {
	topts := opts.TaskOptions
	request := &task.CreateTaskRequest{
        // sandbox容器ID
		ID:         s.ID(),
        // /run/containerd/io.containerd.runtime.v2.task/k8s.io/<sandbox容器ID>
		Bundle:     s.Bundle(),
        ...
	}
	for _, m := range opts.Rootfs {
		request.Rootfs = append(request.Rootfs, &types.Mount{
			Type:    m.Type,
			Source:  m.Source,
			Target:  m.Target,
			Options: m.Options,
		})
	}
	_, err := s.task.Create(ctx, request)
	return s, nil
}

cmd/containerd-shim-runc-v2/task/service.go:shim侧,根据rootfs配置执行overlayfs挂载,执行runc create。

Go 复制代码
func (s *service) Create(ctx context.Context, r *taskAPI.CreateTaskRequest) (*taskAPI.CreateTaskResponse, error) {
    // 创建container
    container, err := runc.NewContainer(ctx, s.platform, r)
    // 缓存containerId -> container
    s.containers[r.ID] = container
    // 响应容器进程pid
    return &taskAPI.CreateTaskResponse{Pid: uint32(container.Pid())}, nil
}
func NewContainer(ctx context.Context, platform stdio.Platform, r *task.CreateTaskRequest) (_ *Container, retErr error) {
	ns, err := namespaces.NamespaceRequired(ctx)
	opts := &options.Options{}
	var pmounts []process.Mount
	for _, m := range r.Rootfs {
		pmounts = append(pmounts, process.Mount{
			Type:    m.Type,
			Source:  m.Source,
			Target:  m.Target,
			Options: m.Options,
		})
	}
	config := &process.CreateConfig{
		ID:               r.ID,
		Bundle:           r.Bundle,
        ...
	}
	var mounts []mount.Mount
	for _, pm := range pmounts {
		mounts = append(mounts, mount.Mount{
			Type:    pm.Type,
			Source:  pm.Source,
			Target:  pm.Target,
			Options: pm.Options,
		})
	}
    // overlayfs挂载
	err := mount.All(mounts, rootfs)
	p, err := newInit(...)
    // 执行runc create
	err := p.Create(ctx, config)
	container := &Container{
		ID:              r.ID,
		Bundle:          r.Bundle,
		process:         p,
		processes:       make(map[string]process.Process),
		reservedProcess: make(map[string]struct{}),
	}
	pid := p.Pid()
	return container, nil
}

overlayfs 挂载,等同如下命令:

  1. lowerdir = 镜像里的N个只读层,比如sandbox的只有1个只读层,pause镜像公用/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs

  2. upperdir = 一个容器可写层,每个容器不同,id自增,如/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/<id>/fs

  3. workdir = overlay临时工作目录,每个容器不同,id自增,如/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/<id>/work

  4. mergedir = 挂载点 = Bundle路径/rootfs = /run/containerd/io.containerd.runtime.v2.task/k8s.io/容器id/rootfs

Shell 复制代码
mount -t overlay overlay -o lowerdir=<lowerdir>,upperdir=<upperdir>,workdir=<workdir> <mergedir>

等价 runc 命令runc create 后 产生 runc init 进程,处于 created(暂停) 状态,容器运行环境都准备完毕(cgroup/namespace)。

Shell 复制代码
runc --root /run/containerd/runc/k8s.io \
    --log Bundle路径/log.json --log-format json \
    create --bundle Bundle路径 \
    --pid-file Bundle路径/init.pid 容器id

1.5. 启动 Sandbox(Task.Start)

client/task.go:containerd 发送 StartRequest(containerId),shim返回pause进程的pid。

Go 复制代码
func (t *task) Start(ctx context.Context) error {
	r, err := t.client.TaskService().Start(ctx, &tasks.StartRequest{
		ContainerID: t.id,
	})
	t.pid = r.Pid
	return nil
}

cmd/containerd-shim-runc-v2/process/init.go:shim侧执行runc start。

Go 复制代码
func (p *Init) start(ctx context.Context) error {
    return p.runtime.Start(ctx, p.id)
}

等价 runc 命令:至此容器中的用户程序开始运行,sandbox容器对应/pause程序。

Shell 复制代码
runc --root /run/containerd/runc/k8s.io \
  --log Bundle路径/log.json --log-format json \
  start 容器id

二、启动普通容器

要点:

  1. CreateContainer 只创建 containerd 容器对象 + snapshot + OCI spec,不启动进程

  2. StartContainerNewTask + Start,触发 runc;

  3. 业务容器通过 WithSandbox(sandboxID) 加入 sandbox shim,不再新起 shim 进程

2.1. CreateContainer

CRI 入参 / 出参

方向 字段 含义
入参 pod_sandbox_id 归属 sandbox
入参 config 容器配置(镜像、命令、挂载、资源等)
入参 sandbox_config sandbox配置
出参 container_id 新容器 ID

等价crictlcrictl create <sandbox-id> <container-config.json> <pod-config.json>

关键步骤

internal/cri/server/container_create.go

1)根据sandboxId查询内存sandbox信息;

2)解析镜像(kubelet会查询镜像是否存在,主动调用CRI拉取镜像,这里已经有镜像了);

3)创建container对象,持久化到本地boltdb;

Go 复制代码
func (c *criService) CreateContainer(ctx context.Context, r *runtime.CreateContainerRequest) (*runtime.CreateContainerResponse, error) {
    // 1. 内存读取sandbox数据
    sandbox, _ := c.sandboxStore.Get(r.GetPodSandboxId())
    cstatus, _ := c.sandboxService.SandboxStatus(ctx, sandbox.Sandboxer, sandbox.ID, false)
    sandboxPid := cstatus.Pid 
    // 2. 解析镜像(kubelet 已 Pull)
    image, _ := c.LocalResolve(config.GetImage().GetImage())
    // 3. 创建container对象,持久化到本地boltdb
    c.createContainer(&createContainerRequest{
        sandboxID: sandboxID,
        sandboxPid: sandboxPid,
        ...
    })
    return &runtime.CreateContainerResponse{ContainerId: id}, nil
}

createContainer 内部

  1. buildContainerSpec:组装OCI Runtime Spec;

  2. namespace:业务容器的 linux.namespaces 中,net / ipc / uts 均指向 sandbox已创建的命名空间;

  3. 写入本地boltdb,同sandbo,数据文件位置为:/var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db

此阶段 无 runc 调用,仅创建容器记录和rootfs等

Go 复制代码
func (c *criService) createContainer(r *createContainerRequest) (string, error) {
    // 1. 构造 OCI Spec:network/pid/ipc/uts 加入 sandbox 命名空间
    spec, _ := c.buildContainerSpec(..., r.sandboxPid, r.NetNSPath, ...)
    // 2. NewContainer:snapshot + spec + runtime + WithSandbox
    opts := []containerd.NewContainerOpts{
        containerd.WithSnapshotter(...),
        customopts.WithNewSnapshot(r.containerID, *r.containerdImage, ...),
        containerd.WithSpec(spec, specOpts...),
        containerd.WithRuntime(runtimeName, runtimeOption),
    }
    opts = append(opts, containerd.WithSandbox(r.sandboxID))
    cntr, _ := c.client.NewContainer(r.ctx, r.containerID, opts...)
    // 3. 写入 containerStore
    c.containerStore.Add(container)
    return containerRootDir, nil
}
// internal/cri/opts/spec_opts.go
func WithPodNamespaces(...) oci.SpecOpts {
	namespaces := config.GetNamespaceOptions()
    // net/ipc/uts会进入sandbox的namespace
	opts := []oci.SpecOpts{
		oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.NetworkNamespace, Path: GetNetworkNamespace(sandboxPid)}),
		oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.IPCNamespace, Path: GetIPCNamespace(sandboxPid)}),
		oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.UTSNamespace, Path: GetUTSNamespace(sandboxPid)}),
	}
}

2.2. StartContainer

CRI 入参 / 出参

方向 字段 含义
入参 container_id CreateContainer 返回的 ID
出参 (空) 成功即 RUNNING

等价crictlcrictl start <container-id>

关键步骤

internal/cri/server/container_start.go:调用shim创建和启动容器。

Go 复制代码
func (c *criService) StartContainer(ctx context.Context, r *runtime.StartContainerRequest) (*runtime.StartContainerResponse, error) {
    cntr, _ := c.containerStore.Get(r.GetContainerId())
    sandbox, _ := c.sandboxStore.Get(meta.SandboxID)
    // 1. NewTask:TaskService.Create → shim runc create
    task, _ := container.NewTask(...)
    // 2. task.Start → shim runc start
    task.Start(ctx)
    // 3. 更新状态、启动 exit monitor
    c.startContainerExitMonitor(..., exitCh)
    return &runtime.StartContainerResponse{}, nil
}

复用 sandbox shim

core/runtime/v2/shim_manager.go:业务容器 NewContainer 时设置了 WithSandbox(sandboxID)ShimManager.Start 发现 sandbox 已存在,不再启动新 shim ,而是通过 sandbox 的 bootstrap 参数连接到 pause 容器已有的 containerd-shim-runc-v2 进程。

Go 复制代码
if opts.SandboxID != "" {
    // 内存记录了 sandboxId -> shim
    process, _ := m.Get(ctx, opts.SandboxID)
    params, _ := restoreBootstrapParams(process.Bundle())
    // loadShim:复用已有 TTRPC 连接
}

因此一个 Pod 通常只有一个 shim 进程,管理 pause + 所有业务容器的多个 runc 容器。

但是每个容器有自己的Bundle,对应不同的OCI Runtime Spec容器配置。

runc 命令(与 sandbox 相同模式)

Shell 复制代码
# CreateContainer 后 StartContainer 的 NewTask 阶段
runc --root /run/containerd/runc/k8s.io create \
     --bundle /run/containerd/io.containerd.runtime.v2.task/k8s.io/<container-id> \
     <container-id>

# StartContainer 的 task.Start 阶段
runc --root /run/containerd/runc/k8s.io start <container-id>

三、runc侧实现

shim创建并启动容器对应两条runc命令:

1)runc create:拉起 runc init,完成 namespace / cgroup / rootfs,进程停在 created,等 exec fifo;

2)runc start:打开 exec fifo,唤醒 init,execve 成用户进程(sandbox 即 /pause);

3.1. runc create

create.goutils_linux.gostartContainer(..., CT_ACT_CREATE)

1)读 Bundle 下 config.json 得到 OCI Runtime Spec;

2)createContainer 创建 Container 对象(含 namespaces / cgroups / mounts);

3)runner.run 创建容器;

Go 复制代码
// create.go
var createCommand = cli.Command{
	Action: func(context *cli.Context) error {
		status, err := startContainer(context, CT_ACT_CREATE, nil)
	},
}
// utils_linux.go
func startContainer(context *cli.Context, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) {
	// 1. 从 Bundle/config.json 中加载 OCI Runtime Spec
	spec, err := setupSpec(context)
	// 2. 入参容器ID
	id := context.Args().First()
	// 3. 构造container对象
	container, err := createContainer(context, id, spec)
	r := &runner{
		// ...
		container:       container,
		init:            true,
	}
	// 4. 启动容器
	return r.run(spec.Process)
}
func (r *runner) run(config *specs.Process) (int, error) {
	process, err := newProcess(config)
	process.Init = r.init
	switch r.action {
	case CT_ACT_CREATE:
		err = r.container.Start(process)
	}
}

libcontainer/container_linux.go:调用runc init前,创建/run/containerd/runc/k8s.io/<container-id>/exec.info管道,用于卡住容器进程,后面需要runc start才会实际执行容器里的进程。

Go 复制代码
func (c *Container) start(process *Process) (retErr error) {
	// 创建exec.fifo文件
	c.createExecFifo()
	// 创建runc init命令
	parent, err := c.newParentProcess(process)
	// 启动runc init进程
	parent.start()
	return nil
}
func (c *Container) createExecFifo() (retErr error) {
	fifoName := filepath.Join(c.stateDir, execFifoFilename)
}

libcontainer/container_linux.go

  1. 启动runc init作为最终容器进程;

  2. 通过initSock,发送bootstrapData给init进程,用于构建namespace,发送initConfig,用于rootfs挂载和构建等;

  3. 通过syncSock,与init进程通讯,收到procHooks触发cgroup阈值设置,收到procReady代表init完成,持久化容器状态到/run/containerd/runc/k8s.io/<container-id>/state.json

Go 复制代码
func (p *initProcess) start() error {
	// 1. 启动 runc init(先跑 nsexec,再进 Go)
	p.cmd.Start()
	// 2. 把runc init 的 pid 放进 cgroup
	p.manager.Apply(p.pid())
	// 3. initSock 发 bootstrap(namespace paths、clone flags、uidmap...)
	io.Copy(p.comm.initSockParent, p.bootstrapData)
	// 4. wait stage-1,收init stage2 返回pid,即最终容器进程pid
	childPid, err := p.getChildPid()
	// wait stage-0,并把 cmd.Process 换成 stage-2
	p.waitForChildExit(childPid)
	// 5. initSock 下发完整 initConfig(rootfs、进程参数、hooks...)
	utils.WriteJSON(p.comm.initSockParent, p.config)
	// 6. syncSock 与 runc init 通讯,等返回procReady,流程结束
	parseSync(p.comm.syncSockParent, func(sync *syncT) error {
		switch sync.Type {
		case procReady:
		    // 收到runc init返回procReady
			seenProcReady = true
			// 持久化容器状态到state.json
			p.container.updateState(p)
			writeSync(..., procRun)
		case procHooks:
		    // 收到runc init返回procHooks,runc create设置cgroup限制
			p.manager.Set(p.config.Config.Cgroups.Resources)
		// procMountPlease / procSeccomp ...
		}
	})
}

3.2. create 与 init 通讯

libcontainer/process_linux.gorunc create 进程侧创建两对已连通的 Unix socket,再通过 cmd.ExtraFiles + 环境变量把 child 端 fd 传给 runc init

Go 复制代码
func newProcessComm() (*processComm, error) {
	comm.initSockParent, comm.initSockChild, err = utils.NewSockPair("init")
	comm.syncSockParent, comm.syncSockChild, err = newSyncSockpair("sync")
	comm.logPipeParent, comm.logPipeChild, err = os.Pipe()
	return &comm, nil
}

container_linux.gorunc create 进程 组装 runc init 命令。本质是内核 socketpair,两端共享同一 socket 对象,与 netns / IP 无关;fork-exec 继承 fd 后即可跨进程读写。

Go 复制代码
cmd.ExtraFiles = append(cmd.ExtraFiles, comm.initSockChild)
cmd.Env = append(cmd.Env, "_LIBCONTAINER_INITPIPE="+strconv.Itoa(...))
cmd.ExtraFiles = append(cmd.ExtraFiles, comm.syncSockChild.File())
cmd.Env = append(cmd.Env, "_LIBCONTAINER_SYNCPIPE="+strconv.Itoa(...))

container_linux.go:另外exec.fifo的fd也在这里传递给runc init。

Go 复制代码
func (c *Container) includeExecFifo(cmd *exec.Cmd) error {
	fifoName := filepath.Join(c.stateDir, execFifoFilename)
	fifo, err := os.OpenFile(fifoName, unix.O_PATH|unix.O_CLOEXEC, 0)
	c.fifo = fifo
	cmd.ExtraFiles = append(cmd.ExtraFiles, fifo)
	cmd.Env = append(cmd.Env,
		"_LIBCONTAINER_FIFOFD="+strconv.Itoa(stdioFdCount+len(cmd.ExtraFiles)-1))
	return nil
}

3.3. runc init - nsexec - 进命名空间

init.go 空白 import nsenter,且 Args[1]=="init" 时走 libcontainer.Init()nsexec()cgo constructor ,在 Go runtime 启动前跑完,避免多线程下 setns 问题。

Go 复制代码
import (
	_ "github.com/opencontainers/runc/libcontainer/nsenter"
)
func init() {
	if len(os.Args) > 1 && os.Args[1] == "init" {
		libcontainer.Init()
	}
}

package nsenter
/*
#cgo CFLAGS: -Wall
extern void nsexec();
void __attribute__((constructor)) init(void) {
	nsexec();
}
*/
import "C"

libcontainer/nsenter/nsexec.c 三阶段,每个阶段的进程之间通过socketpair通讯

  1. STAGE_PARENTstage-0,run create 直接创建的 子进程,读取initSock中的bootstrapData,clone出stage-1,通过initSock返回run create另外两个stage的pid,等待两个stage结束后直接退出;

  2. STAGE_CHILDstage-1setns / unshare 进目标命名空间,再 clone stage-2,然后退出;

  3. STAGE_INITstage-2,必须再 fork 一次的原因是 PID namespace 只对 子进程 生效,这是 最终容器 进程 ,C返回后 Go runtime 接管,继续 libcontainer.Init

Go 复制代码
void nsexec(void)
{
	int pipenum;
	jmp_buf env;
	int sync_child_pipe[2], sync_grandchild_pipe[2];
	struct nlconfig_t config = { 0 };
	// runc create传入,initsock的fd
	pipenum = getenv_int("_LIBCONTAINER_INITPIPE");
	// 从runc create获取initSock写入的bootstrapData
	nl_parse(pipenum, &config);
	// 开启2个socketpair,与stage-1和stage-2通讯
	if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_child_pipe) < 0)
	if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_grandchild_pipe) < 0)
	switch (setjmp(env)) {
	case STAGE_PARENT:{
			int len;
			pid_t stage1_pid = -1, stage2_pid = -1;
			bool stage1_complete, stage2_complete;
			// clone 一个 runc init,到setjmp,直接进入STAGE_CHILD
			stage1_pid = clone_parent(&env, STAGE_CHILD);
			syncfd = sync_child_pipe[1];
			stage1_complete = false;
			// 等stage-1结束
			while (!stage1_complete) {
				enum sync_t s;
				// 与stage-1通讯,推进状态
				xread(syncfd, &s, sizeof(s));
				switch (s) {
				case SYNC_RECVPID_PLS:
					// 通过initSock 返回runc create stage-1和2的pid
					len =
					    dprintf(pipenum, "{\"stage1_pid\":%d,\"stage2_pid\":%d}\n", stage1_pid,
						    stage2_pid);
					break;
				case SYNC_CHILD_FINISH:
					stage1_complete = true;
					stage1_pid = -1;
					break;
				}
			}
			syncfd = sync_grandchild_pipe[1];
			stage2_complete = false;
			// 等stage-2结束
			while (!stage2_complete) {
				enum sync_t s;
				// 与stage-2通讯,推进状态
				xread(syncfd, &s, sizeof(s));
				// 从syncfd等SYNC_CHILD_FINISH...
			}
			exit(0);
		}
		break;
	case STAGE_CHILD:{
			syncfd = sync_child_pipe[0];
			// 根据bootstrapData中的ns配置,进入namespace
			if (config.namespaces)
				join_namespaces(config.namespaces);
			try_unshare(config.cloneflags, "remaining namespaces");
			// ...
			// 通知stage-0 stage-1结束
			s = SYNC_CHILD_FINISH;
			xwrite(syncfd, &s, sizeof(s));
			exit(0);
		}
		break;
	case STAGE_INIT:{
		    syncfd = sync_grandchild_pipe[0];
			// ...
			// 通知stage-0 stage-2结束
			s = SYNC_CHILD_FINISH;
			xwrite(syncfd, &s, sizeof(s));
			// 进入init.go
			return;
		}
		break;
	}
}
static nsset_t __join_namespaces(nsset_t allow, struct namespace_t *ns_list, size_t ns_len)
{
	nsset_t joined = 0;
	for (size_t i = 0; i < ns_len; i++) {
		struct namespace_t *ns = &ns_list[i];
		int type = nstype(ns->type);
		// setns 进入指定namespace
		err = setns(ns->fd, type);
	}
}

void try_unshare(int flags, const char *msg)
{
	// unshare 新建ns,比如sandbox容器除了net都是新建
	if (unshare(flags) == 0) {
		return;
	}
}

3.4. runc init - Go部分

上面nsexec最终stage-2放行后,执行init.go主程序。

libcontainer/init_linux.go:从 initSock 读 initConfig,再 containerInit

Go 复制代码
func Init() {
	err := startInitialization()
}
func startInitialization() (retErr error) {
	// 1. 和runc create通讯的syncSock
	envSyncPipe := os.Getenv("_LIBCONTAINER_SYNCPIPE")
	syncPipeFd, err := strconv.Atoi(envSyncPipe)
	syncPipe := newSyncSocket(os.NewFile(uintptr(syncPipeFd), "sync"))
	// 2. 和runc create通讯的initSock
	envInitPipe := os.Getenv("_LIBCONTAINER_INITPIPE")
	initPipeFd, err := strconv.Atoi(envInitPipe)
	initPipe := os.NewFile(uintptr(initPipeFd), "init")
	// 3. 打开exec.fifo文件
	var fifoFile *os.File
	fifoFd, err := strconv.Atoi(os.Getenv("_LIBCONTAINER_FIFOFD"))
	fifoFile = os.NewFile(uintptr(fifoFd), "initfifo")
    // 4. 从initSock读取initConfig
	var config initConfig
	json.NewDecoder(initPipe).Decode(&config)
	// 5. 执行init
	return containerInit(it, &config, syncPipe, consoleSocket, pidfdSocket, fifoFile, logPipe)
}
func containerInit(...) error {
	i := &linuxStandardInit{...}
	return i.Init()
}

libcontainer/standard_init_linux.go 主要步骤:

1)setupNetwork / setupRoute(容器内把 lo 等拉起); 2)prepareRootfs:按照配置proc、sys、dev等挂到{rootfs}/proc、{rootfs}/sys、{rootfs}/dev,最终通过pivot_root把rootfs变为容器的根目录; 3)hostname / apparmor / sysctl / maskedPaths / no_new_privs / seccomp; 4)syncParentReady:用syncSock与run create进程通讯,等返回procRun; 5)阻塞打开 exec.fifo 并写入一字节 ,需要等到exec.fifo有人接收才能进下一步; 6)当fifo被人读走,execve 执行容器中的用户进程,runc init变为用户程序,比如sandbox是/pause;

Go 复制代码
func (l *linuxStandardInit) Init() error {
	setupNetwork(l.config)
	// pivot_root rootfs变为容器根目录
	prepareRootfs(l.pipe, l.config)
	// ... finalizeNamespace / LookPath / seccomp ...
	// 通过syncSock通知runc create procReady,等run create返回procRun
	syncParentReady(l.pipe)
	// 阻塞直到 start
	fifoFile, err := pathrs.Reopen(l.fifoFile, unix.O_WRONLY|unix.O_CLOEXEC) 
	fifoFile.Write([]byte("0"))
	 // exec 容器命令 如/pause
	unix.Exec(name, l.config.Args[0:], os.Environ())
}

3.5. runc start

start.go:从state.json加载容器状态,读取exec.fifo放行runc init执行容器中的用户程序。

Go 复制代码
// 1. 从state.json加载容器状态
container, err := getContainer(context)
status, err := container.Status()
switch status {
case libcontainer.Created:
	return container.Exec()
}
// container_linux.go
func (c *Container) exec() error {
	path := filepath.Join(c.stateDir, "exec.fifo")
	// 2. 打开 exec.fifo
	result := <-awaitFifoOpen(path)
	// 3. 读取exec.fifo,触发runc init放行,exec执行容器中的用户程序
	return handleFifoResult(result)
}

四、ListPodSandbox/ListContainers

kubelet的PLEG循环每秒调用CRI的ListPodSandbox/ListContainers接口,发现容器状态变更。

比如容器停机,kubelet根据restartPolicy决定是否需要启动新容器。

internal/cri/server/container_list.go:以ListContainers为例,containerd侧实际都是从内存获取容器元数据和状态返回。

Go 复制代码
func (c *criService) ListContainers(ctx context.Context, r *runtime.ListContainersRequest) (*runtime.ListContainersResponse, error) {
    // 内存读取container元数据
	containersInStore := c.containerStore.List()
	var containers []*runtime.Container
	for _, container := range containersInStore {
	    // 转换为CRI对应模型
		containers = append(containers, toCRIContainer(container))
	}
	containers = c.filterCRIContainers(containers, r.GetFilter())
	return &runtime.ListContainersResponse{Containers: containers}, nil
}
func toCRIContainer(container containerstore.Container) *runtime.Container {
    // 获取内存中的容器状态
	status := container.Status.Get()
	return &runtime.Container{
		Id:           container.ID,
		PodSandboxId: container.SandboxID,
		Metadata:     container.Config.GetMetadata(),
		Image:        container.Config.GetImage(),
		ImageRef:     container.ImageRef,
		ImageId:     container.ImageRef,
		// 映射到CRI的状态
		State:       status.State(),
		CreatedAt:   status.CreatedAt,
		Labels:      container.Config.GetLabels(),
		Annotations: container.Config.GetAnnotations(),
	}
}
func (s Status) State() runtime.ContainerState {
	if s.Unknown {
		return runtime.ContainerState_CONTAINER_UNKNOWN
	}
	if s.FinishedAt != 0 {
		return runtime.ContainerState_CONTAINER_EXITED
	}
	if s.StartedAt != 0 {
		return runtime.ContainerState_CONTAINER_RUNNING
	}
	if s.CreatedAt != 0 {
		return runtime.ContainerState_CONTAINER_CREATED
	}
	return runtime.ContainerState_CONTAINER_UNKNOWN
}

internal/cri/server/container_start.go:containerd需要监控容器退出,更新容器状态。

1)启动容器前,containerd 对 shim 发起 Wait RPC,shim会阻塞该请求,直到观察到容器退出,返回containerd退出结果;

2)容器状态会持久化到/var/lib/containerd/io.containerd.grpc.v1.cri/containers/<id>/status并更新到内存;

Go 复制代码
func (c *criService) StartContainer(...) (...) {
    // 调用shim -> shim阻塞等容器pid退出后响应containerd
	exitCh, err := task.Wait(ctrdutil.NamespacedContext())
    // 调用shim -> shim执行runc start <container-id>
	task.Start(ctx);
    // 持久化container状态 并 更新到内存
    cntr.Status.UpdateSync(func(status containerstore.Status) (containerstore.Status, error) {
        status.Pid = task.Pid()
        status.StartedAt = time.Now().UnixNano()
        return status, nil
    })
    // 监控exitCh
	c.startContainerExitMonitor(context.Background(), id, task.Pid(), exitCh)
}

internal/cri/server/events.go:containerd开启一个协程,如果exitCh有数据,代表shim返回容器退出,持久化容器状态并更新内存,至此PLEG能感知到容器状态变更(FinishedAt非0)。

Go 复制代码
func (c *criService) startContainerExitMonitor(ctx context.Context, id string, pid uint32, exitCh <-chan containerd.ExitStatus) <-chan struct{} {
	stopCh := make(chan struct{})
	go func() {
		defer close(stopCh)
		select {
		case exitRes := <-exitCh:
			exitStatus, exitedAt, err := exitRes.Result()
			e := &eventtypes.TaskExit{...}
			err = func() error {
				cntr, err := c.containerStore.Get(e.ID)
				// 处理容器退出
				err := c.handleContainerExit(dctx, e, cntr, cntr.SandboxID)
			return
		case <-ctx.Done():
		}
	}()
	return stopCh
}
func (c *criService) handleContainerExit(...) error {
	if e.ExitStatus == oomExitCodeInLinux && cntr.Status.Get().Reason != oomExitReason {
      // OOM更新Reason 
      err = cntr.Status.UpdateSync(func(status containerstore.Status) (containerstore.Status, error) {
          status.Reason = oomExitReason
          return status, nil
      })
	}
	// 持久化容器状态并更新内存
	err = cntr.Status.UpdateSync(func(status containerstore.Status) (containerstore.Status, error) {
		if status.FinishedAt == 0 {
			status.Pid = 0
			status.FinishedAt = protobuf.FromTimestamp(e.ExitedAt).UnixNano()
			status.ExitCode = int32(e.ExitStatus)
		}
		return status, nil
	})
}

internal/cri/server/restart.go:containerd重启后,需要重新加载容器元数据和状态到内存,并重新向shim发起Wait RPC。

1)容器元数据来源于boltdb:/var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db

2)每个容器状态在各自的状态文件/var/lib/containerd/io.containerd.grpc.v1.cri/containers/<id>/status

Go 复制代码
func (c *criService) recover(ctx context.Context) error {
    // sandbox数据恢复到内存...
    // 从boltdb加载容器元数据
	containers, err := c.client.Containers(...)
	for _, container := range containers {
		eg.Go(func() error {
		    // 从status加载容器状态
			cntr, exitCh, pid, err := c.loadContainer(ctx2, container)
			// 恢复到内存
			c.containerStore.Add(cntr)
            // 监听容器退出
			c.startContainerExitMonitor(context.Background(), cntr.ID, pid, exitCh)
			return nil
		})
	}
}

总结

RunPodSandbox

  1. containerd:建 netns、调 CNI 拿 PodIP,组装 pause 的 Spec、snapshot 与 Bundle,拉起 shim 并下发 create/start,元数据落 bolt、状态进内存;

  2. shim:1Pod1进程,启动后返回 ttrpc 地址 给 containerd连接。create 把 snapshot 各层 overlay 挂到 Bundle/rootfs,再调 runc create;start 调 runc start;

  3. runc:create 读 config.json 拉起 init,nsexec 进 ns、设 cgroup、切根后停在 created 等 fifo;start 打开 fifo,init 变成 /pause;

CreateContainer

  1. containerd:建对象、snapshot 与 Spec,Spec 中 net/ipc/uts 加入 sandbox;

  2. shim:不参与;

  3. runc:不参与,不启动进程;

StartContainer

  1. containerd:复用 sandbox shim 下发 create/start,发送 Wait 给shim;

  2. shim:不新起进程,create 把 snapshot 各层 overlay 挂到 Bundle/rootfs,再调 runc create/start。Wait 阻塞至容器退出再返回;

  3. runc:同 sandbox,create 停在 created,start 放行后 init 变成业务进程;

  4. containerd:收到退出后更新内存 FinishedAt,List 供 PLEG 感知;

相关推荐
名字还没想好☜1 小时前
Python sqlite3 实战:事务提交、参数化防注入、WAL 并发与 row_factory 取字典
后端·python·编程语言
花间相见1 小时前
【AI应用开发|Agent记忆】—— Codex 与 Claude Code 持久化记忆对比:两条不用向量库的路线
后端·agent
葡萄城技术团队1 小时前
GcExcel V9.2 新特性揭秘:公式对象的无损往返
后端
蜗牛互联网1 小时前
Claude放宽生命科学限制:代价是验证、分级和30天留存
java·人工智能·后端
上进小菜猪1 小时前
被 32 位整数卡了 30 年脖子:PostgreSQL 事务号困局与 V9 的 64 位解法
后端
知识的搬运工旺仔3 小时前
CREATE INDEX CONCURRENTLY:线上建索引不阻塞 DML 的代价与坑
数据库·后端·sql
小蒜学长3 小时前
大学生健康饮食的智慧管理系统(代码+数据库+LW)
java·后端·springboot·大学生·健康饮食
小蒜学长3 小时前
基于Java的论坛数据可视化分析系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·数据可视化·论坛系统