[ docker-ce源码分析系列 ]docker build源码简析

  • Post author:
  • Post category:其他




1 概述:



1.1 环境

版本信息如下:

a、操作系统: centos 7.6,amd64

b、服务器docker版本:v18.09.2

c、docker的存储驱动: overlay2



2 源码简析:

用户执行docker build命令,本篇文章简要分析docker daemon构建镜像的过程。



2.1 服务端注册路由initRoutes()

r.postBuild就是处理docker build请求的方法。

func (r *buildRouter) initRoutes() {
	r.routes = []router.Route{		
		router.NewPostRoute("/build", r.postBuild, router.WithCancel),
		router.NewPostRoute("/build/prune", r.postPrune, router.WithCancel),
		router.NewPostRoute("/build/cancel", r.postCancel),
	}
}



2.2 postBuild(…)

核心方法是br.backend.Build(…)。

func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {

	/*
		其他非核心代码
	*/

	imgID, err := br.backend.Build(ctx, backend.BuildConfig{
		Source:         body,
		Options:        buildOptions,
		ProgressWriter: buildProgressWriter(out, wantAux, createProgressReader),
	})

	/*
		其他非核心代码
	*/

	return nil
}



2.3 func (b *Backend) Build(…)

br.backend的实现位于docker/api/server/backend/build/backend.go。

// 构建一个容器镜像,返回结果镜像的ID
// 核心代码行是b.builder.Build(ctx, config)
func (b *Backend) Build(ctx context.Context, config backend.BuildConfig) (string, error) {
	options := config.Options
	useBuildKit := options.Version == types.BuilderBuildKit

	tagger, err := NewTagger(b.imageComponent, config.ProgressWriter.StdoutFormatter, options.Tags)
	if err != nil {
		return "", err
	}

	// 变量build是构建的结果,build.ImageID是用来结果镜像的ID
	var build *builder.Result
	if useBuildKit {
		// 使用buildkit构建器进行构建
		build, err = b.buildkit.Build(ctx, config)
		if err != nil {
			return "", err
		}
	} else {
		// 使用默认的构建器进行构建	
		// b.builder的实现是github.com/docker/docker/builder/dockerfile.BuilderManager结构体
		// 这个if分支是本篇文章的关注核心。
		build, err = b.builder.Build(ctx, config)
		if err != nil {
			return "", err
		}
	}

	if build == nil {
		return "", nil
	}

	var imageID = build.ImageID
	
	/*
		如果在docker build命令中开启了压缩镜像,则进行压缩镜像
	*/
	
	
	/*
		当使用默认构建器时,打印信息
	*/
	
	return imageID, err
}



2.4 func (bm *BuildManager) Build(…)

b.builder的实现位于docker/builder/dockerfile/builder.go。

// 解析Dockerfile文件的内容到内存对象中,开始构建镜像
func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) (*builder.Result, error) {
	buildsTriggered.Inc()
	if config.Options.Dockerfile == "" {
		config.Options.Dockerfile = builder.DefaultDockerfileName
	}

	// 1) 解析Dockerfile文件中的内容
	source, dockerfile, err := remotecontext.Detect(config)
	if err != nil {
		return nil, err
	}
	defer func() {
		if source != nil {
			if err := source.Close(); err != nil {
				logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err)
			}
		}
	}()

	ctx, cancel := context.WithCancel(ctx)
	defer cancel()


	// 2) 给一个机会覆盖变量source
	// bm.initializeClientSession()一般返回nil, nil
	if src, err := bm.initializeClientSession(ctx, cancel, config.Options); err != nil {
		return nil, err
	} else if src != nil {
		source = src
	}

	// 3) 构建器的选项,内容其实都来自方法的入参和方法的接收者
	builderOptions := builderOptions{
		Options:        config.Options,
		ProgressWriter: config.ProgressWriter,
		Backend:        bm.backend,
		PathCache:      bm.pathCache,
		IDMapping:      bm.idMapping,
	}
	
	// 4) 根据构建器的选项,创建一个Dockerfile构建器对象
	b, err := newBuilder(ctx, builderOptions)
	if err != nil {
		return nil, err
	}
	
	// 5) 真正构建镜像,代码位于docker/builder/dockerfile/builder.go
	return b.build(source, dockerfile)
}

局部变量dockerfile的内容如下:

在这里插入图片描述



2.5 func (b *Builder) Build(…)

Dockerfile构建器位于docker/builder/dockerfile/builder.go。

func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*builder.Result, error) {
	defer b.imageSources.Unmount()

	stages, metaArgs, err := instructions.Parse(dockerfile.AST)
	/*
		容错代码
	*/

	/*
		无关紧要的代码
	*/
	
	dispatchState, err := b.dispatchDockerfileWithCancellation(stages, metaArgs, dockerfile.EscapeToken, source)
	if err != nil {
		return nil, err
	}
	
	// dispatchState.imageID为空,说明构建过程并没有生成镜像
	if dispatchState.imageID == "" {
		buildsFailed.WithValues(metricsDockerfileEmptyError).Inc()
		return nil, errors.New("No image was generated. Is your Dockerfile empty?")
	}
	
	// 返回构建结果
	return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil
}



2.6 func (b *Builder) dispatchDockerfileWithCancellation(…)

构建镜像的过程,就是不断地将rw层变成ro层的过程:

1)例如COPY指令,是直接在宿主机的文件系统中创建一个目录作为rw层,然后复制文件到rw层,rw层和之前的ro层形成一个新的半成品镜像。

2)例如RUN指令,会运行一个新容器来执行RUN指令,RUN指令会导致容器rw层发生改变,最后将rw层的数据复制到新的ro层,新的ro层和之前的ro层形成一个新的半成品镜像。

从代码结构上看,是两个for循环不断地调用静态方法dispatch(…)方法,因此应该关注的行是

if err := dispatch(dispatchRequest, cmd)。

func (b *Builder) dispatchDockerfileWithCancellation(parseResult []instructions.Stage, metaArgs []instructions.ArgCommand, escapeToken rune, source builder.Source) (*dispatchState, error) {
	dispatchRequest := dispatchRequest{}
	buildArgs := NewBuildArgs(b.options.BuildArgs)
	totalCommands := len(metaArgs) + len(parseResult)
	currentCommandIndex := 1
	for _, stage := range parseResult {
		totalCommands += len(stage.Commands)
	}
	shlex := shell.NewLex(escapeToken)
	for _, meta := range metaArgs {
		currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, &meta)

		err := processMetaArg(meta, shlex, buildArgs)
		if err != nil {
			return nil, err
		}
	}

	stagesResults := newStagesBuildResults()

	
	// 遍历Dockerfile中的阶段stage,每个stage中又遍历其所有的Dockerfile指令	
	// 遍历到的Dockerfile指令,本质是rw层会变成ro层和之前的ro层形成一个新的半成品镜像。
	// 例如COPY指令,是直接在宿主机的文件系统中创建一个目录作为rw层,然后直接复制文件到rw层,rw层和之前的ro层形成一个新的半成品镜像。
	// 例如RUN指令,会运行一个新容器来执行RUN指令,新容器的rw层会和之前的ro层形成一个新的半成品镜像。
	for _, stage := range parseResult {
		if err := stagesResults.checkStageNameAvailable(stage.Name); err != nil {
			return nil, err
		}
		dispatchRequest = newDispatchRequest(b, escapeToken, source, buildArgs, stagesResults)

		currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, stage.SourceCode)
		if err := initializeStage(dispatchRequest, &stage); err != nil {
			return nil, err
		}
		dispatchRequest.state.updateRunConfig()
		fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID))
		
		// 遍历当前stage中的所有Dockerfile指令
		for _, cmd := range stage.Commands {
			select {
			case <-b.clientCtx.Done():
				logrus.Debug("Builder: build cancelled!")
				fmt.Fprint(b.Stdout, "Build cancelled\n")
				buildsFailed.WithValues(metricsBuildCanceled).Inc()
				return nil, errors.New("Build cancelled")
			default:
				// Not cancelled yet, keep going...
			}

			// 打印Dockerfile指令
			currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, cmd)

			// 执行一个具体的Dockerfile指令,每次执行都会诞生一个半成品镜像
			// 新的半成品镜像又会变成下一个指令的基础镜像
			if err := dispatch(dispatchRequest, cmd); err != nil {
				return nil, err
			}
			
			dispatchRequest.state.updateRunConfig()
			
			// 打印半成品镜像的ID
			fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID))

		}
		if err := emitImageID(b.Aux, dispatchRequest.state); err != nil {
			return nil, err
		}
		buildArgs.MergeReferencedArgs(dispatchRequest.state.buildArgs)
		
		// 提交stage,本质是将入参dispatchRequest.state的部分数据保存到入参stagesResults中。
		if err := commitStage(dispatchRequest.state, stagesResults); err != nil {
			return nil, err
		}
	}
	
	buildArgs.WarnOnUnusedBuildArgs(b.Stdout)
	
	// 返回构建结果
	return dispatchRequest.state, nil
}



2.7 静态方法func dispatch(…)

根据入参对象cmd的不同,调用不同的静态方法。本篇文章主要看dispatchRun(…)这个静态方法,其他静态方法的原理是类似的。

func dispatch(d dispatchRequest, cmd instructions.Command) (err error) {
	if c, ok := cmd.(instructions.PlatformSpecific); ok {
		err := c.CheckPlatform(d.state.operatingSystem)
		if err != nil {
			return errdefs.InvalidParameter(err)
		}
	}
	runConfigEnv := d.state.runConfig.Env
	envs := append(runConfigEnv, d.state.buildArgs.FilterAllowed(runConfigEnv)...)

	if ex, ok := cmd.(instructions.SupportsSingleWordExpansion); ok {
		err := ex.Expand(func(word string) (string, error) {
			return d.shlex.ProcessWord(word, envs)
		})
		if err != nil {
			return errdefs.InvalidParameter(err)
		}
	}

	defer func() {
		if d.builder.options.ForceRemove {
			d.builder.containerManager.RemoveAll(d.builder.Stdout)
			return
		}
		if d.builder.options.Remove && err == nil {
			d.builder.containerManager.RemoveAll(d.builder.Stdout)
			return
		}
	}()
	
	// 判断入参的类型,调用不同的静态方法
	switch c := cmd.(type) {
	case *instructions.EnvCommand:
		return dispatchEnv(d, c)
	case *instructions.MaintainerCommand:
		return dispatchMaintainer(d, c)
	case *instructions.LabelCommand:
		return dispatchLabel(d, c)
	case *instructions.AddCommand:
		return dispatchAdd(d, c)
	case *instructions.CopyCommand:
		return dispatchCopy(d, c)
	case *instructions.OnbuildCommand:
		return dispatchOnbuild(d, c)
	case *instructions.WorkdirCommand:
		return dispatchWorkdir(d, c)
	case *instructions.RunCommand:
		return dispatchRun(d, c)
	case *instructions.CmdCommand:
		return dispatchCmd(d, c)
	case *instructions.HealthCheckCommand:
		return dispatchHealthcheck(d, c)
	case *instructions.EntrypointCommand:
		return dispatchEntrypoint(d, c)
	case *instructions.ExposeCommand:
		return dispatchExpose(d, c, envs)
	case *instructions.UserCommand:
		return dispatchUser(d, c)
	case *instructions.VolumeCommand:
		return dispatchVolume(d, c)
	case *instructions.StopSignalCommand:
		return dispatchStopSignal(d, c)
	case *instructions.ArgCommand:
		return dispatchArg(d, c)
	case *instructions.ShellCommand:
		return dispatchShell(d, c)
	}
	return errors.Errorf("unsupported command type: %v", reflect.TypeOf(cmd))
}



2.8 静态方法func dispatchRun(…)


func dispatchRun(d dispatchRequest, c *instructions.RunCommand) error {

	/*
		其他代码
	*/

	// 创建一个容器对象,返回容器的ID
	cID, err := d.builder.create(runConfig)
	if err != nil {
		return err
	}

	// 真正运行容器,容器中执行的命令就是Dockerfile中的RUN指令
	if err := d.builder.containerManager.Run(d.builder.clientCtx, cID, d.builder.Stdout, d.builder.Stderr); err != nil {
		if err, ok := err.(*statusCodeError); ok {
			msg := fmt.Sprintf(
				"The command '%s' returned a non-zero code: %d",
				strings.Join(runConfig.Cmd, " "), err.StatusCode())
			if err.Error() != "" {
				msg = fmt.Sprintf("%s: %s", msg, err.Error())
			}
			return &jsonmessage.JSONError{
				Message: msg,
				Code:    err.StatusCode(),
			}
		}
		return err
	}

	// 此时Dockerfile RUN指令执行完毕
	// 将容器的rw层变成ro层,具体实现过程中会发生文件复制和镜像元数据提交。
	return d.builder.commitContainer(d.state, cID, runConfigForCacheProbe)
}



2.9 func (b *Builder) commitContainer(…)

本方法的核心是b.docker.CommitBuildStep(commitCfg)。

func (b *Builder) commitContainer(dispatchState *dispatchState, id string, containerConfig *container.Config) error {
	if b.disableCommit {
		return nil
	}

	commitCfg := backend.CommitConfig{
		Author: dispatchState.maintainer,
		// TODO: this copy should be done by Commit()
		Config:          copyRunConfig(dispatchState.runConfig),
		ContainerConfig: containerConfig,
		ContainerID:     id,
	}

	// imageID就是新的半成品镜像的ID
	imageID, err := b.docker.CommitBuildStep(commitCfg)
	dispatchState.imageID = string(imageID)
	return err
}



2.10 func (i *ImageService) CommitBuildStep(…)

本质是创建新的ro层。


func (i *ImageService) CommitBuildStep(c backend.CommitConfig) (image.ID, error) {
	// 根据容器ID获取容器对象container
	container := i.containers.Get(c.ContainerID)
	if container == nil {
		// TODO: use typed error
		return "", errors.Errorf("container not found: %s", c.ContainerID)
	}
	// 将容器对象container 中的部分数据复制给入参c
	c.ContainerMountLabel = container.MountLabel
	c.ContainerOS = container.OS
	c.ParentImageID = string(container.ImageID)
	
	// 将容器的rw层的数据复制到新的ro层,并更新镜像元数据
	// 返回值就是新的半成品镜像的ID
	return i.CommitImage(c)
}



2.11 func (i *ImageService) CommitImage(…)

核心步骤有2步:

第一步:在/var/lib/docker/overlay2/xxx/目录下创建目录和文件,并把容器的rw层中的文件都复制(使用docker-untar命令)到/var/lib/docker/overlay2/xxx/目录的diff目录中。

第二步:保存容器镜像的元数据。

func (i *ImageService) CommitImage(c backend.CommitConfig) (image.ID, error) {
	layerStore, ok := i.layerStores[c.ContainerOS]
	if !ok {
		return "", system.ErrNotSupportedOperatingSystem
	}
	
	// 导出容器的rw层,因为后续要进行数据复制
	rwTar, err := exportContainerRw(layerStore, c.ContainerID, c.ContainerMountLabel)
	if err != nil {
		return "", err
	}
	defer func() {
		if rwTar != nil {
			rwTar.Close()
		}
	}()

	var parent *image.Image
	if c.ParentImageID == "" {
		parent = new(image.Image)
		parent.RootFS = image.NewRootFS()
	} else {
		parent, err = i.imageStore.Get(image.ID(c.ParentImageID))
		if err != nil {
			return "", err
		}
	}

	// 1) 在/var/lib/docker/overlay2/目录下创建一个xxx子目录(内容包括diff目录、work目录、link文件、lower文件),
	// 并把容器的rw层中的文件都复制(使用docker-untar命令)到xxx子目录的diff目录中。
	l, err := layerStore.Register(rwTar, parent.RootFS.ChainID())
	if err != nil {
		return "", err
	}
	defer layer.ReleaseAndLog(layerStore, l)

	cc := image.ChildConfig{
		ContainerID:     c.ContainerID,
		Author:          c.Author,
		Comment:         c.Comment,
		ContainerConfig: c.ContainerConfig,
		Config:          c.Config,
		DiffID:          l.DiffID(),
	}
	config, err := json.Marshal(image.NewChildImage(parent, cc, c.ContainerOS))
	if err != nil {
		return "", err
	}

	// 2) 这里操作的是关于image元数据的保存。	
	// 返回值id是入参字节切片config的哈希值(使用github.com/opencontainers/go-digest包),就是结果镜像的ID
	// i.imageStore.Create()方法的执行过程中会打开互斥锁。
	id, err := i.imageStore.Create(config)
	if err != nil {
		return "", err
	}

	if c.ParentImageID != "" {
		if err := i.imageStore.SetParent(id, image.ID(c.ParentImageID)); err != nil {
			return "", err
		}
	}
	return id, nil
}



3 总结:

Dockerfile其实是一个工作流脚本,根据这个脚本不断地将rw层”变成”ro层。

例如COPY指令,是直接在宿主机的文件系统中创建一个目录(/var/lib/docker/overlay2/xxx/)作为rw层,然后复制文件到rw层,rw层和之前的ro层形成一个新的半成品镜像。

例如RUN指令,会运行一个新容器来执行RUN指令,RUN指令会导致容器rw层发生改变,最后将rw层的数据复制到新的ro层,新的ro层和之前的ro层形成一个新的半成品镜像。



版权声明:本文为nangonghen原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。