Containerd 源码解析:(三)Containerd 容器创建流程

大家好,我是费益洲。Containerd 作为 Docker 的默认运行时,Docker 创建容器最终会调用 Containerd 的 GRPC 接口去创建和启动容器,本文将从 Containerd 的源码层面来详细介绍 Containerd 创建容器的流程。
前置条件
Containerd 源码版本:v1.6.33
Containerd 项目地址:https://github.com/containerd/containerd
🔖 需要注意的第三方库:
1️⃣ urfave/cli
-
主要作用:构建命令行工具 -
项目地址:https://github.com/urfave/cli
2️⃣ etcd-io/bbolt
-
主要作用:KV 数据库,用来存储和管理容器相关信息 -
项目地址:https://github.com/etcd-io/bbolt
ctr
❝
🚢 Containerd 有自己的命令行工具:ctr,ctr 包括一系列和 Containerd 交互的命令。本文依然以 docker run 命令作为主线阐述 Containerd 在创建容器的过程作为主线内容,但 ctr 也可以使用 run 命令创建并启动一个容器,这里对 ctr run 的命令执行过程做简要概述。
Containerd 本身并不具备管理和配置网络的能力,所以这里 ctr 只能执行单纯的容器运行命令:ctr run -d docker.io/library/nginx:latest nginx,命令执行的逻辑入口是cmd/ctr/app/main.go:
funcmain() { app := app.New() app.Commands = append(app.Commands, pluginCmds...)if err := app.Run(os.Args); err != nil { fmt.Fprintf(os.Stderr, "ctr: %s\n", err) os.Exit(1) }}
run 子命令在 app.New 函数中加入到根命令:
cmd/ctr/app/main.go
// line 58funcNew() *cli.App { ...// 所有子命令 app.Commands = append([]cli.Command{ ...// run子命令 run.Command, ... }, extraCmds...) ...}
run 子命令的定义如下:
cmd/ctr/commands/run/run.go
// line 92 运行容器子命令var Command = cli.Command{ Name: "run", Usage: "run a container", ...// line 133 Action: func(context *cli.Context)error { ...// step 1:line 166 创建访问 containerd 的 client client, ctx, cancel, err := commands.NewClient(context) ...// step 2:line 171 创建容器 container, err := NewContainer(ctx, client, context) ...// step 3:line 195 创建task task, err := tasks.NewTask(ctx, client, container, context.String("checkpoint"), con, context.Bool("null-io"), context.String("log-uri"), ioOpts, opts...) ...// step 4:line 230 启动taskif err := task.Start(ctx); err != nil {return err } ... }}
从上述代码的四个步骤可以看出,ctr run 运行容器的整体步骤和 docker run 运行容器的整体步骤是一致的:先创建容器、再创建 task、最后启动 task。本文着重阐述容器的创建过程,task 的创建和启动过程数据容器启动,后续文章会继续讲解。进入 NewContainer 函数:
cmd/ctr/commands/run/run_unix.go
// line 85funcNewContainer(ctx gocontext.Context, client *containerd.Client, context *cli.Context)(containerd.Container, error) { ...// line 354return client.NewContainer(ctx, id, cOpts...)}
继续调用 Containerd 的 client 中的 NewContainer 函数:
client.go
// line 271func(c *Client)NewContainer(ctx context.Context, id string, opts ...NewContainerOpts)(Container, error) { ...// line 289 调用Containerd的接口创建容器 r, err := c.ContainerService().Create(ctx, container) ...}
containerstore.go
// line 109func(r *remoteContainers)Create(ctx context.Context, container containers.Container)(containers.Container, error) { created, err := r.client.Create(ctx, &containersapi.CreateContainerRequest{ Container: containerToProto(&container), })if err != nil {return containers.Container{}, errdefs.FromGRPC(err) }return containerFromProto(&created.Container), nil}
最后,还是会调用到 Containerd 创建容器的 grpc 接口:
api/services/containers/v1/containers.pb.go
// line 729func(c *containersClient)Create(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption)(*CreateContainerResponse, error) { out := new(CreateContainerResponse) err := c.cc.Invoke(ctx, "/containerd.services.containers.v1.Containers/Create", in, out, opts...)if err != nil {returnnil, err }return out, nil}
通过上述的流程以及前文Docker 源码解析:(五)Docker Daemon 调用 Containerd 接口创建并启动容器可以看出,ctr run 和 docker run 最后的逻辑都会调用 Contianerd 的 grpc 接口来为 Containerd 创建容器,接下来我们看下 Containerd 中创建容器的具体实现。
具体实现
前文Containerd 源码解析:(二)Containerd 服务启动及插件注册流程已经阐述了 Containerd 中的容器创建是由 Containerd 的 io.containerd.grpc.v1.containers 插件提供的服务。这部分逻辑本文不再阐述,未查看此部分内容的同志们请移步查看。本章直接从容器创建的 Handler 开始:
api/services/containers/v1/containers.pb.go
func _Containers_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{})error, interceptorgrpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateContainerRequest)if err := dec(in); err != nil {returnnil, err }if interceptor == nil {return srv.(ContainersServer).Create(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/containerd.services.containers.v1.Containers/Create", } handler := func(ctx context.Context, req interface{})(interface{}, error) {return srv.(ContainersServer).Create(ctx, req.(*CreateContainerRequest)) }return interceptor(ctx, in, info, handler)}
从上述代码可以看到,grpc 接收到请求后,会调用 srv.(ContainersServer).Create 函数去创建容器,具体的实现如下:
services/containers/service.go
// line 99func(s *service)Create(ctx context.Context, req *api.CreateContainerRequest)(*api.CreateContainerResponse, error) {return s.local.Create(ctx, req)}
服务实例调用了 local 对象的 Create 方法来创建容器,local 对象是在服务注册的时候就完成了初始化:
services/containers/local.go
// line 39funcinit() { plugin.Register(&plugin.Registration{// line 41 Type: plugin.ServicePlugin,// line 42 ID: services.ContainersService, Requires: []plugin.Type{ plugin.EventPlugin, plugin.MetadataPlugin, }, InitFn: func(ic *plugin.InitContext)(interface{}, error) { m, err := ic.Get(plugin.MetadataPlugin)if err != nil {returnnil, err } ep, err := ic.Get(plugin.EventPlugin)if err != nil {returnnil, err }// line 57 bolt数据库实例初始化 db := m.(*metadata.DB)return &local{ Store: metadata.NewContainerStore(db), db: db, publisher: ep.(events.Publisher), }, nil }, })}
从上面代码中的 41、42 行可以看出,local 对象是 Contianerd 中 io.containerd.service.v1.containers-service 插件的实例,这个实例的初始化函数中还完成了对 bolt 数据库对象的初始化。那么继续查看 s.local.Create 函数的具体实现:
services/containers/local.go
// line 116func(l *local)Create(ctx context.Context, req *api.CreateContainerRequest, _ ...grpc.CallOption)(*api.CreateContainerResponse, error) {var resp api.CreateContainerResponseif err := l.withStoreUpdate(ctx, func(ctx context.Context)error {// line 119~132 创建容器的fn匿名函数 container := containerFromProto(&req.Container) created, err := l.Store.Create(ctx, container)if err != nil {return err } resp.Container = containerToProto(&created)returnnil }); err != nil {return &resp, errdefs.ToGRPC(err) } ...}
s.local.Create 最终会调用 l.withStoreUpdate 函数去创建 Container:
services/containers/local.go
// line 211func(l *local)withStoreUpdate(ctx context.Context, fn func(ctx context.Context)error) error {return l.db.Update(l.withStore(ctx, fn))}
而 l.db.Update 最终还是调用匿名函数 fn 去创建容器,fn 匿名函数中需要注意函数 l.Store.Create:
metadata/containers.go
// line 117func(s *containerStore)Create(ctx context.Context, container containers.Container)(containers.Container, error) { ...// line 127if err := update(ctx, s.db, func(tx *bolt.Tx)error {// line 128 创建带版本和Namespace的根bucket bkt, err := createContainersBucket(tx, namespace) ...// line 133 根据容器ID分别创建容器各自的bucket cbkt, err := bkt.CreateBucket([]byte(container.ID)) ... container.CreatedAt = time.Now().UTC() container.UpdatedAt = container.CreatedAt// line 143 写入容器信息if err := writeContainer(cbkt, &container); err != nil {return fmt.Errorf("failed to write container %q: %w", container.ID, err) }returnnil }); err != nil {return containers.Container{}, err }return container, nil}
上述代码的逻辑很清晰,最外层使用事务回滚的方式保证多次操作的数据一致性,内部逻辑就是分层创建 bucket,最后将容器信息写入数据库,具体的写入逻辑如下:
metadata/containers.go
// line 367funcwriteContainer(bkt *bolt.Bucket, container *containers.Container)error {// line 368 记录容器的创建时间if err := boltutil.WriteTimestamps(bkt, container.CreatedAt, container.UpdatedAt); err != nil {return err }// line 372 记录容器的描述信息Specif err := boltutil.WriteAny(bkt, bucketKeySpec, container.Spec); err != nil {return err } ...// line 392 创建容器的runtime bucket,记录容器的runtime信息 rbkt, err := bkt.CreateBucket(bucketKeyRuntime) ...// line 397 记录容器的runtime名称信息if err := rbkt.Put(bucketKeyName, []byte(container.Runtime.Name)); err != nil {return err } ...}
writeContainer 函数中记录了 Containerd 需要的容器信息,上述代码只做了大概阐述,同志们感兴趣可以自行深入探索。
💡 至此我们可以看出,Containerd 中创建容器的过程实际就是将容器信息注册到 boltDB 的过程,这里的容器其实指的是静态的容器信息,并不是具体的容器进程。本文关于 Containerd 的容器创建流程至此就阐述完毕。
元数据
Containerd 中的容器元数据默认存储位置是/var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db。凡是经过 Containerd 创建的容器,其元数据都会存储在这个数据库中。bolt 的数据可以通过 boltbrowser 工具进行查看:
❝
👻 boltbrowser 可以通过 go 直接安装,Containerd 会给 meta.db 文件加锁,在查看之前需要做好备份
[root@master01 ~]# boltbrowser /var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db.copy

夜雨聆风