[docker-ce source code analysis series] A brief analysis of creating (create) containers

1 Overview:

1.1 Environment

The version information is as follows:
a. Operating system: centos 7.6, amd64
b, server docker version: v18.09.2
c, docker storage driver: overlay2

2 Brief analysis of source code:

The user docker run, the client will initiate three remote calls to the docker daemon at most, which are to create (pull) an image, create a container, and start a container. This article analyzes the process of creating a container on the server side.

2.1 Server-side registration route initRoutes()

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

2.1 postContainersCreate(...) method

func (s *containerRouter) postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {

	/*
		检查请求对象的数据,对输入数据进行校验是非常有必要的
	*/

	// 获取容器名
	name := r.Form.Get("name")

	// 从http body中解析出几个对象
	// networkingConfig这个map一般为空map,因为一般不在命令行中设置容器ip等网络信息。
	// config是容器的配置,包括镜像、容器名称、环境变量、启动命令entrypoint、是否挂载终端、容器端口映射等。
	// hostConfig是主机相关的配置,包括挂载目录映射关系、网络模式、重启策略、cgroup设置,是否privileged、DNS设置、日志配置等。
	config, hostConfig, networkingConfig, err := s.decoder.DecodeConfig(r.Body)
	if err != nil {
		return err
	}
	
	// 获取api版本
	version := httputils.VersionFromContext(ctx)
	adjustCPUShares := versions.LessThan(version, "1.19")

	// When using API 1.24 and under, the client is responsible for removing the container
	if hostConfig != nil && versions.LessThan(version, "1.25") {
		hostConfig.AutoRemove = false
	}

	// 创建容器
	ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{
		Name:             name,
		Config:           config,
		HostConfig:       hostConfig,
		NetworkingConfig: networkingConfig,
		AdjustCPUShares:  adjustCPUShares,
	})
	
	if err != nil {
		return err
	}
	
	// 创建容器成功,给客户端返回响应
	return httputils.WriteJSON(w, http.StatusCreated, ccr)
}

The implementation of s.backend is as follows:

// daemon/create.go文件
// 创建一个普通容器
func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (containertypes.ContainerCreateCreatedBody, error) {
	return daemon.containerCreate(params, false)
}

2.2 Container config object

config is the configuration of the container, including image, container name, environment variables, startup command entrypoint, whether to mount the terminal, container port mapping, etc.
Insert picture description here


2.3 Container hostConfig object

hostConfig is the host-level configuration, including mount directory mapping, network mode, restart strategy, cgroup, whether it is privileged, DNS settings, log configuration, OOM score adjustments, etc.
Insert picture description here


2.4 containerCreate(...) method

Check the input parameters, and finally call daemon.create(...) to create the container.

// daemon/create.go文件
func (daemon *Daemon) containerCreate(params types.ContainerCreateConfig, managed bool) (containertypes.ContainerCreateCreatedBody, error) {
	start := time.Now()

	// 校验1,客户端的参数是不合法的
	if params.Config == nil {
		return containertypes.ContainerCreateCreatedBody{}, errdefs.InvalidParameter(errors.New("Config cannot be empty in order to create a container"))
	}

	os := runtime.GOOS
	if params.Config.Image != "" {
		// 获取镜像
		img, err := daemon.imageService.GetImage(params.Config.Image)
		if err == nil {
			os = img.OS
		}
	} else {
		// This mean scratch. On Windows, we can safely assume that this is a linux
		// container. On other platforms, it's the host OS (which it already is)
		if runtime.GOOS == "windows" && system.LCOWSupported() {
			os = "linux"
		}
	}

	// 校验2,客户端的参数是不合法的
	warnings, err := daemon.verifyContainerSettings(os, params.HostConfig, params.Config, false)
	if err != nil {
		return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, errdefs.InvalidParameter(err)
	}
	
	// 校验3,客户端的参数是不合法的
	err = verifyNetworkingConfig(params.NetworkingConfig)
	if err != nil {
		return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, errdefs.InvalidParameter(err)
	}

	// 如果需要,稍微调整一下params对象的内容
	if params.HostConfig == nil {
		params.HostConfig = &containertypes.HostConfig{}
	}
	err = daemon.adaptContainerSettings(params.HostConfig, params.AdjustCPUShares)
	if err != nil {
		return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, errdefs.InvalidParameter(err)
	}

	// 调用核心方法创建容器对象
	// 在内存中创建了container对象,并在宿主机上创建一些目录和文件。
	container, err := daemon.create(params, managed)
	if err != nil {
		return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, err
	}
	
	// prometheus指标
	containerActions.WithValues("create").UpdateSince(start)

	// 创建成功,将容器ID返回
	return containertypes.ContainerCreateCreatedBody{ID: container.ID, Warnings: warnings}, nil
}

2.5 daemon.create(…) method

The way to actually create a container. The business logic is mainly to create container objects in memory, create indexes, and create some directories and files on the host. These directories and files include:
1) Subdirectories (diff, work) and files (link and lower) under the /var/lib/docker/overlay2/{ID}/ directory.
2) /var/lib/docker/image/overlay2/layerdb/mounts/<container ID>/{init-id,mount-id,parent} file
3) /var/lib/docker/containers/<container ID> directory Create text files under: config.v2.json and hostconfig.json

// daemon/create.go文件
func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (retC *container.Container, retErr error) {
	var (
		container *container.Container
		img       *image.Image
		imgID     image.ID
		err       error
	)

	os := runtime.GOOS
	if params.Config.Image != "" {
		img, err = daemon.imageService.GetImage(params.Config.Image)
		if err != nil {
			return nil, err
		}
		if img.OS != "" {
			os = img.OS
		} else {
			// default to the host OS except on Windows with LCOW
			if runtime.GOOS == "windows" && system.LCOWSupported() {
				os = "linux"
			}
		}
		imgID = img.ID()

		if runtime.GOOS == "windows" && img.OS == "linux" && !system.LCOWSupported() {
			return nil, errors.New("operating system on which parent image was created is not Windows")
		}
	} else {
		if runtime.GOOS == "windows" {
			os = "linux" // 'scratch' case.
		}
	}

	// 合并指的是:params.Config有些内容为空,则用img中的值来进行赋值
	// 校验指的是:检查cmd和entrypoint是否都为空,如果都是空则返回错误
	if err := daemon.mergeAndVerifyConfig(params.Config, img); err != nil {
		return nil, errdefs.InvalidParameter(err)
	}

	// 合并指的是:container级别的日志配置项为空,则用daemon的日志配置项来进行赋值
	// 校验指的是:检查日志驱动名称、日志模式,max-buffer-size等等与日志相关的配置项
	if err := daemon.mergeAndVerifyLogConfig(&params.HostConfig.LogConfig); err != nil {
		return nil, errdefs.InvalidParameter(err)
	}

	// 创建一个container结构体,此时它的属性RWLayer还是为空(那应该在后续操作中会进行赋值,确实如此)。
	if container, err = daemon.newContainer(params.Name, os, params.Config, params.HostConfig, imgID, managed); err != nil {
		return nil, err
	}
	defer func() {
		if retErr != nil {
			if err := daemon.cleanupContainer(container, true, true); err != nil {
				logrus.Errorf("failed to cleanup container on create error: %v", err)
			}
		}
	}()

	if err := daemon.setSecurityOptions(container, params.HostConfig); err != nil {
		return nil, err
	}

	container.HostConfig.StorageOpt = params.HostConfig.StorageOpt
	
	/*
		如果是windows操作系统,进行一些操作:
		if runtime.GOOS == "windows" {
			修改container.HostConfig.StorageOpt
		}
	
	/*
	  daemon.imageService.CreateLayer(...)主要做的事情:
	  1)创建/var/lib/docker/overlay2/{ID-init}/目录下的子目录(diff、work)和文件(link和lower)
	  2)创建/var/lib/docker/overlay2/{ID}/目录下的子目录(diff、work)和文件(link和lower)
	  3)创建 /var/lib/docker/image/overlay2/layerdb/mounts/<容器ID>/{init-id,mount-id,parent}文件	  
	*/
	rwLayer, err := daemon.imageService.CreateLayer(container, setupInitLayer(daemon.idMapping))
	if err != nil {
		return nil, errdefs.System(err)
	}
	// container对象的属性RWLayer进行赋值
	container.RWLayer = rwLayer

	// rootIDs是一个结构体,里面包括了UID、GID和SID,一般这三者都是0。
	rootIDs := daemon.idMapping.RootPair()

	// 创建/var/lib/docker/containers/{容器ID}目录,并设置相应的用户ID、组ID、权限等属性
	if err := idtools.MkdirAndChown(container.Root, 0700, rootIDs); err != nil {
		return nil, err
	}
	
	// 创建/var/lib/docker/containers/{容器ID}/checkpoints目录,并设置相应的用户ID、组ID、权限等属性
	if err := idtools.MkdirAndChown(container.CheckpointDir(), 0700, rootIDs); err != nil {
		return nil, err
	}

	// daemon.setHostConfig(...)做的两件事:
	// 1)设置入参container对象的属性MountPoints和属性HostConfig
	// 2)在/var/lib/docker/containers/<容器ID>目录下创建文本文件:config.v2.json和hostconfig.json
	if err := daemon.setHostConfig(container, params.HostConfig); err != nil {
		return nil, err
	}

	// 挂载又卸载容器的merged目录,以及如果使用docker volume,则发生数据复制。
	if err := daemon.createContainerOSSpecificSettings(container, params.Config, params.HostConfig); err != nil {
		return nil, err
	}

	var endpointsConfigs map[string]*networktypes.EndpointSettings
	if params.NetworkingConfig != nil {
		// params.NetworkingConfig.EndpointsConfig往往是一个空map
		endpointsConfigs = params.NetworkingConfig.EndpointsConfig
	}
	
	runconfig.SetDefaultNetModeIfBlank(container.HostConfig)
	// 在非用户自定义网络模式下,做的事情很简单:为container对象的属性NetworkSettings的属性Networks添加一个key,key就是"bridge"
	daemon.updateContainerNetworkSettings(container, endpointsConfigs)

	// container对象注册到内存中,并使用前缀树来索引
	// 将此时的container对象持久化至磁盘:/var/lib/docker/containers/{容器ID}/config.v2.json
	if err := daemon.Register(container); err != nil {
		return nil, err
	}
	
	stateCtr.set(container.ID, "stopped")
	daemon.LogContainerEvent(container, "create")
	return container, nil
}


2.6 createContainerOSSpecificSettings方法

Mount and unmount the merged directory, and if docker volume is used, data replication occurs.

// daemon/create.go文件
func (daemon *Daemon) createContainerOSSpecificSettings(container *container.Container, config *containertypes.Config, hostConfig *containertypes.HostConfig) error {

	// merged目录是在此处创建,并进行绑定挂载
	/*
			mountTarget := merged目录
			mountdata的内容类似: index=off,lowerdir=/var/lib/docker/overlay2/l/3EWF6KYE4B65XPHTQIH5PJAVEQ:/var/lib/docker/overlay2/l/3SIMO6NI4MYP7HAZCQHFISBBV3,upperdir=/var/lib/docker/overlay2/9b30aee99a63b6f5b06a13fdbe78970ac034f5a6e292fdc62620d669cd9715dd/diff,workdir=/var/lib/docker/overlay2/9b30aee99a63b6f5b06a13fdbe78970ac034f5a6e292fdc62620d669cd9715dd/work
			mount("overlay", mountTarget, "overlay", 0, mountData);
	*/
	if err := daemon.Mount(container); err != nil {
		return err
	}
	// 在函数返回时卸载merged目录
	defer daemon.Unmount(container)
	
	rootIDs := daemon.idMapping.RootPair()
	if err := container.SetupWorkingDirectory(rootIDs); err != nil {
		return err
	}
	
	// 有默认的一些路径是masked和只读
	// 例如/proc/acpi是masked path,/proc/bus是readonly path。
	if hostConfig.MaskedPaths == nil && !hostConfig.Privileged {
		hostConfig.MaskedPaths = oci.DefaultSpec().Linux.MaskedPaths // Set it to the default if nil
		container.HostConfig.MaskedPaths = hostConfig.MaskedPaths
	}
	if hostConfig.ReadonlyPaths == nil && !hostConfig.Privileged {
		hostConfig.ReadonlyPaths = oci.DefaultSpec().Linux.ReadonlyPaths // Set it to the default if nil
		container.HostConfig.ReadonlyPaths = hostConfig.ReadonlyPaths
	}

	// 有机会发生数据复制
	// docker run -v同时指定宿主机目录和容器目录时,不会发生数据复制,这种mountPoint对象的voloume字段是nil,因此被跳过
	// docker run -v 不指定宿主目录时,就使用docker volume(即/var/lib/docker/volumes目录下的子目录),此时如果容器中的挂载点已有数据,则把容器中挂载点中的数据复制到docker volume中。
	return daemon.populateVolumes(container)
}

2.7 Masked path and readonly path in the container

Insert picture description here

Insert picture description here


2.8 setHostConfig(…) method

1) Set the attribute MountPoints and the attribute HostConfig of the container object.
2) Create text files in the /var/lib/docker/containers/<container ID> directory: config.v2.json and hostconfig.json

// daemon/create.go文件
// 设置入参container对象的属性MountPoints和属性HostConfig
func (daemon *Daemon) setHostConfig(container *container.Container, hostConfig *containertypes.HostConfig) error {
	
	/*
		registerMountPoints()本质是设置入参container的属性MountPoints。
		挂载点包括镜像Dockerfile中指定的挂载点、用户命令行指定的来自其他容器的volume和命令中指定的绑定挂载。
	*/
	if err := daemon.registerMountPoints(container, hostConfig); err != nil {
		return err
	}

	container.Lock()
	defer container.Unlock()

	// 1)hostConfig使用了link机制的话,则进行相应的操作。
	// 2)将hostConfig对象写到磁盘:/var/lib/docker/containers/{容器ID}/hostconfig.json
	if err := daemon.registerLinks(container, hostConfig); err != nil {
		return err
	}

	// 如果入参hostConfig的NetworkMode为"",则设置为"default"
	runconfig.SetDefaultNetModeIfBlank(hostConfig)
	
	// container对象的属性HostConfig的内容是一堆空值,因此将它直接设置为入参hostConfig
	container.HostConfig = hostConfig
	
	// 把container对象持久化到磁盘中:/var/lib/docker/containers/{容器ID}/config.v2.json
	return container.CheckpointTo(daemon.containersReplica)
}

3 Summary:

The process of creating a container is relatively simple. The essence is to create container objects in memory and create directories and files on the host, as well as some mount operations and possible data replication (docker volume).

Guess you like

Origin blog.csdn.net/nangonghen/article/details/115147049