标签 GCC 下的文章

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

本文首发于CSDN《程序员》杂志2017.12期,这里是原文地址

本文为《程序员》杂志授权转载,谢绝其他转载。全文如下:

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

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

一、镜像:继承中的创新

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

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

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

img{512x368}
图1:Docker镜像原理

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

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

二、“镜像是个筐”:初学者的认知

“镜像是个筐,什么都往里面装” – 这句俏皮话可能是大部分Docker初学者对镜像最初认知的真实写照。这里我们用一个例子来生动地展示一下。我们将httpserver.go这个源文件编译为httpd程序并通过镜像发布,考虑到被编译的源码并非本文重点,这里使用了一个极简的demo代码:

//httpserver.go

package main

import (
        "fmt"
        "net/http"
)

func main() {
        fmt.Println("http daemon start")
        fmt.Println("  -> listen on port:8080")
        http.ListenAndServe(":8080", nil)
}

接下来,我们来编写一个用于构建目标image的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/go
ENV GOROOT /usr/lib/go-1.9
ENV PATH="/usr/lib/go-1.9/bin:${PATH}"

COPY ./httpserver.go /root/httpserver.go
RUN go build -o /root/httpd /root/httpserver.go \
      && chmod +x /root/httpd

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

构建这个Image:

# docker build -t repodemo/httpd:latest .
//...构建输出这里省略...

# docker images
REPOSITORY                       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/httpd
http daemon start
  -> listen on port:8080

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

# docker history 183dbef8eba6
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
183dbef8eba6        21 minutes ago      /bin/sh -c #(nop)  ENTRYPOINT ["/root/httpd"]   0B
27aa721c6f6b        21 minutes ago      /bin/sh -c #(nop) WORKDIR /root                 0B
a9d968c704f7        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
.... ...
<missing>           2 months ago        /bin/sh -c #(nop) ADD file:8f997234193c2f5...   188MB

我们去除掉那些Size为0或很小的layer,我们看到三个size占比较大的layer,见下图:

img{512x368}
图2:Docker镜像分层探索

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

三、”理性的回归”:builder模式的崛起

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

img{512x368}
图3:去除不必要的分层

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

  1. 在本地构建并COPY到镜像中;
  2. 借助构建者镜像(builder image)构建。

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

img{512x368}
图4:借助builder image进行镜像构建的流程图

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

  1. 第一阶段:构建负责编译源码的构建者镜像;
  2. 第二阶段:将第一阶段的输出作为输入,构建出最终的目标镜像。

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

// Dockerfile.build

FROM golang:1.9.2

WORKDIR /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.target
From ubuntu:14.04

COPY ./httpd /root/httpd
RUN chmod +x /root/httpd

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

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

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

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

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

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

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

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

img{512x368}
图5:目标镜像还能更小些吗?

一般应用开发者不会从scratch镜像从头构建自己的base image以及目标镜像的,开发者会挑选适合的base image。一些“蝇量级”甚至是“草量级”的官方base image的出现为这种情况提供了条件。

img{512x368}
图6:一些base image的Size比较(来自imagelayers.io截图)

从图中看,我们有两个选择:busyboxalpine

单从image的size上来说,busybox更小。不过busybox默认的libc实现是uClibc,而我们通常运行环境使用的libc实现都是glibc,因此我们要么选择静态编译程序,要么使用busybox:glibc镜像作为base image。

而 alpine image 是另外一种蝇量级 base image,它使用了比 glibc 更小更安全的 musl libc 库。 不过和 busybox image 相比,alpine image 体积还是略大。除了因为 musl比uClibc 大一些之外,alpine还在镜像中添加了自己的包管理系统apk,开发者可以使用apk在基于alpine的镜像中添 加需要的包或工具。因此,对于普通开发者而言,alpine image显然是更佳的选择。不过alpine使用的libc实现为musl,与基于glibc上编译出来的应用程序不兼容。如果直接将前面构建出的httpd应用塞入alpine,在容器启动时会遇到下面错误,因为加载器找不到glibc这个动态共享库文件:

standard_init_linux.go:185: exec user process caused "no such file or directory"

对于Go应用来说,我们可以采用静态编译的程序,但一旦采用静态编译,也就意味着我们将失去一些libc提供的原生能力,比如:在linux上,你无法使用系统提供的DNS解析能力,只能使用Go自实现的DNS解析器。

我们还可以采用基于alpine的builder image,golang base image就提供了alpine 版本。 我们就用这种方式构建出一个基于alpine base image的极小目标镜像。

img{512x368}
图7:借助 alpine builder image 进行镜像构建的流程图

我们新建两个用于 alpine 版本目标镜像构建的 Dockerfile:Dockerfile.build.alpine 和Dockerfile.target.alpine:

//Dockerfile.build.alpine
FROM golang:alpine

WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go

// Dockerfile.target.alpine
From alpine

COPY ./httpd /root/httpd
RUN chmod +x /root/httpd

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

构建builder镜像:

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

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED              SIZE
repodemo/httpd-alpine-builder    latest              d5b5f8813d77        About a minute ago   275MB

执行“胶水”命令:

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

构建目标镜像:

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

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd-alpine            latest              895de7f785dd        13 seconds ago      16.2MB

16.2MB!目标镜像的Size降为不到原来的十分之一。我们得到了预期的结果。

五、“要有光,于是便有了光”:对多阶段构建的支持

至此,虽然我们实现了目标Image的最小化,但是整个构建过程却是十分繁琐,我们需要准备两个Dockerfile、需要准备“胶水”命令、需要清理中间产物等。作为Docker用户,我们希望用一个Dockerfile就能解决所有问题,于是就有了Docker引擎对多阶段构建(multi-stage build)的支持。注意:这个特性非常新,只有Docker 17.05.0-ce及以后的版本才能支持。

现在我们就按照“多阶段构建”的语法将上面的Dockerfile.build.alpine和Dockerfile.target.alpine合并到一个Dockerfile中:

//Dockerfile

FROM golang:alpine as builder

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o httpd ./httpserver.go

From alpine:latest

WORKDIR /root/
COPY --from=builder /go/src/httpd .
RUN chmod +x /root/httpd

ENTRYPOINT ["/root/httpd"]

Dockerfile的语法还是很简明和易理解的。即使是你第一次看到这个语法也能大致猜出六成含义。与之前Dockefile最大的不同在于在支持多阶段构建的Dockerfile中我们可以写多个“From baseimage”的语句了,每个From语句开启一个构建阶段,并且可以通过“as”语法为此阶段构建命名(比如这里的builder)。我们还可以通过COPY命令在两个阶段构建产物之间传递数据,比如这里传递的httpd应用,这个工作之前我们是使用“胶水”代码完成的。

构建目标镜像:

# docker build -t repodemo/httpd-multi-stage .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd-multi-stage       latest              35e494aa5c6f        2 minutes ago       16.2MB

我们看到通过多阶段构建特性构建的Docker Image与我们之前通过builder模式构建的镜像在效果上是等价的。

六、来到现实

沿着时间的轨迹,Docker 镜像构建走到了今天。追求又快又小的镜像已成为了 Docker 社区 的共识。社区在自创 builder 镜像构建的最佳实践后终于迎来了多阶段构建这柄利器,从此构建 出极简的镜像将不再困难。


微博:@tonybai_cn
微信公众号:iamtonybai
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

Go语言:成长的十年

Go语言之父,Google大神Rob Pike代表Go语言的另外两位缔造者Robert GriesemerKen Thompson在自己的博客上发表了一篇名为《Go: Ten years and climbing》的文章,用以纪念Go语言从最初的设计idea起到目前的十年发展。笔者读完后,也是深有感触,因此在这里粗略翻译一下全文,希望能有更多的程序员加入到Gopher行列中来。

译文全文如下:

img{512x368}
Drawing Copyright ©2017 Renee French

本周是创建Go语言十周年的纪念日。

记得第一次关于这门语言设计的讨论是在2007年9月20日,一个周四的下午。进而在第二天的下午两点,我、Robert Griesemer以及Ken Thompson在谷歌山景城总部43#楼的一间名为Yaounde的会议室里又组织进行了一场有关这门语言设计的会议。这门语言的名字诞生于9月25日,在第一封有关语言设计的mail中可以看到一些关于命名的设计考量:

    Subject: Re: prog lang discussion
    From: Rob 'Commander' Pike
    Date: Tue, Sep 25, 2007 at 3:12 PM
    To: Robert Griesemer, Ken Thompson

    i had a couple of thoughts on the drive home.

    1. name

    'go'. you can invent reasons for this name but it has nice properties.
    it's short, easy to type. tools: goc, gol, goa. if there's an interactive
    debugger/interpreter it could just be called 'go'. the suffix is .go
    ...

(将语言命名为Go这事儿值得一提;“golang”来自于这门语言的web站点地址(因为go.com当时已经是迪斯尼的一个web站点了),但却不是语言的恰当名字。)

Go项目将2009年11月10日,即Go项目正式开源的那天作为其官方生日。最初Go项目托管在code.google.com上,几年后迁移至GitHub。不过,现在我们要回到最初的语言概念构建阶段,即那之前的两年,这可以让我们做更进一步地回顾,以更久远的视角,见证一些语言早期的历史事件。

Go开发过程中的第一个惊喜是收到下面这封mail信息:

    Subject: A gcc frontend for Go
    From: Ian Lance Taylor
    Date: Sat, Jun 7, 2008 at 7:06 PM
    To: Robert Griesemer, Rob Pike, Ken Thompson

    One of my office-mates pointed me at http://.../go_lang.html .  It
    seems like an interesting language, and I threw together a gcc
    frontend for it.  It's missing a lot of features, of course, but it
    does compile the prime sieve code on the web page.

Ian Lance Taylor的加入以及第二个编译器实现(gccgo)在带来震惊的同时,也伴随着喜悦。这对Go项目来说不仅仅是鼓励,更是一种对可行性的证明。有了语言的第二个实现对确定语言规范和标准库的过程是至关重要的,同时也有助于Go保证其高可移植性的承诺

虽然Ian的办公室离我们不远,但在看到这封mail之前我们从未谋面。不过,从那之后,Ian Lance Taylor便成为了Go语言及工具设计和实现的核心人物。

Russ Cox也是在2008年加入到刚成立不久的Go语言开发团队的。随着他的加入,他的一些天赋也随即在语言设计和实现中展现出来。Russ发现Go method的通用性意味着一个函数也可以拥有自己的方法,这直接导致了http.HandlerFunc的出现,这是一个我们所有人都未曾想到的结果。Russ还在当时设计的基础上提出了一些更泛化的想法,比如io.Readerio.Writer接口,奠定了所有I/O库的整体结构。

Jini Kim是我们最初的产品经理,他招来了安全专家Adam Langley来帮助我们将Go推向Google外面的世界。Adam为我们做了许多不为外人所知的事情,包括创建最初golang.org站点的web页面以及build dashboard。不过他最大的贡献当然要属cryptographic库了。起先,对于我们中的一部分人来说,这个库无论是规模还是复杂度,和其他库比起来都不成比例。但是就是这个库在后期成为了很多重要的网络和安全软件的基础,并且成为了Go语言开发历史的关键组成部分。像Cloudflare这样的网络基础设施提供商就重度依赖Adam在Go项目中的工作,Internet也因此变得更好。因此,我们由衷感谢他的工作。

事实上,许多公司在早期使用Go进行开发,尤其是初创公司。其中一些公司成为了云计算的巨头,其中就有一家这样的公司,它现在叫Docker。这家公司使用Go语言,并催化出计算领域的容器行业,进而导致了像Kubernetes这样的项目出现。今天我们可以说Go是容器语言,这是另一个我们完全没有预料到的结果。

不过,Go语言在云计算领域起到作用更大。2015年3月,Donnie Berkholz在为RedMonk撰写的一篇文章中宣称:Go是“云计算基础设施新兴语言”。几乎与此同时,Apcera的Derek Collison说:Go已经是云计算语言了。在那个时候,这也许还不是事实。但Berkholz所使用的“新兴”一词却恰如其分的表明了Go在当时的地位。

今天,Go已经成为云计算语言。想象一下:一个只有10岁的年轻编程语言已经成为这样一个规模庞大且不断发展的行业的主导者,这样的成功以前只是存在于在想象中。如果你觉得“主导”这个词太过强势的话,让我们来看看中国互联网行业。一段时间以来,Go在中国地区大量使用的数据一度让我们误认为Google趋势图出现了某些错误,但是凡是去过中国,参加过中国区Go语言大会的人都可以证实:Google趋势图的数据是真的,Go在中国的使用非常火爆!

简而言之,Go语言的十年发展为我们带来了许多里程碑。 最令人惊讶的是我们现在的位置:保守估计表明至少有50万Go程序员。 当前面那封为Go命名的邮件发送时,憧憬能有有五十万gopher的想法听起来会感觉很荒唐。 但就在此时此刻这里,我们不仅有了50w gopher,并且数量还在持续增长。

说到gophers,很高兴看到来自Renee French想法的吉祥物Go Gopher(地鼠),不仅成为了一个非常受人喜爱的作品,而且也是世界各地Go程序员的象征。许多各个地区顶级的Go大会都被称为GopherCons,因为他们聚集了来自世界各地的gophers。

Gopher大会正在迅速发展。第一次大会的举办只不过是三年前的事情,但今天在全世界各地有很多这样的Go大会。并且还有无数小的本地“聚会(meetups)”。在任何某一天,世界上某个地方都会有不止一个gopher群体在进行有关Go的分享。

回顾过去十年的Go设计和开发,Go社区的发展是惊人的。会议和聚会的数量、长长的且不断增加的Go项目贡献者名单、大量用Go实现的开放源代码存储库、使用Go的公司数量等等,细思恐(吃惊)极!

对于我们三个人,Robert, Rob和Ken,当初只是想让我们的编程生活更轻松一些,而如今,我们难以置信地、欣慰地看到我们的工作已经开始起作用了。

未来十年会带来什么呢?

- Rob Pike, with Robert Griesemer and Ken Thompson


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats