追求极简:Docker镜像构建演化史


自从2013年dotCloud公司(现已改名为Docker Inc)发布Docker容器技术以来,到目前为止已经有四年多的时间了。这期间Docker技术飞速发展,并催生出一个生机勃勃的、以轻量级容器技术为基础的庞大的容器平台生态圈。作为Docker三大核心技术之一的镜像技术在Docker的快速发展之路上可谓功不可没:镜像让容器真正插上了翅膀,实现了容器自身的重用和标准化传播,使得开发、交付、运维流水线上的各个角色真正围绕同一交付物,“test what you write, ship what you test”成为现实。

对于已经接纳和使用Docker技术在日常开发工作中的开发者而言,构建Docker镜像已经是家常便饭。但如何更高效地构建以及构建出Size更小的镜像却是很多Docker技术初学者心中常见的疑问,甚至是一些老手都未曾细致考量过的问题。本文将从一个Docker用户角度来阐述Docker镜像构建的演化史,希望能起到一定的解惑作用。

1.镜像:继承中的创新


谈镜像构建之前,我们先来简要说一下镜像。

Docker技术从本质上说并不是一种新技术,而是将已有技术进行了更好地整合和包装。内核容器技术以一种完整形态最早出现在Sun公司的Solaris操作系统上,Solaris是当时最先进的服务器操作系统。2005年Sun发布了Solaris Container技术,从此开启了内核容器之门。

2008年,以Google公司开发人员为主导实现的Linux Container(即LXC)功能在被merge到Linux内核中。LXC是一种内核级虚拟化技术,主要基于Namespaces和Cgroups技术,实现共享一个操作系统内核前提下的进程资源隔离,为进程提供独立的虚拟执行环境,这样的一个虚拟的执行环境就是一个容器。本质上说,LXC容器与现在的Docker所提供容器是一样的。Docker也是基于Namespaces和Cgroups技术之上实现的。但Docker的创新之处在于其基于Union File System技术定义了一套容器打包规范,真正将容器中的应用及其运行的所有依赖都封装到一种特定格式的文件中去,而这种文件就被称为镜像(即image),原理见下图(引自Docker官网):

追求极简:Docker镜像构建演化史_第1张图片

图1:Docker镜像原理


镜像是容器的“序列化”标准,这一创新为容器的存储、重用和传输奠定了基础,并且容器镜像“坐上了巨轮”传播到世界每一个角落,助力了容器技术的飞速发展。

与Solaris Container、LXC等早期内核容器技术不同,Docker还为开发者提供了开发者体验良好的工具集,这其中就包括了用于镜像构建的Dockerfile以及一种用于编写Dockerfil的领域特定语言。采用Dockerfile方式构建成为镜像构建的标准方法,其可重复、可自动化、可维护以及分层精确控制等特点是采用传统采用docker commit命令提交的镜像所不能比拟的。

2.“镜像是个筐”:初学者的认知


“镜像是个筐,什么都往里面装”- 这句俏皮话可能是大部分Docker初学者对镜像最初认知的真实写照。这里我们用一个例子来生动地展示一下。 

我们现在将httpserver.go这个源文件编译为httpd程序并通过镜像发布。源文件的内容如下:

//httpserver.go
package mainimport (        "fmt"
        "net/http")func main() {        fmt.Println("http daemon start")
        fmt.Println("  -> listen on port:8080")
        http.ListenAndServe(":8080", nil)}


接下来,我们来编写用于构建目标镜像的Dockerfile:

// Dockerfile
From ubuntu:14.04

RUN apt-get update \
      && apt-get install -y software-properties-common \
      && add-apt-repository ppa:gophers/archive \
      && apt-get update \
      && apt-get install -y golang-1.9-go \
                                 git \
      && rm -rf /var/lib/apt/lists/*

ENV GOPATH /root/goENV GOROOT /usr/lib/go-1.9ENV PATH="/usr/lib/go-1.9/bin:${PATH}"COPY ./httpserver.go /root/httpserver.goRUN go build -o /root/httpd /root/httpserver.go \
      && chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]


执行镜像构建:

# docker build -t repodemo/httpd:latest .//...构建输出这里省略...# docker imagesREPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              183dbef8eba6        2 minutes ago       550MB
ubuntu                           14.04               dea1945146b9        2 months ago        188MB


整个镜像的构建过程因环境而定。如果您的网络速度一般,这个构建过程可能会花费你10多分钟甚至更多。最终如我们所愿,基于repodemo/httpd:latest这个镜像的容器可以正常运行:

# docker run repodemo/httpdhttp daemon start
  -> listen on port:8080


一个Dockerfile产出一个镜像。Dockerfile由若干Command组成,每个Command执行结果都会单独形成一个层(layer)。我们来探索一下构建出来的镜像:

# docker history 183dbef8eba6IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
183dbef8eba6        21 minutes ago      /bin/sh -c #(nop)  ENTRYPOINT ["/root/httpd"]   0B27aa721c6f6b        21 minutes ago      /bin/sh -c #(nop) WORKDIR /root                 0Ba9d968c704f7        21 minutes ago      /bin/sh -c go build -o /root/httpd /root/h...   6.14MB... ...aef7700a9036        30 minutes ago      /bin/sh -c apt-get update       && apt-get...   356MB
.... ...           2 months ago        /bin/sh -c #(nop) ADD file:8f997234193c2f5...   188MB


我们去除掉那些Size为0或很小的layer,我们看到三个size占比较大的layer,见下图: 
追求极简:Docker镜像构建演化史_第2张图片

图2:Docker镜像分层探索


虽然Docker引擎利用缓存机制可以让同主机下非首次的镜像构建执行得很快,但是在Docker技术热情催化下的这种构建思路让docker镜像在存储和传输方面的优势荡然无存,要知道一个ubuntu-server 16.04的虚拟机ISO文件的大小也就不过600多MB而已。

3.“理性的回归”:builder模式的崛起


Docker使用者在新技术接触初期的热情“冷却”之后迎来了“理性的回归”。根据上面分层镜像的图示,我们发现最终镜像中包含构建环境是多余的,我们只需要在最终镜像中包含足够支撑httpd运行的运行环境即可,而base image自身就可以满足。于是我们应该剔除不必要的中间层:

追求极简:Docker镜像构建演化史_第3张图片

图3:去除不必要的分层


现在问题来了!如果不在同一镜像中完成应用构建,那么在哪里、由谁来构建应用呢?至少有两种方法:

  1. 在本地构建并COPY到镜像中;

  2. 借助构建者镜像(builder image)构建。


不过方法1本地构建有很多局限性,比如:本地环境无法复用、无法很好融入持续集成/持续交付流水线等。而借助builder image进行构建已经成为Docker社区的一个最佳实践,Docker官方为此也推出了各种主流编程语言的官方base image,包括go、java、nodejs、python以及ruby的等。借助builder image进行镜像构建的流程原理如下图:

追求极简:Docker镜像构建演化史_第4张图片

图4:借助builder image进行镜像构建的流程图


通过原理图,我们可以看到整个目标镜像的构建被分为了两个阶段:

  1. 第一阶段:构建负责编译源码的构建者镜像;

  2. 第二阶段:将第一阶段的输出作为输入,构建出最终的目标镜像。


我们选择golang:1.9.2作为builder base image,构建者镜像的 Dockerfile.build如下:

// Dockerfile.buildFROM golang:1.9.2WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go


执行构建:

# docker build -t repodemo/httpd-builder:latest -f Dockerfile.build .


构建好的应用程序httpd放在了镜像repodemo/httpd-builder中的/go/src目录下,我们需要一些“胶水”命令来连接两个构建阶段,这些命令将httpd从构建者镜像中取出并作为下一阶段构建的输入:

# docker create --name extract-httpserver repodemo/httpd-builder# docker cp extract-httpserver:/go/src/httpd ./httpd# docker rm -f extract-httpserver# docker rmi repodemo/httpd-builder


通过上面的命令,我们将编译好的httpd程序拷贝到了本地。下面是目标镜像的Dockerfile:

// Dockerfile.targetFrom ubuntu:14.04COPY ./httpd /root/httpd
RUN chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]


接下来我们来构建目标镜像:

# docker build -t repodemo/httpd:latest -f Dockerfile.target .


我们来看看这个镜像的“体格”:

# docker imagesREPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              e3d009d6e919        12 seconds ago      200MB


200MB!目标镜像的Size降为原来的1/2还多。

4.“像赛车那样减去所有不必要的东西”:追求最小镜像


前面我们构建出的镜像的Size已经缩小到200MB,但这还不够。200MB的“体格”在我们的网络环境下缓存和传输仍然很难令人满意。我们要为镜像进一步减重,减到尽可能的小,就像赛车那样,为了能减轻重量将所有不必要的东西都拆除掉:我们仅保留能支撑我们的应用运行的必要库、命令,其余的一律不纳入目标镜像。当然不仅仅是Size上的原因,小镜像还有额外的好处,比如:内存占用小,启动速度快,更加高效;不会因其他不必要的工具、库的漏洞而被攻击,减少了“攻击面”,更加安全等。





宁波整形医院http://www.zuanno.com/

你可能感兴趣的:(追求极简:Docker镜像构建演化史)