[docker-ce source code analysis series] Uma breve análise da criação (criação) de contêineres

1. Visão Geral:

1.1 Meio Ambiente

As informações da versão são as seguintes:
a. Sistema operacional: centos 7.6, amd64
b, versão docker do servidor: v18.09.2
c, driver de armazenamento docker: overlay2

2 Breve análise do código-fonte:

Quando o docker do usuário é executado, o cliente inicia três chamadas remotas para o daemon do docker, que são para criar (puxar) uma imagem, criar um contêiner e iniciar um contêiner. Este artigo analisa o processo de criação de um contêiner no lado do servidor.

2.1 Rota de registro do lado do servidor initRoutes ()

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

2.1 Método postContainersCreate (...)

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)
}

A implementação de s.backend é a seguinte:

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

2.2 Objeto de configuração do contêiner

config é a configuração do contêiner, incluindo imagem, nome do contêiner, variáveis ​​de ambiente, ponto de entrada do comando de inicialização, se deve montar o terminal, mapeamento da porta do contêiner, etc.
Insira a descrição da imagem aqui


2.3 Objeto hostConfig do contêiner

hostConfig é a configuração em nível de host, incluindo mapeamento de diretório de montagem, modo de rede, estratégia de reinicialização, cgroup, se é privilegiado, configurações de DNS, configuração de log, ajustes de pontuação OOM, etc.
Insira a descrição da imagem aqui


2.4 Método containerCreate (...)

Verifique os parâmetros de entrada e finalmente chame daemon.create (...) para criar o contêiner.

// 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
}

Método 2.5 daemon.create (...)

A maneira de realmente criar um contêiner. A lógica de negócios é principalmente criar objetos de contêiner na memória, criar índices e criar alguns diretórios e arquivos no host. Esses diretórios e arquivos incluem:
1) Subdiretórios (diff, work) e arquivos (link e inferior) no diretório / var / lib / docker / overlay2 / {ID} /.
2) / var / lib / docker / image / overlay2 / layerdb / mounts / <ID do contêiner> / arquivo {init-id, mount-id, pai}
3) diretório / var / lib / docker / containers / <container ID> Crie arquivos de texto em: config.v2.json e 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 方法

Monte e desmonte o diretório mesclado e, se o volume do docker for usado, ocorrerá a replicação de dados.

// 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 Caminho mascarado e caminho somente leitura no contêiner

Insira a descrição da imagem aqui

Insira a descrição da imagem aqui


2.8 Método setHostConfig (…)

1) Defina o atributo MountPoints e o atributo HostConfig do objeto container.
2) Crie arquivos de texto no diretório / var / lib / docker / containers / <container ID>: config.v2.json e 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 Resumo:

O processo de criação de um container é relativamente simples.A essência é criar objetos container na memória e criar diretórios e arquivos no host, bem como algumas operações de montagem e possível replicação de dados (volume docker).

Acho que você gosta

Origin blog.csdn.net/nangonghen/article/details/115147049
Recomendado
Clasificación