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层形成一个新的半成品镜像。