前言

Kubernetes:kubelet 源码分析之 pod 创建流程 介绍了 kubelet 创建 pod 的流程,containerd 源码分析:kubelet 和 containerd 交互 介绍了 kubelet 通过 cri 接口和 containerd 交互的过程,containerd 源码分析:启动注册流程 介绍了 containerd 作为高级容器运行时的启动流程。通过这三篇文章熟悉了 kubeletcontainerd 的行为,对于 containerd 如何通过 OCI 接口创建容器 container 并没有涉及。

kubelet 和 containerd 接口

本文将继续介绍 containerd 是如何创建容器 container 的。

ctr

在介绍创建容器前,首先简单介绍下 ctrctrcontainerd 的命令行客户端,本文会通过 ctr 进行调试和分析。

ctr CLI

作为命令行工具 ctr 包括一系列和 containerd 交互的命令。主要命令如下:

 1COMMANDS:
 2   plugins, plugin            provides information about containerd plugins
 3   containers, c, container   manage containers
 4   images, image, i           manage images
 5   run                        run a container
 6   snapshots, snapshot        manage snapshots
 7   tasks, t, task             manage tasks
 8   install                    install a new package
 9   oci                        OCI tools
10   shim                       interact with a shim directly

containers|c|container

不同与 Kubernetes 层面的 container,这里 ctr 命令管理的 containers 实际是管理存储在 boltDB 中的 container metadata。

创建 container

1# ctr c create docker.io/library/nginx:alpine nginx1
2# ctr c ls
3CONTAINER    IMAGE                             RUNTIME
4nginx1       docker.io/library/nginx:alpine    io.containerd.runc.v2

通过 boltbrowser 查看 boltDB 存储的 container metadata,container metadata 存储在目录 /var/lib/containerd/io.containerd.metadata.v1.bolt

boltDB container

tasks|t|task

task 是实际启动容器进程的命令,ctr task start 根据创建的 container 启动容器:

1# ctr t start nginx1
2/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
3/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
4...

run

ctr 的 run 命令,实际是 ctr c createctr t start 命令的组合。

接下来,使用 ctr run 命令做为调试参数分析完整的创建 container 容器的流程。

ctr 调试

ctr 代码集中在 containerd 项目中,配置 ctr 的调试参数:

 1{
 2   "version": "0.2.0",
 3   "configurations": [
 4      {
 5         "name": "ctr",
 6         "type": "go",
 7         "request": "launch",
 8         "mode": "auto",
 9         "program": "${fileDirname}",
10         "args": ["run", "docker.io/library/nginx:alpine", "nginx1"]
11      }
12   ]
13}

调试 ctr

调试 ctr

进入 run.Command 看其中做了什么。

 1// containerd/cmd/ctr/commands/run/run.go
 2// Command runs a container
 3var Command = &cli.Command{
 4	Name:      "run",
 5	Usage:     "Run a container",
 6   ...
 7   Action: func(context *cli.Context) error {
 8      ...
 9      // step1: 创建访问 containerd 的 client
10      client, ctx, cancel, err := commands.NewClient(context)
11		if err != nil {
12			return err
13		}
14		defer cancel()
15
16      // step2: 创建 container
17      container, err := NewContainer(ctx, client, context)
18		if err != nil {
19			return err
20		}
21      ...
22
23      opts := tasks.GetNewTaskOpts(context)
24		ioOpts := []cio.Opt{cio.WithFIFODir(context.String("fifo-dir"))}
25      // step3: 创建 task
26		task, err := tasks.NewTask(ctx, client, container, context.String("checkpoint"), con, context.Bool("null-io"), context.String("log-uri"), ioOpts, opts...)
27		if err != nil {
28			return err
29		}
30
31      ...
32      // step4: 启动 task
33      if err := task.Start(ctx); err != nil {
34			return err
35		}
36      ...
37   }
38}

NewContainer 中根据 client 创建 container。接着根据 container 创建 task,然后启动该 task 来启动容器。

创建 container

进入 NewContainer

 1// containerd/cmd/ctr/commands/run/run_unix.go
 2func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli.Context) (containerd.Container, error) {
 3   ...
 4   return client.NewContainer(ctx, id, cOpts...)
 5}
 6
 7// containerd/client/client.go
 8func (c *Client) NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error) {
 9   ...
10   container := containers.Container{
11		ID: id,
12		Runtime: containers.RuntimeInfo{
13			Name: c.runtime,
14		},
15	}
16   ...
17   // 调用 containerd 接口创建 container
18   r, err := c.ContainerService().Create(ctx, container)
19	if err != nil {
20		return nil, err
21	}
22	return containerFromRecord(c, r), nil
23}

重点在 Client.ContainerService().Create

 1// containerd/client/containerstore.go
 2func (r *remoteContainers) Create(ctx context.Context, container containers.Container) (containers.Container, error) {
 3	created, err := r.client.Create(ctx, &containersapi.CreateContainerRequest{
 4		Container: containerToProto(&container),
 5	})
 6	if err != nil {
 7		return containers.Container{}, errdefs.FromGRPC(err)
 8	}
 9
10	return containerFromProto(created.Container), nil
11}
12
13// containerd/api/services/containers/v1/containers_grpc.pb.go
14func (c *containersClient) Create(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {
15	out := new(CreateContainerResponse)
16	err := c.cc.Invoke(ctx, "/containerd.services.containers.v1.Containers/Create", in, out, opts...)
17	if err != nil {
18		return nil, err
19	}
20	return out, nil
21}

调用 /containerd.services.containers.v1.Containers/Create grpc 接口创建 container。container 并不是容器进程,而是存储在数据库中的 container metadata。

/containerd.services.containers.v1.Containers/Create 是由 containerdio.containerd.grpc.v1.containers 插件提供的服务:

1// containerd/plugins/services/service.go
2func (s *service) Create(ctx context.Context, req *api.CreateContainerRequest) (*api.CreateContainerResponse, error) {
3	return s.local.Create(ctx, req)
4}

插件实例调用 local 对象的 Create 方法创建 container。查看 local 对象具体指的什么。

 1// containerd/plugins/services/service.go
 2func init() {
 3	registry.Register(&plugin.Registration{
 4		Type: plugins.GRPCPlugin,
 5		ID:   "containers",
 6		Requires: []plugin.Type{
 7			plugins.ServicePlugin,
 8		},
 9		InitFn: func(ic *plugin.InitContext) (interface{}, error) {
10         // plugins.ServicePlugin:io.containerd.service.v1
11         // services.ContainersService:containers-service
12			i, err := ic.GetByID(plugins.ServicePlugin, services.ContainersService)
13			if err != nil {
14				return nil, err
15			}
16			return &service{local: i.(api.ContainersClient)}, nil
17		},
18	})
19}

local 对象是 containerdio.containerd.service.v1.containers-service 插件的实例。查看该实例的 Create 方法。

 1// containerd/plugins/services/containers/local.go
 2func (l *local) Create(ctx context.Context, req *api.CreateContainerRequest, _ ...grpc.CallOption) (*api.CreateContainerResponse, error) {
 3	var resp api.CreateContainerResponse
 4
 5	if err := l.withStoreUpdate(ctx, func(ctx context.Context) error {
 6		container := containerFromProto(req.Container)
 7
 8		created, err := l.Store.Create(ctx, container)
 9		if err != nil {
10			return err
11		}
12
13		resp.Container = containerToProto(&created)
14
15		return nil
16	}); err != nil {
17		return &resp, errdefs.ToGRPC(err)
18	}
19	...
20
21	return &resp, nil
22}

local.Create 调用 local.withStoreUpdate 方法创建 container。

1// containerd/plugins/services/containers/local.go
2func (l *local) withStoreUpdate(ctx context.Context, fn func(ctx context.Context) error) error {
3	return l.db.Update(l.withStore(ctx, fn))
4}

local.withStoreUpdate 调用 db 对象的 Update 方法创建 container。

 1// containerd/plugins/services/containers/local.go
 2func init() {
 3	registry.Register(&plugin.Registration{
 4		...
 5		InitFn: func(ic *plugin.InitContext) (interface{}, error) {
 6			m, err := ic.GetSingle(plugins.MetadataPlugin)
 7			if err != nil {
 8				return nil, err
 9			}
10			ep, err := ic.GetSingle(plugins.EventPlugin)
11			if err != nil {
12				return nil, err
13			}
14
15			db := m.(*metadata.DB)
16			return &local{
17				Store:     metadata.NewContainerStore(db),
18				db:        db,
19				publisher: ep.(events.Publisher),
20			}, nil
21		},
22	})
23}

db 对象是 io.containerd.metadata.v1 插件的实例,该插件通过 boltDB 提供 metadata 存储服务。

metadata 插件实际调用的是匿名函数 fn 的内容,在 fn 中通过 l.Store.Create(ctx, container) 将 container 的 metadata 信息注册到 boltDB 数据库中。

创建 container 的过程实际是将 container 信息注册到 boltDB 的过程。