tags: docker,container,源码分析

docker container create 流程 源码分析

1. docker container create 简介

创建容器,通过docker container create命令调用,该流程在运行容器时也会执行。

https://docs.docker.com/engine/reference/commandline/container_create/

2. 源码入口位置

由cli接收docker container create命令参数,发送至docker engine api

cli与engine api的代码入口分别位于:

cli https://github.com/docker/cli/blob/v20.10.13/cli/command/container/create.go#L43

engine https://github.com/moby/moby/blob/v20.10.13/api/server/router/container/container.go#L49

3. cli

docker使用 github.com/spf13/cobra 实现cli功能,NewCreateCommand函数中就定义了cobra.Command。该函数中用户输入的参数经cobra解析后,会被传给runCreate函数。

https://github.com/docker/cli/blob/v20.10.13/cli/command/container/create.go#L56

func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
    ...
    cmd := &cobra.Command{
        ...
        RunE: func(cmd *cobra.Command, args []string) error {
            ...
            return runCreate(dockerCli, cmd.Flags(), &opts, copts)
        },
    }
    ...
    return cmd
}

https://github.com/docker/cli/blob/v20.10.13/cli/command/container/create.go#L97

func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
    ...
    response, err := createContainer(context.Background(), dockerCli, containerConfig, options)
    ...
}

跟踪调用链,来到createContainer函数,如果配置了pull镜像选项,或是第一次创建容器返回镜像不存在,则会先拉取镜像,再调用dockerCli.Client().ContainerCreate方法创建容器。

https://github.com/docker/cli/blob/v20.10.13/cli/command/container/create.go#L192

func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, opts *createOptions) (*container.ContainerCreateCreatedBody, error) {
    ...
    if opts.pull == PullImageAlways {
        if err := pullAndTagImage(); err != nil {
            return nil, err
        }
    }

    response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, opts.name)
    if err != nil {
        // Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
        if apiclient.IsErrNotFound(err) && namedRef != nil && opts.pull == PullImageMissing {
            // we don't want to write to stdout anything apart from container.ID
            fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
            if err := pullAndTagImage(); err != nil {
                return nil, err
            }

            var retryErr error
            response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, opts.name)
    ...
}

接下来,在校验了一些版本信息后,调用api /containers/create,由docker engine具体实现容器创建。

https://github.com/docker/cli/blob/master/vendor/github.com/docker/docker/client/container_create.go#L54

func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *specs.Platform, containerName string) (container.ContainerCreateCreatedBody, error) {
	...
	query := url.Values{}
	if platform != nil {
		query.Set("platform", platforms.Format(*platform))
	}

	if containerName != "" {
		query.Set("name", containerName)
	}

	body := configWrapper{
		Config:           config,
		HostConfig:       hostConfig,
		NetworkingConfig: networkingConfig,
	}

	serverResp, err := cli.post(ctx, "/containers/create", query, body, nil)
	defer ensureReaderClosed(serverResp)
	if err != nil {
		return response, err
	}

	err = json.NewDecoder(serverResp.body).Decode(&response)
	return response, err
}

4. api

api /containers/create 对应 r.postContainersCreate方法。

https://github.com/moby/moby/blob/v20.10.13/api/server/router/container/container.go#L49

func (r *containerRouter) initRoutes() {
    r.routes = []router.Route{
        ...
        router.NewPostRoute("/containers/create", r.postContainersCreate),
        ...
    }
}

https://github.com/moby/moby/blob/v20.10.13/api/server/router/container/container_routes.go#L460

func (s *containerRouter) postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    ...
    ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{
        Name:             name,
        Config:           config,
        HostConfig:       hostConfig,
        NetworkingConfig: networkingConfig,
        AdjustCPUShares:  adjustCPUShares,
        Platform:         platform,
    })
    ...
}

s.backend 初始化为 Daemon 对象,所以跟进Daemon.ContainerCreate

https://github.com/moby/moby/blob/v20.10.13/daemon/create.go#L42

func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (containertypes.ContainerCreateCreatedBody, error) {
    return daemon.containerCreate(createOpts{
        params:                  params,
        managed:                 false,
        ignoreImagesArgsEscaped: false})
}

func (daemon *Daemon) containerCreate(opts createOpts) (containertypes.ContainerCreateCreatedBody, error) {
    ...
    ctr, err := daemon.create(opts)
    ...
}

跳过一些配置封装和校验,来到daemon.create方法。该方法创建了容器,并为容器准备好了文件系统、创建了卷、把容器中已存在的文件复制到卷中等,这些操作将在下文中逐一分析。


创建容器,如果遇到错误在函数结束前清理容器;这里创建的容器实际只是创建了一个github.com/docker/docker/container.Container类型的实例。

https://github.com/moby/moby/blob/v20.10.13/daemon/create.go#L174-L183

func (daemon *Daemon) create(opts createOpts) (retC *container.Container, retErr error) {
    ...
    if ctr, err = daemon.newContainer(opts.params.Name, os, opts.params.Config, opts.params.HostConfig, imgID, opts.managed); err != nil {
        return nil, err
    }
    defer func() {
        if retErr != nil {
            if err := daemon.cleanupContainer(ctr, true, true); err != nil {
                logrus.Errorf("failed to cleanup container on create error: %v", err)
            }
        }
    }()
    ...
}

https://github.com/moby/moby/blob/v20.10.13/daemon/container.go#L128

func (daemon *Daemon) newContainer(name string, operatingSystem string, config *containertypes.Config, hostConfig *containertypes.HostConfig, imgID image.ID, managed bool) (*container.Container, error) {
    ...
    base := daemon.newBaseContainer(id)
    base.Created = time.Now().UTC()
    base.Managed = managed
    base.Path = entrypoint
    ...
    return base, err
}

创建容器文件系统,以容器镜像rootfs的只读层作为上层文件系统,创建一个读写层。

https://github.com/moby/moby/blob/v20.10.13/daemon/create.go#L209-L221

func (daemon *Daemon) create(opts createOpts) (retC *container.Container, retErr error) {
    ...
    rwLayer, err := daemon.imageService.CreateLayer(ctr, setupInitLayer(daemon.idMapping))
    if err != nil {
        return nil, errdefs.System(err)
    }
    ctr.RWLayer = rwLayer

    current := idtools.CurrentIdentity()
    if err := idtools.MkdirAndChown(ctr.Root, 0710, idtools.Identity{UID: current.UID, GID: daemon.IdentityMapping().RootPair().GID}); err != nil {
        return nil, err
    }
    if err := idtools.MkdirAndChown(ctr.CheckpointDir(), 0700, current); err != nil {
        return nil, err
    }
    ...
}

https://github.com/moby/moby/blob/v20.10.13/daemon/images/service.go#L128

func (i *ImageService) CreateLayer(container *container.Container, initFunc layer.MountInit) (layer.RWLayer, error) {
    var layerID layer.ChainID
    if container.ImageID != "" {
        img, err := i.imageStore.Get(container.ImageID)
        if err != nil {
            return nil, err
        }
        layerID = img.RootFS.ChainID()
    }
    ...
    return i.layerStores[container.OS].CreateRWLayer(container.ID, layerID, rwLayerOpts)
}

https://github.com/moby/moby/blob/v20.10.13/daemon/daemon_unix.go#L1069

func setupInitLayer(idMapping *idtools.IdentityMapping) func(containerfs.ContainerFS) error {
    return func(initPath containerfs.ContainerFS) error {
        return initlayer.Setup(initPath, idMapping.RootPair())
    }
}

其中,在创建读写层前,先对容器的rootfs做了一些设置:清除并重建了一些重要路径。

https://github.com/moby/moby/blob/v20.10.13/daemon/initlayer/setup_unix.go#L20

func Setup(initLayerFs containerfs.ContainerFS, rootIdentity idtools.Identity) error {
    // Since all paths are local to the container, we can just extract initLayerFs.Path()
    initLayer := initLayerFs.Path()

    for pth, typ := range map[string]string{
        "/dev/pts":         "dir",
        "/dev/shm":         "dir",
        "/proc":            "dir",
        "/sys":             "dir",
        "/.dockerenv":      "file",
        "/etc/resolv.conf": "file",
        "/etc/hosts":       "file",
        "/etc/hostname":    "file",
        "/dev/console":     "file",
        "/etc/mtab":        "/proc/mounts",
    } {
        parts := strings.Split(pth, "/")
        prev := "/"
        for _, p := range parts[1:] {
            prev = filepath.Join(prev, p)
            unix.Unlink(filepath.Join(initLayer, prev))
        }

        if _, err := os.Stat(filepath.Join(initLayer, pth)); err != nil {
            if os.IsNotExist(err) {
                if err := idtools.MkdirAllAndChownNew(filepath.Join(initLayer, filepath.Dir(pth)), 0755, rootIdentity); err != nil {
                    return err
                }
                switch typ {
                case "dir":
                    if err := idtools.MkdirAllAndChownNew(filepath.Join(initLayer, pth), 0755, rootIdentity); err != nil {
                        return err
                    }
                case "file":
                    f, err := os.OpenFile(filepath.Join(initLayer, pth), os.O_CREATE, 0755)
                    if err != nil {
                        return err
                    }
                    f.Chown(rootIdentity.UID, rootIdentity.GID)
                    f.Close()
                default:
                    if err := os.Symlink(typ, filepath.Join(initLayer, pth)); err != nil {
                        return err
                    }
                }
            } else {
                return err
            }
        }
    }

    // Layer is ready to use, if it wasn't before.
    return nil
}

setHostConfig, createContainerOSSpecificSettings都主要是创建卷。

https://github.com/moby/moby/blob/v20.10.13/daemon/create.go#L223-L229

func (daemon *Daemon) create(opts createOpts) (retC *container.Container, retErr error) {
    ...
    if err := daemon.setHostConfig(ctr, opts.params.HostConfig); err != nil {
        return nil, err
    }

    if err := daemon.createContainerOSSpecificSettings(ctr, opts.params.Config, opts.params.HostConfig); err != nil {
        return nil, err
    }
    ...
}

https://github.com/moby/moby/blob/v20.10.13/daemon/create_unix.go#L22

func (daemon *Daemon) createContainerOSSpecificSettings(container *container.Container, config *containertypes.Config, hostConfig *containertypes.HostConfig) error {
    if err := daemon.Mount(container); err != nil {
        return err
    }
    defer daemon.Unmount(container)
    ...
    for spec := range config.Volumes {
        name := stringid.GenerateRandomID()
        destination := filepath.Clean(spec)
        ...
        v, err := daemon.volumes.Create(context.TODO(), name, hostConfig.VolumeDriver, volumeopts.WithCreateReference(container.ID))
        ...
        container.AddMountPointWithVolume(destination, &volumeWrapper{v: v, s: daemon.volumes}, true)
    }
    return daemon.populateVolumes(container)
}

创建完卷后,对于每个挂载点,如果rootfs中挂载点已存在,则将从该路径复制文件到卷中。

https://github.com/moby/moby/blob/v20.10.13/daemon/create_unix.go#L80

func (daemon *Daemon) populateVolumes(c *container.Container) error {
    for _, mnt := range c.MountPoints {
        ...
        if err := c.CopyImagePathContent(mnt.Volume, mnt.Destination); err != nil {
            return err
        }
    }
    return nil
}

https://github.com/moby/moby/blob/master/container/container_unix.go#L129

func (container *Container) CopyImagePathContent(v volume.Volume, destination string) error {
    rootfs, err := container.GetResourcePath(destination)
    if err != nil {
        return err
    }
    ...
    id := stringid.GenerateRandomID()
    path, err := v.Mount(id)
    if err != nil {
        return err
    }

    defer func() {
        if err := v.Unmount(id); err != nil {
            logrus.Warnf("error while unmounting volume %s: %v", v.Name(), err)
        }
    }()
    ...
    return copyExistingContents(rootfs, path)
}

至此,创建容器的流程基本结束。