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.
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.
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(¶ms.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
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).