乐趣区

关于docker:万字长文彻底搞懂容器镜像构建

大家好,我是张晋涛。

我将在这篇文章中深刻 Docker 的源码,与你聊聊镜像构建的原理。

Docker 架构

这里咱们先从宏观上对 Docker 有个大略的意识,它整体上是个 C/S 架构;咱们平时应用的 docker 命令就是它的 CLI 客户端,而它的服务端是 dockerd 在 Linux 零碎中,通常咱们是应用 systemd 进行治理,所以咱们能够应用 systemctl start docker 来启动服务。(然而请留神,dockerd 是否能运行与 systemd 并无任何关系,你能够像平时执行一个一般的二进制程序一样,间接通过 dockerd 来启动服务,留神须要 root 权限)

实际上也就是

(图片起源:docker overview)

docker CLI 与 dockerd 的交互是通过 REST API 来实现的,当咱们执行 docker version 的时候过滤 API 能够看到如下输入:

➜  ~ docker version |grep API
 API version:       1.41
  API version:      1.41 (minimum version 1.12)

下面一行是 docker CLI 的 API 版本,上面则代表了 dockerd 的 API 版本,它的前面还有个括号,是因为 Docker 具备了很良好的兼容性,这里示意它最小可兼容的 API 版本是 1.12。

对于咱们进行 C/S 架构的我的项目开发而言,个别都是 API 后行, 所以咱们先来看下 API 的局部。

当然,本文的主体是构建零碎相干的,所以咱们就间接来看构建相干的 API 即可。

接下来会说 CLI,代码以 v20.10.5 为准。最初说服务端 Dockerd。

API

Docker 保护团队在每个版本正式公布之后,都会将 API 文档公布进去,能够通过 Docker Engine API 在线浏览,也能够自行构建 API 文档。

首先 clone Docker 的源代码仓库, 进入我的项目仓库内执行 make swagger-docs 即可在启动一个容器同时将端口裸露至本地的 9000 端口,你能够间接通过 http://127.0.0.1:9000 拜访本地的 API 文档。

(MoeLove) ➜  git clone https://github.com/docker/docker.git docker
(MoeLove) ➜  cd docker
(MoeLove) ➜  docker git:(master) git checkout -b v20.10.5 v20.10.5
(MoeLove) ➜  docker git:(v20.10.5) make swagger-docs
API docs preview will be running at http://localhost:9000

关上 http://127.0.0.1:9000/#operation/ImageBuild 这个地址就能够看到 1.41 版本的构建镜像所需的 API 了。咱们对此 API 进行下剖析。

申请地址和办法

接口地址是 /v1.41/build 办法是 POST,咱们能够应用一个较新版本的 curl 工具来验证下此接口(须要应用 --unix-socket 连贯 Docker 监听的 UNIX Domain Socket)。dockerd 默认状况下监听在 /var/run/docker.sock,当然你也能够给 dockerd 传递 --host 参数用于监听 HTTP 端口或者其余门路的 unix socket .

/ # curl -X POST --unix-socket /var/run/docker.sock  localhost/v1.41/build 
{"message":"Cannot locate specified Dockerfile: Dockerfile"}

从下面的输入咱们能够看到,咱们的确拜访到了该接口,同时该接口的响应是提醒须要 Dockerfile .

申请体

A tar archive compressed with one of the following algorithms: identity (no compression), gzip, bzip2, xz.
string <binary>

申请体是一个 tar 归档文件,可抉择无压缩、gzipbzip2xz 压缩等模式。对于这几种压缩格局就不再开展介绍了,但值得注意的是 如果应用了压缩,则传输体积会变小,即网络耗费会相应缩小。但压缩 / 解压缩须要消耗 CPU 等计算资源 这在咱们对大规模镜像构建做优化时是个值得衡量的点。

申请头

因为要发送的是个 tar 归档文件,Content-type 默认是 application/x-tar。另一个会发送的头是 X-Registry-Config,这是一个由 Base64 编码后的 Docker Registry 的配置信息,内容与 $HOME/.docker/config.json 中的 auths 内的信息统一。

这些配置信息,在你执行 docker login 后会主动写入到 $HOME/.docker/config.json 文件内的。这些信息被传输到 dockerd 在构建过程中作为拉取镜像的认证信息应用。

申请参数

最初就是申请参数了,参数有很多,通过 docker build --help 根本都能够看到对应含意的,这里不再一一开展了,前面会有一些要害参数的介绍。

小结

下面咱们介绍了 Docker 构建镜像相干的 API,咱们能够间接拜访 Docker Engine 的 API 文档。或者通过源码仓库,本人来构建一个本地的 API 文档服务,应用浏览器进行拜访。

通过 API 咱们也晓得了该接口所需的申请体是一个 tar 归档文件(可抉择压缩算法进行压缩),同时它的申请头中会携带用户在镜像仓库中的认证信息。这揭示咱们,如果在应用近程 Dockerd 构建时,请注意安全,尽量应用 tls 进行加密,免得数据透露。

CLI

API 曾经介绍完了,咱们来看下 docker CLI,我以前的文章中介绍过当初 Docker 中有两个构建零碎,一个是 v1 版本的 builder 另一个是 v2 版本的即 BuildKit 咱们来别离深刻源码来看看在构建镜像时,他们各自的行为吧。

筹备代码

CLI 的代码仓库在 https://github.com/docker/cli 本文的代码以 v20.10.5 为准。

通过以下步骤应用此版本的代码:

(MoeLove) ➜  git clone https://github.com/docker/cli.git
(MoeLove) ➜  cd cli
(MoeLove) ➜  cli git:(master) git checkout -b v20.10.5 v20.10.5

逐渐合成

docker 是咱们所应用的客户端工具,用于与 dockerd 进行交互。对于构建相干的局部,咱们所熟知的便是 docker build 或者是 docker image build,在 19.03 中新增的是 docker builder build,但其实他们都是同一个只是做了个 alias 罢了:

// cmd/docker/docker.go#L237
if v, ok := aliasMap["builder"]; ok {
    aliases = append(aliases,
        [2][]string{{"build"}, {v, "build"}},
        [2][]string{{"image", "build"}, {v, "build"}},
    )
}

真正的入口函数其实在 cli/command/image/build.go;辨别如何调用的逻辑如下:

func runBuild(dockerCli command.Cli, options buildOptions) error {buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo())
    if err != nil {return err}
    if buildkitEnabled {return runBuildBuildKit(dockerCli, options)
    }
    // 省略掉了对于 builder 的理论逻辑
}

这里就是判断下是否反对 buildkit

// cli/command/cli.go#L176
func BuildKitEnabled(si ServerInfo) (bool, error) {
    buildkitEnabled := si.BuildkitVersion == types.BuilderBuildKit
    if buildkitEnv := os.Getenv("DOCKER_BUILDKIT"); buildkitEnv != "" {
        var err error
        buildkitEnabled, err = strconv.ParseBool(buildkitEnv)
        if err != nil {return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
        }
    }
    return buildkitEnabled, nil
}

当然,从这里能够失去两个信息:

  • 通过 dockerd 的配置可开启 buildkit。在 /etc/docker/daemon.json 中增加如下内容,并重启 dockerd 即可:
{
  "features": {"buildkit": true}
}
  • docker CLI 上也可开启 buildkit 的反对,并且 CLI 的配置可笼罩服务端配置。通过 export DOCKER_BUILDKIT=1 即可开启 buildkit 的反对,设置为 0 则敞开(0/false/f/F 之类的也都是雷同的后果)

从下面的介绍也看到了,对于本来默认的 builder 而言,入口逻辑在 runBuild 中,而对于应用 buildkit 的则是 runBuildBuildKit 接下来,咱们对两者进行逐渐合成。

builder v1

runBuild 函数中,大抵经验了以下阶段:

参数解决

最开始的局部是一些对参数的解决和校验。

  • streamcompress 不可同时应用。

因为如果咱们指定了 compress 的话,则 CLI 会应用 gzip 将构建上下文进行压缩,这样也就没法很好的通过 stream 的模式来解决构建的上下文了。

当然你也可能会想,从技术上来讲,压缩和流式没有什么必然的抵触,是可实现的。事实的确如此,如果从技术的角度上来讲两者并非齐全不能一起存在,无非就是减少解压缩的动作。然而当开启 stream 模式,对每个文件都进行压缩和解压的操作那将会是很大的资源节约,同时也减少了其复杂度,所以在 CLI 中便间接进行了限度,不容许同时应用 compressstream

  • 不可同时应用 stdin 读取 Dockerfilebuild context

在进行构建时,如果咱们将 Dockerfile 的名字传递为 - 时,示意从 stdin 读取其内容。

例如,某个目录下有三个文件 foo barDockerfile,通过管道将 Dockerfile 的内容通过 stdin 传递给 docker build

(MoeLove) ➜  x ls
bar  Dockerfile  foo
(MoeLove) ➜  x cat Dockerfile | DOCKER_BUILDKIT=0 docker build -f - .
Sending build context to Docker daemon  15.41kB
Step 1/3 : FROM scratch
 ---> 
Step 2/3 : COPY foo foo
 ---> a2af45d66bb5
Step 3/3 : COPY bar bar
 ---> cc803c675dd2
Successfully built cc803c675dd2

能够看到通过 stdin 传递 Dockerfile 的形式能胜利的构建镜像。接下来咱们尝试通过 stdinbuild context 传递进去。

(MoeLove) ➜  x tar -cvf x.tar foo bar Dockerfile 
foo                                                     
bar                         
Dockerfile
(MoeLove) ➜  x cat x.tar| DOCKER_BUILDKIT=0 docker build -f Dockerfile -
Sending build context to Docker daemon  10.24kB
Step 1/3 : FROM scratch
 ---> 
Step 2/3 : COPY foo foo
 ---> 09319712e220
Step 3/3 : COPY bar bar
 ---> ce88644a7395
Successfully built ce88644a7395

能够看到通过 stdin 传递 build context 的形式也能够胜利构建镜像。

但如果 Dockerfile 的名称与构建的上下文都指定为 -docker build -f - - 时,会产生什么呢?

(MoeLove) ➜  x DOCKER_BUILDKIT=0 docker build -f - -             
invalid argument: can't use stdin for both build context and dockerfile

就会报错了。所以,不能同时应用 stdin 读取 Dockerfilebuild context

  • build context 反对四种行为。
switch {case options.contextFromStdin():
    // 省略
case isLocalDir(specifiedContext):
    // 省略
case urlutil.IsGitURL(specifiedContext):
    // 省略
case urlutil.IsURL(specifiedContext):
    // 省略
default:
    return errors.Errorf("unable to prepare context: path %q not found", specifiedContext)
}

stdin 传入,上文曾经演示过了,传递给 stdin 的是 tar 归档文件。当然也能够是指定一个具体的 PATH,咱们通常应用的 docker build . 便是这种用法;

或者能够指定一个 git 仓库的地址,CLI 会调用 git 命令将仓库 clone 至一个长期目录,进行应用;

最初一种是,给定一个 URL 地址,该地址能够是 一个具体的 Dockerfile 文件地址 或者是 一个 tar 归档文件的下载地址

这几种根本就是字面上的区别,至于 CLI 的行为差别,次要是最初一种,当 URL 地址是一个具体的 Dockerfile 文件地址,在这种状况下 build context 相当于只有 Dockerfile 本身,所以并不能应用 COPY 之类的指定,至于 ADD 也只能应用可拜访的内部地址。

  • 可应用 .dockerignore 疏忽不须要的文件

我在之前的文章中有分享过相干的内容。这里咱们看看它的实现逻辑。

// cli/command/image/build/dockerignore.go#L13
func ReadDockerignore(contextDir string) ([]string, error) {var excludes []string

    f, err := os.Open(filepath.Join(contextDir, ".dockerignore"))
    switch {case os.IsNotExist(err):
        return excludes, nil
    case err != nil:
        return nil, err
    }
    defer f.Close()

    return dockerignore.ReadAll(f)
}
  • .dockerignore 是一个固定的文件名,并且须要放在 build context 的根目录下。相似后面提到的,应用一个 Dockerfile 文件的 URL 地址作为 build context 传入的形式,便无奈应用 .dockerignore
  • .dockerignore 文件能够不存在,但在读取的时候如果遇到谬误,便会抛出谬误。
  • 通过 .dockerignore 将会过滤掉不心愿退出到镜像内,或者过滤掉与镜像无关的内容。

最初 CLI 会将 build context 中的内容通过 .dockerignore 过滤后,打包成为真正的 build context 即真正的构建上下文。这也是为什么有时候你发现自己明明在 Dockerfile 外面写了 COPY xx xx 然而最初没有发现该文件的状况。很可能就是被 .dockerignore 给疏忽掉了。这样有利于优化 CLI 与 dockerd 之间的传输压力之类的。

  • docker CLI 还会去读取 ~/.docker/config.json 中的内容。

这与后面 API 局部所形容的内容根本是统一的。将认证信息通过 X-Registry-Config 头传递给 dockerd 用于在须要拉取镜像时进行身份校验。

  • 调用 API 进行理论构建工作

当所有所需的校验和信息都准备就绪之后,则开始调用 dockerCli.Client 封装的 API 接口,将申请发送至 dockerd,进行理论的构建工作。

response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
if err != nil {
    if options.quiet {fmt.Fprintf(dockerCli.Err(), "%s", progBuff)
    }
    cancel()
    return err
}
defer response.Body.Close()

到这里其实一次构建的过程中 CLI 所解决的流程就根本完结了,之后便是依照传递的参数进行进度的输入或是将镜像 ID 写入到文件之类的。这部分就不进行开展了。

小结

整个过程大抵如下图:

从入口函数 runBuild 开始,通过判断是否反对 buildkit,如果不反对 buildkit 则持续应用 v1 的 builder。接下来读取各类参数,依照不同的参数执行各类不同的解决逻辑。这里须要留神的就是 Dockerfilebuild context 都可反对从文件或者 stdin 等读入,具体应用时,须要留神。另外 .dockerignore 文件可过滤掉 build context 中的一些文件,在应用时,可通过此办法进行构建效率的优化,当然也须要留神,在通过 URL 获取 Dockerfile 的时候,是不存在 build context 的,所以相似 COPY 这样的命令也就无奈应用了。当所有的 build context 和参数都准备就绪后,接下来调用封装好的客户端,将这些申请依照本文开始之初介绍的 API 发送给 dockerd,由其进行真正的构建逻辑。

最初当构建完结后,CLI 依据参数决定是否要显示构建进度或者后果。

buildkit

接下来咱们来看看 buildkit 如何来执行构建,办法入口与 builder 统一,然而在 buildkitEnabled 处,因为开启了 buildkit 反对,所以跳转到了 runBuildBuildKit

func runBuild(dockerCli command.Cli, options buildOptions) error {buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo())
    if err != nil {return err}
    if buildkitEnabled {return runBuildBuildKit(dockerCli, options)
    }
    // 省略掉了对于 builder 的理论逻辑
}

创立会话

然而与 builder 不同的是,这里先执行了一次 trySession 函数。

// cli/command/image/build_buildkit.go#L50
s, err := trySession(dockerCli, options.context, false)
if err != nil {return err}
if s == nil {return errors.Errorf("buildkit not supported by daemon")
}

这个函数是用来做什么的呢?咱们来找到该函数所在的文件 cli/command/image/build_session.go

// cli/command/image/build_session.go#L29
func trySession(dockerCli command.Cli, contextDir string, forStream bool) (*session.Session, error) {if !isSessionSupported(dockerCli, forStream) {return nil, nil}
    sharedKey := getBuildSharedKey(contextDir)
    s, err := session.NewSession(context.Background(), filepath.Base(contextDir), sharedKey)
    if err != nil {return nil, errors.Wrap(err, "failed to create session")
    }
    return s, nil
}

当然还包含它其中最次要的 isSessionSupported 函数:

// cli/command/image/build_session.go#L22
func isSessionSupported(dockerCli command.Cli, forStream bool) bool {if !forStream && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.39") {return true}
    return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
}

isSessionSupported 很显著是用于判断是否反对 Session,这里因为咱们会传入 forStreamfalse,而且以后的 API 版本是 1.41 比 1.39 大,所以此函数会返回 true。其实在 builder 中也执行过雷同的逻辑,只不过是在传递了 --stream 参数后,应用 Session 获取一个长连贯以达到 stream 的解决能力。

这也就是为什么会有上面 dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31") 这个判断存在的起因了。

当确认反对 Session 时,则会调用 session.NewSession 创立一个新的会话。

// github.com/moby/buildkit/session/session.go#L47
func NewSession(ctx context.Context, name, sharedKey string) (*Session, error) {id := identity.NewID()

    var unary []grpc.UnaryServerInterceptor
    var stream []grpc.StreamServerInterceptor

    serverOpts := []grpc.ServerOption{}
    if span := opentracing.SpanFromContext(ctx); span != nil {tracer := span.Tracer()
        unary = append(unary, otgrpc.OpenTracingServerInterceptor(tracer, traceFilter()))
        stream = append(stream, otgrpc.OpenTracingStreamServerInterceptor(span.Tracer(), traceFilter()))
    }

    unary = append(unary, grpcerrors.UnaryServerInterceptor)
    stream = append(stream, grpcerrors.StreamServerInterceptor)

    if len(unary) == 1 {serverOpts = append(serverOpts, grpc.UnaryInterceptor(unary[0]))
    } else if len(unary) > 1 {serverOpts = append(serverOpts, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(unary...)))
    }

    if len(stream) == 1 {serverOpts = append(serverOpts, grpc.StreamInterceptor(stream[0]))
    } else if len(stream) > 1 {serverOpts = append(serverOpts, grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(stream...)))
    }

    s := &Session{
        id:         id,
        name:       name,
        sharedKey:  sharedKey,
        grpcServer: grpc.NewServer(serverOpts...),
    }

    grpc_health_v1.RegisterHealthServer(s.grpcServer, health.NewServer())

    return s, nil
}

它创立了一个长连贯会话,接下来的操作也都会基于这个会话来做。接下来的操作与 builder 大体一致,先判断 context 是以哪种模式提供的;当然它也与 builder 一样,是不容许同时从 stdin 获取 Dockerfilebuild context

switch {case options.contextFromStdin():
    // 省略解决逻辑
case isLocalDir(options.context):
    // 省略解决逻辑
case urlutil.IsGitURL(options.context):
    // 省略解决逻辑
case urlutil.IsURL(options.context):
    // 省略解决逻辑
default:
    return errors.Errorf("unable to prepare context: path %q not found", options.context)
}

这里的解决逻辑与 v1 builder 保持一致的起因,次要在于用户体验上,以后的 CLI 的性能曾经根本稳固,用户也曾经习惯,所以即便是减少了 BuildKit 也并没有对主体的操作逻辑造成多大扭转。

抉择输入模式

BuildKit 反对了三种不同的输入模式 local tar 和失常模式(即存储在 dockerd 中),格局为 -o type=local,dest=path 如果须要将构建的镜像进行散发,或是须要进行镜像内文件浏览的话,应用这个形式也是很不便的。

outputs, err := parseOutputs(options.outputs)
if err != nil {return errors.Wrapf(err, "failed to parse outputs")
}

for _, out := range outputs {
    switch out.Type {
    case "local":
        // 省略
    case "tar":
        // 省略
    }
}

其实它反对的模式还有第 4 种,名为 cacheonly 但它并不会像后面提到的三种模式一样,有个很直观的输入,而且用的人可能会很少,所以就没有独自写了。

读取认证信息

dockerAuthProvider := authprovider.NewDockerAuthProvider(os.Stderr)
s.Allow(dockerAuthProvider)

这里的行为与下面提到的 builder 的行为基本一致,这里次要有两个须要留神的点:

  • Allow() 函数
func (s *Session) Allow(a Attachable) {a.Register(s.grpcServer)
}

这个 Allow 函数就是容许通过下面提到的 grpc 会话拜访给定的服务。

  • authprovider

authproviderBuildKit 提供的一组形象接口汇合,通过它们能够拜访到机器上的配置文件,进而拿到认证信息,行为与 builder 基本一致。

高阶个性:mount secretsssh

我其余的文章讲过这两种高阶个性的应用了,本篇中就不再多应用进行过多阐明了,只来大体看下该局部的原理和逻辑。

secretsprovidersshprovider 都是 buildkit 在提供的,利用这两种个性能够在 Docker 镜像进行构建时更加平安,且更加灵便。

func parseSecretSpecs(sl []string) (session.Attachable, error) {fs := make([]secretsprovider.Source, 0, len(sl))
    for _, v := range sl {s, err := parseSecret(v)
        if err != nil {return nil, err}
        fs = append(fs, *s)
    }
    store, err := secretsprovider.NewStore(fs)
    if err != nil {return nil, err}
    return secretsprovider.NewSecretProvider(store), nil
}

对于 secrets 方面,最终的 parseSecret 会实现格局相干的校验之类的;

func parseSSHSpecs(sl []string) (session.Attachable, error) {configs := make([]sshprovider.AgentConfig, 0, len(sl))
    for _, v := range sl {c := parseSSH(v)
        configs = append(configs, *c)
    }
    return sshprovider.NewSSHAgentProvider(configs)
}

而对于 ssh 方面,则与上方的 secrets 基本一致,通过 sshprovider 容许进行 ssh 转发之类的,这里不再深刻开展了。

调用 API 发送构建申请

这里次要有两种状况。

  • build context 是从 stdin 读,并且是一个 tar 文件时
buildID := stringid.GenerateRandomID()
if body != nil {eg.Go(func() error {
        buildOptions := types.ImageBuildOptions{
            Version: types.BuilderBuildKit,
            BuildID: uploadRequestRemote + ":" + buildID,
        }

        response, err := dockerCli.Client().ImageBuild(context.Background(), body, buildOptions)
        if err != nil {return err}
        defer response.Body.Close()
        return nil
    })
}

它会执行上述这部分逻辑,但同时也要留神,这是应用的是 Golang 的 goroutine,到这里也并不是完结,这部分代码之后的代码也同样会被执行。这就说到了另一种状况了(通常状况)。

  • 应用 doBuild 实现逻辑
eg.Go(func() error {defer func() {s.Close()
    }()

    buildOptions := imageBuildOptions(dockerCli, options)
    buildOptions.Version = types.BuilderBuildKit
    buildOptions.Dockerfile = dockerfileName
    buildOptions.RemoteContext = remote
    buildOptions.SessionID = s.ID()
    buildOptions.BuildID = buildID
    buildOptions.Outputs = outputs
    return doBuild(ctx, eg, dockerCli, stdoutUsed, options, buildOptions)
})

doBuild 会做些什么呢?它同样也调用了 API 向 dockerd 发动了构建申请。

func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, stdoutUsed bool, options buildOptions, buildOptions types.ImageBuildOptions, at session.Attachable) (finalErr error) {response, err := dockerCli.Client().ImageBuild(context.Background(), nil, buildOptions)
    if err != nil {return err}
    defer response.Body.Close()
    // 省略
}

从以上的介绍咱们能够先做个小的总结。build contextstdin 读,并且是个 tar 归档时,理论会向 dockerd 发动两次 /build 申请 而个别状况下只会发送一次申请。

那这里会有什么差异呢?此处先不开展,咱们留到上面讲 dockerd 服务端的时候再来解释。

小结

这里咱们对开启了 buildkit 反对的 CLI 构建镜像的过程进行了剖析,大抵过程如下:

从入口函数 runBuild 开始,判断是否反对 buildkit,如果反对 buildkit 则调用 runBuildBuildKit。与 v1 的 builder 不同的是,开启了 buildkit 后,会首先创立一个长连贯的会话,并始终放弃。其次,与 builder 雷同,判断 build context 的起源,格局之类的,校验参数等。当然,buildkit 反对三种不同的输入格局 tar, local 或失常的存储于 Docker 的目录中。另外是在 buildkit 中新增的高阶个性,能够配置 secretsssh 密钥等性能。最初,再调用 API 与 dockerd 交互实现镜像的构建。

服务端:dockerd

下面别离介绍了 API,CLI 的 v1 builderbuildkit,接下来咱们看看服务端的具体原理和逻辑。

Client 函数

还记得下面局部中最初通过 API 与服务端交互的 ImageBuild 函数吗?在开始 dockerd 的介绍前,咱们来看下这个客户端接口的具体内容。

// github.com/docker/docker/client/image_build.go#L20
func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {query, err := cli.imageBuildOptionsToQuery(options)
    if err != nil {return types.ImageBuildResponse{}, err
    }

    headers := http.Header(make(map[string][]string))
    buf, err := json.Marshal(options.AuthConfigs)
    if err != nil {return types.ImageBuildResponse{}, err
    }
    headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf))

    headers.Set("Content-Type", "application/x-tar")

    serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers)
    if err != nil {return types.ImageBuildResponse{}, err
    }

    osType := getDockerOS(serverResp.header.Get("Server"))

    return types.ImageBuildResponse{
        Body:   serverResp.body,
        OSType: osType,
    }, nil
}

没有什么太特地的中央,行为与 API 统一。通过这里咱们确认它的确拜访的 /build 接口,所以,咱们来看看 dockerd/build 接口,看看它在构建镜像的时候做了什么。

dockerd

因为本文集中探讨的是构建零碎相干的局部,所以也就不再过多赘述与构建无关的内容了,咱们间接来看,当 CLI 通过 /build 接口发送申请后,会产生什么。

先来看该 API 的入口:

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

dockerd 提供了一套类 RESTful 的后端接口服务,解决逻辑的入口便是下面的 postBuild 函数。

该函数的内容较多,咱们来合成下它的次要步骤。

buildOptions, err := newImageBuildOptions(ctx, r)
if err != nil {return errf(err)
}

newImageBuildOptions 函数就是结构构建参数的,将通过 API 提交过去的参数转换为构建动作理论须要的参数模式。

buildOptions.AuthConfigs = getAuthConfigs(r.Header)

getAuthConfigs 函数用于从申请头拿到认证信息

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

这里就须要留神了: 真正的构建过程要开始了。应用 backend 的 Build 函数来实现真正的构建过程

// api/server/backend/build/backend.go#L53
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}

    var build *builder.Result
    if useBuildKit {build, err = b.buildkit.Build(ctx, config)
        if err != nil {return "", err}
    } else {build, err = b.builder.Build(ctx, config)
        if err != nil {return "", err}
    }

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

    var imageID = build.ImageID
    if options.Squash {if imageID, err = squashBuild(build, b.imageComponent); err != nil {return "", err}
        if config.ProgressWriter.AuxFormatter != nil {if err = config.ProgressWriter.AuxFormatter.Emit("moby.image.id", types.BuildResult{ID: imageID}); err != nil {return "", err}
        }
    }

    if !useBuildKit {
        stdout := config.ProgressWriter.StdoutFormatter
        fmt.Fprintf(stdout, "Successfully built %s\n", stringid.TruncateID(imageID))
    }
    if imageID != "" {err = tagger.TagImages(image.ID(imageID))
    }
    return imageID, err
}

这个函数看着比拟长,但次要性能就以下三点:

  • NewTagger 是用于给镜像打标签,也就是咱们的 -t 参数相干的内容,这里不做开展。
  • 通过判断是否应用了 buildkit 来调用不同的构建后端。
useBuildKit := options.Version == types.BuilderBuildKit

var build *builder.Result
if useBuildKit {build, err = b.buildkit.Build(ctx, config)
    if err != nil {return "", err}
} else {build, err = b.builder.Build(ctx, config)
    if err != nil {return "", err}
}
  • 解决构建实现后的动作。

到这个函数之后,就别离是 v1 builderbuildkitDockerfile 的解析,以及对 build context 的操作了。

这里波及到的内容与我下一篇文章《高效构建 Docker 镜像的最佳实际》的外部关联比拟大,此处就不再进行开展了。敬请期待下一篇文章。

总结

本文首先介绍了 Docker 的 C/S 架构,介绍了构建镜像所用的 API , API 文档能够在线查看或者本地构建。之后深刻到 Docker CLI 的源码中,逐渐合成 v1 builderbuildkit 在构建镜像时执行的过程的差别。最初,咱们深刻到 dockerd 的源码中,理解到了对不同构建后端的调用。至此,Docker 构建镜像的原理及主体代码就介绍结束。

但这还并不是完结,我会在后续文章中分享镜像构建的相干实际,敬请期待!


欢送订阅我的文章公众号【MoeLove】

退出移动版