Docker 深入篇之 Build 原理

使用 Docker 时,最常用的命令无非是 docker containerdocker image 相关的子命令,当然最初没有管理类命令(或者说分组)的时候,最常使用的命令也无非是 docker run docker commit docker builddocker images 这些。

今天来聊一下和 Docker 中核心概念 image 相关的重要命令, docker build 或者说 docker image build 为了简便起见,下文的命令全部使用 docker build

Docker Image

先简单介绍下 Docker Image, 通常情况下我们将其称之为镜像,镜像是由多个层组成的文件,这些层用于在容器内执行代码(命令)等。每个镜像基本上都是根据应用程序完整的可执行版本进行构建的,并且需要注意的是,它会依赖于主机的系统内核。当用户在运行镜像时,这将会创建一个或者多个容器实例。

Dockerd

Dockerd 是 Docker 的服务端,默认情况下提供 Unix Domain Socket 连接,当然也可以监听某个端口,用于对外提供服务。 所以有时候,我们也可以使用服务器上的 Docker daemon 来提供服务,以加快构建速度及解决一些网络问题之类的。

好的,基础概念了解了, 那我们开始进入正题。

使用 Dockerfile

我们知道构建镜像的方法有多种,本文中我们只介绍使用 Dockerfile 通过 docker build 的方式构建镜像。

为了简便,我们以一个简单的 Dockerfile 开始。构建一个容器内使用的 kubectl 工具 (当然选择它的原因在于 kubectl 足够大,并不考虑可用性,这个稍后解释)

FROM scratch

LABEL maintainer='Jintao Zhang <moelove.info>'

ADD kubectl /kubectl
ENTRYPOINT [ "/kubectl" ]

Dockerfile 足够简单,只是将 kubectl 的二进制文件拷贝进去,并将 Entrypoint 设置为 kubectl 。

Dockerd in Docker

我个人一般为了避免环境的污染,大多数的事情都在容器内完成。包括 dockerd 我也启在容器内。其中的原理不再介绍,可以参考我之前的文章或分享。使用起来很简单:

docker run --privileged -d -P docker:stable-dind

注意这里使用了 -P 所以本地会随机映射一个端口,当然你也可以直接指定映射到容器内的 2375 端口。

(Tao) ➜  build git:(master) docker ps                                                       
CONTAINER ID        IMAGE                       COMMAND                  CREATED             STATUS              PORTS                     NAMES
b56f6483614d        docker:stable-dind          "dockerd-entrypoint.…"   9 hours ago         Up 9 hours          0.0.0.0:32769->2375/tcp   trusting_babbage

构建

我们直接使用启动在容器内的 dockerd 进行构建,通过上面的 docker ps 命令可以看到是映射到了本地的 32769 端口。所以我们使用以下命令进行构建:

(Tao) ➜  kubectl git:(master) docker -H 0.0.0.0:32769 images                                                              
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE                                      
(Tao) ➜  kubectl git:(master) docker -H 0.0.0.0:32769 build -t local/kubectl .                                            
Sending build context to Docker daemon  55.09MB
Step 1/4 : FROM scratch
 --->
Step 2/4 : LABEL maintainer='Jintao Zhang <moelove.info>'
 ---> Running in ebcf44071bf0
Removing intermediate container ebcf44071bf0
 ---> eb4ea1725ff2
Step 3/4 : ADD kubectl /kubectl
 ---> 1aad06c4dbb4
Step 4/4 : ENTRYPOINT [ "/kubectl" ]
 ---> Running in 2fc78fe974e3
Removing intermediate container 2fc78fe974e3
 ---> 457802d4bf3e
Successfully built 457802d4bf3e
Successfully tagged local/kubectl:latest
(Tao) ➜  kubectl git:(master) docker -H 0.0.0.0:32769 images                  
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
local/kubectl       latest              457802d4bf3e        3 seconds ago       55.1MB

看日志及结果,可以看到我们已经成功的构建了我们所需的镜像。说了这么多,其实我们今天的内容才刚刚开始。

深入原理

Dockerd 服务

在本文一开始,我已经提过 Dockerd 是 Docker 的后端服务,通过上面的

docker -H 0.0.0.0:32769 images

这条命令可以看到我们通过 -H 指定了本地 32679 端口的 dockerd 服务,这其实是个 HTTP 服务,我们来验证下。

(Tao) ➜  kubectl git:(master) curl -i   0.0.0.0:32769/_ping
HTTP/1.1 200 OK
Api-Version: 1.37
Docker-Experimental: false
Ostype: linux
Server: Docker/18.03.1-ce (linux)
Date: Tue, 04 Sep 2018 17:20:51 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8

OK%

可以看到几条关键的信息 Api-Version: 1.37 这个表明了当前使用的 API 版本,本文的内容也是以 1.37 为例进行介绍,这是当前的稳定版本。我们也可以通过 docker version 进行查看。

(Tao) ➜  kubectl git:(master) docker -H 0.0.0.0:32769 version                                                             
Client:
 Version:           18.06.0-ce
 API version:       1.37 (downgraded from 1.38)
 Go version:        go1.10.3
 Git commit:        0ffa825
 Built:             Wed Jul 18 19:11:45 2018
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.03.1-ce
  API version:      1.37 (minimum version 1.12)
  Go version:       go1.9.5
  Git commit:       9ee9f40
  Built:            Thu Apr 26 07:23:03 2018
  OS/Arch:          linux/amd64
  Experimental:     false

可以看到我本地在用的 docker cli 版本较高,当连接到低版本的 dockerd 时,API 版本降级至与 dockerd 版本保持一致。

当然,你可能会问,如果是 dockerd 版本高会如何呢?其实我日常中的开发环境就是这样,大多数 API 都没什么影响, 不过这并不是今天的重点。

root@bdcdac73ee20:/# docker version
Client:
 Version:      17.06.0-ce
 API version:  1.30
 Go version:   go1.8.3
 Git commit:   02c1d87
 Built:        Fri Jun 23 21:15:15 2017
 OS/Arch:      linux/amd64

Server:
 Version:      dev
 API version:  1.39 (minimum version 1.12)
 Go version:   go1.10.3
 Git commit:   e8cc5a0b3
 Built:        Tue Sep  4 10:00:36 2018
 OS/Arch:      linux/amd64
 Experimental: false

build context

回到我们上面的构建过程中。我们可以看到日志内容的第一行:

...
Sending build context to Docker daemon  55.09MB

从这条日志,我们可以得到两个信息:

  • 构建的过程是将 build context 发送给 dockerd , 实际的构建压力在 dockerd 上
  • 发送了 55.09 MB

第一条结论,我们在上一小节已经讨论过了,我们来重点看下第二条结论。

(Tao) ➜  kubectl git:(master) ls -al 
总用量 53808
drwxrwxr-x. 2 tao tao     4096 9月   5 01:00 .
drwxrwxr-x. 3 tao tao     4096 9月   5 00:57 ..
-rw-rw-r--. 1 tao tao      109 9月   5 01:00 Dockerfile
-rwxrwxr-x. 1 tao tao 55084063 9月   5 00:53 kubectl
(Tao) ➜  kubectl git:(master) du -sh .
53M     .
(Tao) ➜  kubectl git:(master) du -sh kubectl Dockerfile 
53M     kubectl
4.0K    Dockerfile

按照我们 Dockerfile 的内容,我们需要将 kubectl 的二进制包放入镜像内,所以 build context 虽然比二进制文件多出来 2M 左右的大小你也不会很意外。

但我这里做了另一个例子,不多赘述,代码可以在我的 GitHub 中找到。这里贴出来结果:

(Tao) ➜  text git:(master) ls -al                                                                                          
总用量 16                                                                                                                  
drwxrwxr-x. 2 tao tao 4096 9月   5 01:45 .                                                                                
drwxrwxr-x. 4 tao tao 4096 9月   5 01:44 ..                             
-rw-rw-r--. 1 tao tao   77 9月   5 01:45 Dockerfile                       
-rw-rw-r--. 1 tao tao   61 9月   5 01:45 file  
(Tao) ➜  text git:(master) du -b Dockerfile file
77      Dockerfile
61      file                                                                                                              
(Tao) ➜  text git:(master) docker -H 0.0.0.0:32769 build --no-cache=true -t local/file .                                  
Sending build context to Docker daemon  3.072kB
...

相信你看到这个结果已经明白我想表达的意思,我们继续探索下这个过程。

/build 请求

前面我们已经说过,这就是个普通的 HTTP 请求,所以我们当然可以直接抓包来看看到底发生了什么?

很简单,通过 dockerd 的地址,使用 POST 方法,访问 /build 接口, 当然实际情况是会增加前缀,即我在上面提到的版本号,在目前的环境中使用的是 /v1.37/build 这个接口。

而这个请求携带了一些很有用的参数,和头信息。这里我来简单说下:

Header

build 请求的头部,主要有以下两个

  • Content-Type 默认值为 application/x-tar,表明自己是一个归档。
  • X-Registry-Config 这个头部信息中包含着 registry 的地址及认证信息,并且以 base64 进行编码。对 docker 熟悉的朋友或者看过我之前文章的朋友应该知道, Docker cli 在 login 成功后,会将认证信息保存至本地,密码做 base64 保存。而 build 的时候则会将此信息再次 base64 进行编码。通过这里也可以看出来,在使用远端 Dockerd 的时候, 应该尽量配置 TLS 以防止中间人攻击,造成密码泄漏等情况。

Parameters

请求参数中,列几个比较有意义的:

  • t 这其实就是我们 docker build -t 时候指定的参数,并且,我们可以同时指定多个 -t 同时构建多个不同名称的镜像。
  • memory cpusetcpus 这些主要用于资源限制
  • buildargs 如果想要了解这个参数,可以回忆下 Dockerfile 中的 ARG 指令的用法

当然,我们想要探索的过程其实重点就在于请求头部了, 整个请求的输入流,必须是一个 tar 压缩包,并且支持 identity (不压缩), gzip, bzip2, xz 等压缩算法。

实现

我们来看下基本的实现:

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
}

总结

这篇主要内容集中在 docker build 的过程及其原理上,为什么首先要写这篇,主要是因为镜像和我们息息相关, 并且也是我们使用的第一步。而很多情况下,推进业务容器化也都要面临着性能优化及其他规范之类的。

其实关于 build 的细节还有很多,如果有空,我就再更新下一篇。

原文发布于微信公众号 - MoeLove(TheMoeLove)

原文发表时间:2018-09-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IT笔记

Docker学习之搭建JavaWeb环境

自上次从北京参加阿里云社区开发者进阶大会回来,就萌发了学习Docker的种子,尽管公司现在的业务并没有什么需求,但学习先进的东西总没有坏处。

5617
来自专栏bdcn

CoreOS那些事之Rkt容器尝鲜(下) 转

2015年是各种容器技术与名词扎堆的一年,Docker的出现使得“应用容器”的实施变得易如反掌的同时,也带动了它的许多竞争者。其中一个比较有趣的看点就在于“容器...

2092
来自专栏Debian社区

Docker 17.06 社区版发布

今天我们发布了Docker CE 17.06,它包含了诸多新特性、优化和bug修复。我们在四月份的DockeCon上公布了Moby项目,Docker CE 17...

1314
来自专栏Coding01

初次学习 Docker Volume 的基本使用 (四)

在很早的一篇帖子里 http://dockone.io/question/24 就有人问:「请教下代码放在 Docker 里面还是外面呢」多数人评论类似下面的观...

972
来自专栏Jerry的SAP技术分享

最简单的教程:在Ubuntu操作系统里安装Docker

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容...

1054
来自专栏marsggbo

OpenShift的容器映像(第3部分):使你的映像可用

这是我在2017年欧洲、中东和非洲(EMEA)红帽技术交流会议上的一个会议记录,该会议集合了EMEA所有红帽解决方案架构师和顾问。它主要讨论在创建运行于Open...

2229
来自专栏云鼎实验室的专栏

ShadowBrokers 方程式工具包浅析

臭名昭著的方程式组织工具包再次被公开,TheShadowBrokers在steemit.com博客上提供了相关消息。本次被公开的工具包大小为117.9MB,包含...

2.4K0

构建远程缓存系统

上个月,我们的工程团队发布了一个大的更新,关于在使用我们的Docker平台Jet时Docker镜像是如何被缓存和存储的。在本文中,我们将讨论更新的动机,特性的设...

2406
来自专栏跟我一起学Docker

第二章 Docker环境安装

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实...

2413
来自专栏王小雷

Docker网络管理机制实例解析+创建自己Docker网络

实例解析Docker网络管理机制(bridge network,overlay network),介绍Docker默认的网络方式,并创建自己的网络桥接方式,将开...

2489

扫码关注云+社区

领取腾讯云代金券