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

-
kubelet:watch apiserver获取分配给当前Node的Pod,调用CRI接口,管理Pod生命周期
-
RunPodSandbox/PodSandboxStatus,创建Sandbox容器,获取Sandbox容器详情,包含PodIP; -
ImageStatus/PullImage,查询容器镜像是否存在,不存在拉取镜像; -
CreateContainer/StartContainer,创建和启动业务容器; -
ListPodSandbox/ListContainers,PLEG循环每秒扫描容器状态,更新内存缓存,发现容器下线重新拉起;
-
-
CRI:Container Runtime Interface,kubelet对接Runtime的接口定义,见github.com/kubernetes/...
-
CNI:Container Network Interface,用于在容器中配置网络接口,见github.com/containerne...
-
containerd:默认CRI实现,同类还有CRI-Ogithub.com/cri-o/cri-o;
-
shim:一个Pod对应一个shim常驻进程,containerd定义协议。默认containerd使用runc,如gVisor要接入自己的Runtime,需要自行实现如github.com/google/gvis...;
-
OCI Runtime Spec:OCI 运行时规范旨在定义容器的配置、执行环境和生命周期,github.com/opencontain...
-
Runtime:容器运行时,如runc是github.com/opencontain...,gVisor是github.com/google/gvis... ;
注:
-
kubelet:1.36.1
-
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.go:setupPodNetwork 从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.v2,resolveRuntimePath会解析最后两段,和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, ¶ms) != nil {
}
// start,返回bootapi.BootstrapResult
result, err := manager.Start(ctx, ¶ms)
// 标准输出 响应
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 ¶ms, 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 挂载,等同如下命令:
-
lowerdir = 镜像里的N个只读层,比如sandbox的只有1个只读层,pause镜像公用
/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs; -
upperdir = 一个容器可写层,每个容器不同,id自增,如
/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/<id>/fs; -
workdir = overlay临时工作目录,每个容器不同,id自增,如
/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/<id>/work; -
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
二、启动普通容器
要点:
-
CreateContainer只创建 containerd 容器对象 + snapshot + OCI spec,不启动进程; -
StartContainer才NewTask+Start,触发 runc; -
业务容器通过
WithSandbox(sandboxID)加入 sandbox shim,不再新起 shim 进程。
2.1. CreateContainer
CRI 入参 / 出参
| 方向 | 字段 | 含义 |
|---|---|---|
| 入参 | pod_sandbox_id |
归属 sandbox |
| 入参 | config |
容器配置(镜像、命令、挂载、资源等) |
| 入参 | sandbox_config |
sandbox配置 |
| 出参 | container_id |
新容器 ID |
等价crictl :crictl 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 内部
-
buildContainerSpec:组装OCI Runtime Spec;
-
namespace:业务容器的
linux.namespaces中,net/ipc/uts均指向 sandbox已创建的命名空间; -
写入本地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 |
等价crictl :crictl 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.go → utils_linux.go:startContainer(..., 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:
-
启动runc init作为最终容器进程;
-
通过initSock,发送bootstrapData给init进程,用于构建namespace,发送initConfig,用于rootfs挂载和构建等;
-
通过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.go:runc 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.go:runc 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通讯
-
STAGE_PARENT:stage-0,run create 直接创建的 子进程,读取initSock中的bootstrapData,clone出stage-1,通过initSock返回run create另外两个stage的pid,等待两个stage结束后直接退出; -
STAGE_CHILD:stage-1,setns/unshare进目标命名空间,再 clone stage-2,然后退出; -
STAGE_INIT:stage-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
-
containerd:建 netns、调 CNI 拿 PodIP,组装 pause 的 Spec、snapshot 与 Bundle,拉起 shim 并下发 create/start,元数据落 bolt、状态进内存;
-
shim:1Pod1进程,启动后返回 ttrpc 地址 给 containerd连接。create 把 snapshot 各层 overlay 挂到 Bundle/rootfs,再调 runc create;start 调 runc start;
-
runc:create 读 config.json 拉起 init,nsexec 进 ns、设 cgroup、切根后停在 created 等 fifo;start 打开 fifo,init 变成 /pause;
CreateContainer
-
containerd:建对象、snapshot 与 Spec,Spec 中 net/ipc/uts 加入 sandbox;
-
shim:不参与;
-
runc:不参与,不启动进程;
StartContainer
-
containerd:复用 sandbox shim 下发 create/start,发送 Wait 给shim;
-
shim:不新起进程,create 把 snapshot 各层 overlay 挂到 Bundle/rootfs,再调 runc create/start。Wait 阻塞至容器退出再返回;
-
runc:同 sandbox,create 停在 created,start 放行后 init 变成业务进程;
-
containerd:收到退出后更新内存 FinishedAt,List 供 PLEG 感知;