版本信息如下:
a、操作系统: centos 7.6,amd64
b、服务器docker版本:v18.09.2
c、docker的存储驱动: overlay2
用户docker run,client至多会向docker daemon发起三次远程调用,分别是创建(拉取)镜像、创建容器、启动容器。本篇文章分析服务端创建容器的过程。
func (r *containerRouter) initRoutes() {r.routes = []router.Route{/*其他接口*/router.NewPostRoute("/containers/create", r.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 containerif 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)
}
s.backend的实现如下:
// daemon/create.go文件
// 创建一个普通容器
func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (containertypes.ContainerCreateCreatedBody, error) {return daemon.containerCreate(params, false)
}
config是容器的配置,包括镜像、容器名称、环境变量、启动命令entrypoint、是否挂载终端、容器端口映射等。
hostConfig是主机层次的配置,包括挂载目录映射关系、网络模式、重启策略、cgroup,是否privileged、DNS设置、日志配置、OOM分数的调整数等。
校验入参,最终调用daemon.create(…)创建容器。
// 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.GOOSif 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
}
真正创建容器的方法。业务逻辑主要是在内存中创建container对象,创建索引,以及在宿主机上创建一些目录和文件。这些目录和文件包括:
1)/var/lib/docker/overlay2/{ID}/目录下的子目录(diff、work)和文件(link和lower)。
2&#xff09;/var/lib/docker/image/overlay2/layerdb/mounts/<容器ID>/{init-id,mount-id,parent}文件
3&#xff09;/var/lib/docker/containers/<容器ID>目录下创建文本文件&#xff1a;config.v2.json和hostconfig.json
// daemon/create.go文件
func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (retC *container.Container, retErr error) {var (container *container.Containerimg *image.ImageimgID image.IDerr error)os :&#61; runtime.GOOSif params.Config.Image !&#61; "" {img, err &#61; daemon.imageService.GetImage(params.Config.Image)if err !&#61; nil {return nil, err}if img.OS !&#61; "" {os &#61; img.OS} else {// default to the host OS except on Windows with LCOWif runtime.GOOS &#61;&#61; "windows" && system.LCOWSupported() {os &#61; "linux"}}imgID &#61; img.ID()if runtime.GOOS &#61;&#61; "windows" && img.OS &#61;&#61; "linux" && !system.LCOWSupported() {return nil, errors.New("operating system on which parent image was created is not Windows")}} else {if runtime.GOOS &#61;&#61; "windows" {os &#61; "linux" // &#39;scratch&#39; case.}}// 合并指的是&#xff1a;params.Config有些内容为空&#xff0c;则用img中的值来进行赋值// 校验指的是&#xff1a;检查cmd和entrypoint是否都为空&#xff0c;如果都是空则返回错误if err :&#61; daemon.mergeAndVerifyConfig(params.Config, img); err !&#61; nil {return nil, errdefs.InvalidParameter(err)}// 合并指的是&#xff1a;container级别的日志配置项为空&#xff0c;则用daemon的日志配置项来进行赋值// 校验指的是&#xff1a;检查日志驱动名称、日志模式&#xff0c;max-buffer-size等等与日志相关的配置项if err :&#61; daemon.mergeAndVerifyLogConfig(¶ms.HostConfig.LogConfig); err !&#61; nil {return nil, errdefs.InvalidParameter(err)}// 创建一个container结构体&#xff0c;此时它的属性RWLayer还是为空&#xff08;那应该在后续操作中会进行赋值&#xff0c;确实如此&#xff09;。if container, err &#61; daemon.newContainer(params.Name, os, params.Config, params.HostConfig, imgID, managed); err !&#61; nil {return nil, err}defer func() {if retErr !&#61; nil {if err :&#61; daemon.cleanupContainer(container, true, true); err !&#61; nil {logrus.Errorf("failed to cleanup container on create error: %v", err)}}}()if err :&#61; daemon.setSecurityOptions(container, params.HostConfig); err !&#61; nil {return nil, err}container.HostConfig.StorageOpt &#61; params.HostConfig.StorageOpt/*如果是windows操作系统&#xff0c;进行一些操作&#xff1a;if runtime.GOOS &#61;&#61; "windows" {修改container.HostConfig.StorageOpt}/*daemon.imageService.CreateLayer(...)主要做的事情&#xff1a;1&#xff09;创建/var/lib/docker/overlay2/{ID-init}/目录下的子目录&#xff08;diff、work&#xff09;和文件&#xff08;link和lower&#xff09;2&#xff09;创建/var/lib/docker/overlay2/{ID}/目录下的子目录&#xff08;diff、work&#xff09;和文件&#xff08;link和lower&#xff09;3&#xff09;创建 /var/lib/docker/image/overlay2/layerdb/mounts/<容器ID>/{init-id,mount-id,parent}文件 */rwLayer, err :&#61; daemon.imageService.CreateLayer(container, setupInitLayer(daemon.idMapping))if err !&#61; nil {return nil, errdefs.System(err)}// container对象的属性RWLayer进行赋值container.RWLayer &#61; rwLayer// rootIDs是一个结构体&#xff0c;里面包括了UID、GID和SID&#xff0c;一般这三者都是0。rootIDs :&#61; daemon.idMapping.RootPair()// 创建/var/lib/docker/containers/{容器ID}目录&#xff0c;并设置相应的用户ID、组ID、权限等属性if err :&#61; idtools.MkdirAndChown(container.Root, 0700, rootIDs); err !&#61; nil {return nil, err}// 创建/var/lib/docker/containers/{容器ID}/checkpoints目录&#xff0c;并设置相应的用户ID、组ID、权限等属性if err :&#61; idtools.MkdirAndChown(container.CheckpointDir(), 0700, rootIDs); err !&#61; nil {return nil, err}// daemon.setHostConfig(...)做的两件事&#xff1a;// 1&#xff09;设置入参container对象的属性MountPoints和属性HostConfig// 2&#xff09;在/var/lib/docker/containers/<容器ID>目录下创建文本文件&#xff1a;config.v2.json和hostconfig.jsonif err :&#61; daemon.setHostConfig(container, params.HostConfig); err !&#61; nil {return nil, err}// 挂载又卸载容器的merged目录&#xff0c;以及如果使用docker volume&#xff0c;则发生数据复制。if err :&#61; daemon.createContainerOSSpecificSettings(container, params.Config, params.HostConfig); err !&#61; nil {return nil, err}var endpointsConfigs map[string]*networktypes.EndpointSettingsif params.NetworkingConfig !&#61; nil {// params.NetworkingConfig.EndpointsConfig往往是一个空mapendpointsConfigs &#61; params.NetworkingConfig.EndpointsConfig}runconfig.SetDefaultNetModeIfBlank(container.HostConfig)// 在非用户自定义网络模式下&#xff0c;做的事情很简单&#xff1a;为container对象的属性NetworkSettings的属性Networks添加一个key&#xff0c;key就是"bridge"daemon.updateContainerNetworkSettings(container, endpointsConfigs)// container对象注册到内存中&#xff0c;并使用前缀树来索引// 将此时的container对象持久化至磁盘&#xff1a;/var/lib/docker/containers/{容器ID}/config.v2.jsonif err :&#61; daemon.Register(container); err !&#61; nil {return nil, err}stateCtr.set(container.ID, "stopped")daemon.LogContainerEvent(container, "create")return container, nil
}
挂载又卸载merged目录&#xff0c;以及如果使用docker volume&#xff0c;则发生数据复制。
// daemon/create.go文件
func (daemon *Daemon) createContainerOSSpecificSettings(container *container.Container, config *containertypes.Config, hostConfig *containertypes.HostConfig) error {// merged目录是在此处创建&#xff0c;并进行绑定挂载/*mountTarget :&#61; merged目录mountdata的内容类似: index&#61;off,lowerdir&#61;/var/lib/docker/overlay2/l/3EWF6KYE4B65XPHTQIH5PJAVEQ:/var/lib/docker/overlay2/l/3SIMO6NI4MYP7HAZCQHFISBBV3,upperdir&#61;/var/lib/docker/overlay2/9b30aee99a63b6f5b06a13fdbe78970ac034f5a6e292fdc62620d669cd9715dd/diff,workdir&#61;/var/lib/docker/overlay2/9b30aee99a63b6f5b06a13fdbe78970ac034f5a6e292fdc62620d669cd9715dd/workmount("overlay", mountTarget, "overlay", 0, mountData);*/if err :&#61; daemon.Mount(container); err !&#61; nil {return err}// 在函数返回时卸载merged目录defer daemon.Unmount(container)rootIDs :&#61; daemon.idMapping.RootPair()if err :&#61; container.SetupWorkingDirectory(rootIDs); err !&#61; nil {return err}// 有默认的一些路径是masked和只读// 例如/proc/acpi是masked path&#xff0c;/proc/bus是readonly path。if hostConfig.MaskedPaths &#61;&#61; nil && !hostConfig.Privileged {hostConfig.MaskedPaths &#61; oci.DefaultSpec().Linux.MaskedPaths // Set it to the default if nilcontainer.HostConfig.MaskedPaths &#61; hostConfig.MaskedPaths}if hostConfig.ReadonlyPaths &#61;&#61; nil && !hostConfig.Privileged {hostConfig.ReadonlyPaths &#61; oci.DefaultSpec().Linux.ReadonlyPaths // Set it to the default if nilcontainer.HostConfig.ReadonlyPaths &#61; hostConfig.ReadonlyPaths}// 有机会发生数据复制// docker run -v同时指定宿主机目录和容器目录时&#xff0c;不会发生数据复制&#xff0c;这种mountPoint对象的voloume字段是nil&#xff0c;因此被跳过// docker run -v 不指定宿主目录时&#xff0c;就使用docker volume&#xff08;即/var/lib/docker/volumes目录下的子目录&#xff09;&#xff0c;此时如果容器中的挂载点已有数据&#xff0c;则把容器中挂载点中的数据复制到docker volume中。return daemon.populateVolumes(container)
}
1&#xff09;设置container对象的属性MountPoints和属性HostConfig。
2&#xff09;在/var/lib/docker/containers/<容器ID>目录下创建文本文件&#xff1a;config.v2.json和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 :&#61; daemon.registerMountPoints(container, hostConfig); err !&#61; nil {return err}container.Lock()defer container.Unlock()// 1&#xff09;hostConfig使用了link机制的话&#xff0c;则进行相应的操作。// 2&#xff09;将hostConfig对象写到磁盘&#xff1a;/var/lib/docker/containers/{容器ID}/hostconfig.jsonif err :&#61; daemon.registerLinks(container, hostConfig); err !&#61; nil {return err}// 如果入参hostConfig的NetworkMode为""&#xff0c;则设置为"default"runconfig.SetDefaultNetModeIfBlank(hostConfig)// container对象的属性HostConfig的内容是一堆空值&#xff0c;因此将它直接设置为入参hostConfigcontainer.HostConfig &#61; hostConfig// 把container对象持久化到磁盘中&#xff1a;/var/lib/docker/containers/{容器ID}/config.v2.jsonreturn container.CheckpointTo(daemon.containersReplica)
}
创建容器的过程比较简单&#xff0c;本质就是在内存中创建容器对象和在宿主机上创建目录和文件&#xff0c;以及发生一些挂载操作和可能发生的数据复制&#xff08;docker volume&#xff09;。