乐于分享
好东西不私藏

Docker 源码解析:(五)Docker Daemon 调用 Containerd 接口创建并启动容器

Docker 源码解析:(五)Docker Daemon 调用 Containerd 接口创建并启动容器

大家好,我是费益洲。本文将从 Docker Daemon 源码出发,详细介绍 Docker Daemon 调用 Containerd 接口创建并启动容器的全过程,本文是对前文Docker 源码解析:(三)Docker Daemon 处理请求与执行任务的方式中 Docker Daemon 创建容器的补充,也是后续 Containerd 系列文章创建并启动容器逻辑的基本前提。

前置条件

Docker Daemon 源码版本:v26.1.4

Docker Daemon 项目地址:https://github.com/moby/moby

Docker Client 执行命令:docker run --name nginx -d -p 80:80 -p 443:443 nginx:latest 

创建容器

💡 本节的内容指的是 Docker Daemon 调用 Containerd 创建容器,和前文中的 Docker Daemon 创建容器有本质区别:前文中的 Docker Daemon 创建容器,只是在 Docker 的存储中写入容器相关信息,供 Docker 记录查询使用。此处的调用 Containerd 接口创建容器,指的是在 Containerd 的存储中写入容器相关信息,供后续 Containerd 创建和启动 Task 使用。

Containerd 是可以单独使用 ctr 命令行工具创建和管理容器的,而 Containerd 自己本身也有自己的存储数据库,同 Docker 创建容器的本质逻辑一样,Containerd 创建容器的本质也是在自己的存储数据库中,写入 Containerd 需要的容器信息。这里需要注意一点,Docker 和 Containerd 都会存储容器相关的信息,也都使用了第三方库 etcd-io/bbolt 作为存储,但分别拥有自己独立的存储,而且存储的内容不一样。

由于 Docker 和 Containerd 分别拥有自己的存储,所以 Docker Daemon 在调用 Containerd 创建 容器 之前,会先去调用 Containerd 存储相关的接口,判断是否已经存在相同 ID 的容器,并对容器的相关信息做必要校验。

这部分源码的逻辑入口在 daemon/start.go:

// line 69func(daemon *Daemon)containerStart(ctx context.Context, daemonCfg *configStore, container *container.Container, checkpoint string, checkpointDir string, resetRestartManager bool)(retErr error) {    ...// line 169    ctr, err := libcontainerd.ReplaceContainer(ctx, daemon.containerd, container.ID, spec, shim, createOptions)    ...}

ReplaceContainer 函数的功能比清晰,即在调用 Containerd 创建容器时,判断当前容器 ID 是否已在 Containerd 中存在,具体实现如下:

libcontainerd/replace.go

// line 17funcReplaceContainer(ctx context.Context, client types.Client, id string, spec *specs.Spec, shim string, runtimeOptions interface{}, opts ...containerd.NewContainerOpts)(types.Container, error) {// line 18 新建容器,即在Containerd的存储中写入容器信息    newContainer := func()(types.Container, error) {return client.NewContainer(ctx, id, spec, shim, runtimeOptions, opts...) }    ctr, err := newContainer()if err == nil || !errdefs.IsConflict(err) {return ctr, err }    log := log.G(ctx).WithContext(ctx).WithField("container", id) log.Debug("A container already exists with the same ID. Attempting to clean up the old container.")// line 28 ~ line 59 删除已在Containerd的重复ID的容器信息// 加载容器,并判断容器是否已经存在,如果不存在,则直接新建容器    ctr, err = client.LoadContainer(ctx, id)if err != nil {if errdefs.IsNotFound(err) {return newContainer()  }returnnil, errors.Wrap(err, "could not load stale containerd container object") }// 加载Task,并判断Task是否已经存在,如果不存在,则直接删除容器信息    tsk, err := ctr.Task(ctx)if err != nil {if errdefs.IsNotFound(err) {goto deleteContainer  }returnnil, errors.Wrap(err, "could not load stale containerd task object")    }// 如果Task存在,则强制删除Taskif err := tsk.ForceDelete(ctx); err != nil {if !errdefs.IsNotFound(err) {returnnil, errors.Wrap(err, "could not delete stale containerd task object")  }    }// 删除容器信息代码块deleteContainer:if err := ctr.Delete(ctx); err != nil && !errdefs.IsNotFound(err) {returnnil, errors.Wrap(err, "could not delete stale containerd container object") }// 删除成功后,默认新建容器return newContainer()}

在上面 ReplaceContainer 函数的具体实现中,主体逻辑非常清晰,即如果不存在重复 ID 的老旧容器,则直接返回,如果存在,则删除 Containerd 中已经存在的老旧容器相关的信息,包括容器本身及 task,删除成功后,再调用 newContainer 函数块继续调用 Containerd 创建容器。接下来我们继续看 NewContainer 的具体实现:

libcontainerd/remote/client.go

// line 119func(c *client)NewContainer(ctx context.Context, id string, ociSpec *specs.Spec, shim string, runtimeOptions interface{}, opts ...containerd.NewContainerOpts)(libcontainerdtypes.Container, error) {    ...// line 130    ctr, err := c.client.NewContainer(ctx, id, opts...)    ...}

🔖 到此 Docker Daemon 源码部分结束,从这里开始 Docker Daemon 通过包引用的方式,直接调用 Containerd 的函数,具体的 NewContainer 函数实现如下:

vendor/github.com/containerd/containerd/client.go

// line 280func(c *Client)NewContainer(ctx context.Context, id string, opts ...NewContainerOpts)(Container, error) {    ...// line 298    r, err := c.ContainerService().Create(ctx, container)if err != nil {// 返回容器ID已经存在的异常if cerrdefs.IsAlreadyExists(err) {        returnnil, errors.WithStack(errdefs.Conflict(errors.New("id already in use")))      }returnnil, wrapError(err)    }    ...}

调用 Containerd 的 ContainerService 中的 Create 函数,这里的 Create 函数,是由 remoteContainers 具体实现的:

vendor/github.com/containerd/containerd/containerstore.go

// line 111func(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}

Create 则是通过 grpc 调用 Containerd 的容器创建接口创建容器:

vendor/github.com/containerd/containerd/api/services/containers/v1/containers_grpc.pb.go

// line 92func(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}

启动容器

启动容器是 Containerd 来具体实现的,而启动容器对 Containerd 来说就是一个 Task,Task 需要创建才能执行,所以在 Docker Daemon 的源码中,也是分两步来调用 Containerd 接口的,分别进行 Task 的创建和启动。

创建 Task

创建 Task 的逻辑源码入口依然在 daemon/start.go 中:

// line 69func(daemon *Daemon)containerStart(ctx context.Context, daemonCfg *configStore, container *container.Container, checkpoint string, checkpointDir string, resetRestartManager bool)(retErr error) {    ...// line 183 新建Task    tsk, err := ctr.NewTask(context.TODO(), // Passing ctx caused integration tests to be stuck in the cleanup phase  checkpointDir, container.StreamConfig.Stdin() != nil || container.Config.Tty,  container.InitializeStdio)    ...}

进入 NewTask,该函数由 libcontainerd.remote.container 具体实现:

libcontainerd/remote/client.go

// line 149func(c *container)NewTask(ctx context.Context, checkpointDir string, withStdin bool, attachStdio libcontainerdtypes.StdioCallback)(libcontainerdtypes.Task, error) {    ...// line 218    t, err = c.c8dCtr.NewTask(ctx,func(id string)(cio.IO, error) {   fifos := newFIFOSet(bundle, id, withStdin, spec.Process.Terminal)   rio, err = c.createIO(fifos, stdinCloseSync, attachStdio)return rio, err  },  taskOpts..., )    ...}

🔖 到此 Docker Daemon 源码部分结束,从这里开始 Docker Daemon 通过包引用的方式,直接调用 Containerd 的函数,具体的 c.c8dCtr.NewTask 函数实现如下:

vendor/github.com/containerd/containerd/container.go

// line 210func(c *container)NewTask(ctx context.Context, ioCreate cio.Creator, opts ...NewTaskOpts)(_ Task, err error) {    ...// line 301    response, err := c.client.TaskService().Create(ctx, request)    ...}

这里调用了 Containerd 的 TaskService 的 Create 函数创建 Task,具体调用如下所示:

vendor/github.com/containerd/containerd/api/services/tasks/v1/tasks_grpc.pb.go

// line 57func(c *tasksClient)Create(ctx context.Context, in *CreateTaskRequest, opts ...grpc.CallOption)(*CreateTaskResponse, error) { out := new(CreateTaskResponse) err := c.cc.Invoke(ctx, "/containerd.services.tasks.v1.Tasks/Create", in, out, opts...)if err != nil {returnnil, err }return out, nil}

启动 Task

启动 Task 的源码逻辑在之前的文章中有列举,此处只做简单描述,其逻辑源码入口也在 daemon/start.go 中:

// line 69func(daemon *Daemon)containerStart(ctx context.Context, daemonCfg *configStore, container *container.Container, checkpoint string, checkpointDir string, resetRestartManager bool)(retErr error) {    ...// line 202 启动Taskif err := tsk.Start(context.TODO()); err != nil { // passing ctx caused integration tests to be stuck in the cleanup phasereturn setExitCodeFromError(container.SetExitCode, err) }    ...}

调用 libcontainerd.remote 包中 task 实例的 Start 函数:

libcontainerd/remote/client.go

// line 242func(t *task)Start(ctx context.Context)error {return wrapError(t.Task.Start(ctx))}

🔖 到此 Docker Daemon 源码部分结束,从这里开始 Docker Daemon 通过包引用的方式,直接调用 Containerd 的函数,具体的 t.Task.Start 函数实现如下:

vendor/github.com/containerd/containerd/task.go

// line 215func(t *task)Start(ctx context.Context)error {    r, err := t.client.TaskService().Start(ctx, &tasks.StartRequest{  ContainerID: t.id, })    ...}

最终还是会通过 Containerd 的 grpc 接口发出启动 task 的请求:

vendor/github.com/containerd/containerd/api/services/tasks/v1/tasks_grpc.pb.go

// line 66func(c *tasksClient)Start(ctx context.Context, in *StartRequest, opts ...grpc.CallOption)(*StartResponse, error) { out := new(StartResponse) err := c.cc.Invoke(ctx, "/containerd.services.tasks.v1.Tasks/Start", in, out, opts...)if err != nil {returnnil, err }return out, nil}

总体来说,容器的创建分为两部分:Docker Daemon 创建容器和 Containerd 创建容器。这两部分的主要目的就是将 Docker Daemon 和 Containerd 用到的容器信息分别写入各自的存储中,方便后续的查询和管理,而数据一致性方面,则主要根据 Docker 的容器 ID 进行区分和校验。对于 Containerd 的容器创建和启动的具体逻辑实现,会在后续 Containerd 系列文章中进行说明。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Docker 源码解析:(五)Docker Daemon 调用 Containerd 接口创建并启动容器

评论 抢沙发

8 + 4 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮