七步制作精簡鏡像,So easy
文章來自黃慶兵老師的網(wǎng)易蜂巢《玩轉(zhuǎn) Docker 鏡像》系列https://github.com/bingohuang/play-docker-images,原本分為上下兩篇,這里由生態(tài)君整合成一篇便于閱讀。希望能對大家有所幫助。
目錄
介紹
鏡像層(Layers)
制作步驟
lab-1:初始化構(gòu)建 Redis 鏡像
lab-2:優(yōu)化基礎(chǔ)鏡像
lab-3:串聯(lián) Dockerfile 指令
lab-4:壓縮你的鏡像
lab-5:使用最精簡的 base image
lab-6:提取動態(tài)鏈接的 .so 文件
lab-7:為 Go 應(yīng)用構(gòu)建精簡鏡像
總結(jié)
參考
作者簡介: 黃慶兵,畢業(yè)于浙大,工作于網(wǎng)易,從事云計算、Docker和Go相關(guān)開發(fā)及布道工作;喜歡開源,樂于分享,勤于布道,折騰過開源小工具,制作過Docker課程,分享過 Gopher Meetup。我的 Github 賬號:https://github.com/bingohuang,歡迎一起來 寫 Go 玩 Docker!

介紹
前段時間網(wǎng)易蜂巢曾經(jīng)推出蜂巢 Logo T恤,用的正是 Docker 鏡像制作,最神奇的是,它最終的鏡像大小只有 585 字節(jié)。
$?docker?images?|?grep?hub.c.163.com/public/logo
REPOSITORY??????????????????????TAG???????????IMAGE?ID?????????CREATED???????SIZE
hub.c.163.com/public/logo???????latest????????6fbdd13cd204?????11?days?ago???585?B
有些鏡像都不是我們自己來打包的(比如下載公共鏡像),那是否有一些通用的精簡 Docker 鏡像的手段呢?答案是肯定的,甚至有的鏡像可以精簡 98%。精簡鏡像大小的好處不言而喻,既節(jié)省了存儲空間,又能節(jié)省帶寬,加快傳輸?shù)?。那好,接下來就請跟隨我來學(xué)習(xí)怎么制作精簡 Docker 鏡像。
鏡像層(Layers)
在開始制作鏡像之前,首先了解下鏡像的原理,而這其中最重要的概念就是鏡像層(Layers)。鏡像層依賴于一系列的底層技術(shù),比如文件系統(tǒng)(filesystems)、寫時復(fù)制(copy-on-write)、聯(lián)合掛載(union mounts)等,幸運的是你可以在很多地方學(xué)習(xí)到這些技術(shù)[1],這里就不再贅述技術(shù)細(xì)節(jié)。

總的來說,你最需要記住這點:
在 Dockerfile 中,?每一條指令都會創(chuàng)建一個鏡像層,繼而會增加整體鏡像的大小。
舉例來說:
FROM?busybox
RUN?mkdir?/tmp/foo
RUN?dd?if=/dev/zero?of=/tmp/foo/bar?bs=1048576?count=100
RUN?rm?/tmp/foo/bar
以上 Dockerfile 干了幾件事:
基于一個官方的基礎(chǔ)鏡像 busybox(只有1M多) 創(chuàng)建一個文件夾(/tmp/foo)和一個文件(bar),該文件分配了100M大小 再把這個大文件刪除
實際上它最終什么也沒做,我們把它構(gòu)建成鏡像(構(gòu)建可以參考一期[2]):
docker?build?-t?busybox:test?.
再讓我們來對比下原生的 busybox 鏡像大小和我們生成的鏡像大?。?/p>
$?docker?images?|?grep?busybox
busybox????test?????896c63dbdb96????2?seconds?ago????106?MB
busybox????latest???2b8fd9751c4c????9?weeks?ago??????1.093?MB
出乎意料的是,卻生成了 106 MB 的鏡像。
多出了 100 M,這是為何?這點和 Git 類似(都用到了Copy-On-Write技術(shù)),我用 git 做了如下兩次提交(添加了又刪除),請問 A_VERY_LARGE_FILE 還在 git 倉庫中嗎?
$?git?add??A_VERY_LARGE_FILE
$?git?commit
$?git?rm??A_VERY_LARGE_FILE
$?git?commit
答案是:在的,并且會占用倉庫的大小。Git 會保存每一次提交的文件版本,而 Dockerfile 中每一條指令都可能增加整體鏡像的大小,即使它最終什么事情都沒做。
制作步驟
了解了鏡像層知識,有助于我們接下來制作精簡鏡像。這里開始,以最常用的開源緩存軟件 Redis 為例,從一步步試驗,來介紹如何制作更精簡的 Docker 鏡像。
lab-1:初始化構(gòu)建 Redis 鏡像
直接上 Dockerfile :
FROM?ubuntu:trusty
ENV?VER?????3.0.0
ENV?TARBALL?http://download.redis.io/releases/redis-$VER.tar.gz
#?==>?Install?curl?and?helper?tools...
RUN?apt-get?update
RUN?apt-get?install?-y??curl?make?gcc
#?==>?Download,?compile,?and?install...
RUN?curl?-L?$TARBALL?|?tar?zxv
WORKDIR??redis-$VER
RUN?make
RUN?make?install
#...
#?==>?Clean?up...
WORKDIR?/
RUN?apt-get?remove?-y?--auto-remove?curl?make?gcc
RUN?apt-get?clean
RUN?rm?-rf?/var/lib/apt/lists/*??/redis-$VER
#...
CMD?["redis-server"]
結(jié)合注釋,讀起來并不困難,用到的都是常規(guī)的幾個命令,簡要介紹如下:
FROM:頂頭寫,指定一個基礎(chǔ)鏡像,此處基于 ubuntu:trustyENV:設(shè)置環(huán)境變量,這里設(shè)置了 VER和TARBALL兩個環(huán)境變量RUN:最常用的 Dockerfile 指令,用于運行各種命令,這里調(diào)用了 8 次 RUN 指令 WORKDIR:指定工作目錄,相當(dāng)于指令 cdCMD:指定鏡像默認(rèn)執(zhí)行的命令,此處默認(rèn)執(zhí)行 redis-server 命令來啟動 redis
執(zhí)行構(gòu)建:
$?docker?build??-t?redis:lab-1??.
注:國內(nèi)網(wǎng)絡(luò),更新下載可能會較慢
查看大?。?/strong>
| Lab | iamge | Base | Lang | .red[*] | Size (MB) | ?? Memo |
|---|---|---|---|---|---|---|
| 1 | redis | ubuntu | C | dyn | 347.3 | ?? base ubuntu |
動輒就有 300多 M 的大小,不能忍,下面我們開始一步步優(yōu)化。
lab-2:優(yōu)化基礎(chǔ)鏡像
精簡1:選用更小的基礎(chǔ)鏡像。
常用的 Linux 系統(tǒng)鏡像一般有 ubuntu、centos、debian,其中debian 更輕量,而且夠用,對比如下:
REPOSITORY??????????TAG????????IMAGE?ID?????????VIRTUAL?SIZE
---------------?????------?????------------?????------------
centos??????????????7??????????214a4932132a?????215.7?MB
centos??????????????6??????????f6808a3e4d9e?????202.6?MB
ubuntu??????????????trusty?????d0955f21bf24?????188.3?MB
ubuntu??????????????precise????9c5e4be642b7?????131.9?MB
debian??????????????jessie?????65688f7c61c4?????122.8?MB
debian??????????????wheezy?????1265e16d0c28?????84.96?MB
替換 debian:jessie 作為我們的基礎(chǔ)鏡像。
優(yōu)化 Dockerfile:
FROM?debian:jessie
#...
執(zhí)行構(gòu)建:
$?docker?build??-t?redis:lab-2??.
查看大?。?/strong>
| Lab | image | Base | Lang | .red[*] | Size (MB) | ?? Memo |
|---|---|---|---|---|---|---|
| 01 | redis | ubuntu | C | dyn | 347.3 | ?? base ubuntu |
| 02 | redis | debian | C | dyn | 305.7 | ?? base debian |
減少了42M,稍有成效,但并不明顯。細(xì)心的同學(xué)應(yīng)該發(fā)現(xiàn),只有 122 MB 的 debian 基礎(chǔ)鏡像,構(gòu)建后增加到了 305 MB,看來這里面肯定有優(yōu)化的空間,如何優(yōu)化就要用到我們開頭說到的 Image Layer 知識了。
lab-3:串聯(lián) Dockerfile 指令
精簡2:串聯(lián)你的 Dockerfile 指令(一般是 RUN 指令)。
Dockerfile 中的 RUN 指令通過 && 和 / 支持將命令串聯(lián)在一起,有時能達(dá)到意想不到的精簡效果。
優(yōu)化 Dockerfile:
FROM?debian:jessie
ENV?VER?????3.0.0
ENV?TARBALL?http://download.redis.io/releases/redis-$VER.tar.gz
RUN?echo?"==>?Install?curl?and?helper?tools..."??&&?\
????apt-get?update??????????????????????&&?\
????apt-get?install?-y??curl?make?gcc???&&?\
????\
????echo?"==>?Download,?compile,?and?install..."??&&?\
????curl?-L?$TARBALL?|?tar?zxv??&&?\
????cd?redis-$VER???????????????&&?\
????make????????????????????????&&?\
????make?install????????????????&&?\
????...
????echo?"==>?Clean?up..."??&&?\
????apt-get?remove?-y?--auto-remove?curl?make?gcc??&&?\
????apt-get?clean??????????????????????????????????&&?\
????rm?-rf?/var/lib/apt/lists/*??/redis-$VER
#...
CMD?["redis-server"]
構(gòu)建:
$?docker?build??-t?redis:lab-3??.
查看大?。?/p>Lab Image Base Lang .red[*] Size (MB) ?? Memo 01 redis ubuntuC dyn 347.3 ?? base ubuntu 02 redis debianC dyn 305.7 ?? base debian 03 redis debianC dyn 151.4 ?? cmd chaining
哇!一下子減少了 50%,效果明顯??!這是最常用的一個精簡手段了。
lab-4:壓縮你的鏡像
優(yōu)化3:試著用命令或工具壓縮你的鏡像。
docker 自帶的一些命令還能協(xié)助壓縮鏡像,比如 export 和 import
$?docker?run?-d?redis:lab-3
$?docker?export?71b1c0ad0a2b?|?docker?import?-?redis:lab-4
但麻煩的是需要先將容器運行起來,而且這個過程中你會丟失鏡像原有的一些信息,比如:導(dǎo)出端口,環(huán)境變量,默認(rèn)指令。
所以一般通過命令行來精簡鏡像都是實驗性的,那么這里再推薦一個小工具:docker-squash[3]。用起來更簡單方便,并且不會丟失原有鏡像的自帶信息。
下載安裝:
https://github.com/jwilder/docker-squash#installation
壓縮操作:
$?docker?save?redis:lab-3?\
??|?sudo?docker-squash?-verbose?-t?redis:lab-4??\
??|?docker?load
注:該工具在 Mac 下并不好使,請在 Linux 下使用
對比大小:
| Lab | Image | Base | PL | .red[*] | Size (MB) | ?? Memo |
|---|---|---|---|---|---|---|
| 01 | redis | ubuntu | C | dyn | 347.3 | ?? base ubuntu |
| 02 | redis | debian | C | dyn | 305.7 | ?? base debian |
| 03 | redis | debian | C | dyn | 151.4 | ?? cmd chaining |
| 04 | redis | debian | C | dyn | 151.4 | ?? docker-squash |
好吧,從這里看起來并沒有太大作用,所以我只能說試著,而不要報太大期望。
lab-5:使用最精簡的 base image

使用 scratch 或者 busybox 作為基礎(chǔ)鏡像。
關(guān)于 scratch:
一個空鏡像,只能用于構(gòu)建鏡像,通過 FROM scratch在構(gòu)建一些基礎(chǔ)鏡像,比如 debian、busybox,非常有用用于構(gòu)建超少鏡像,比如構(gòu)建一個包含所有庫的二進制文件
關(guān)于 busybox
只有 1~5M 的大小 包含了常用的 UNIX 工具 非常方便構(gòu)建小鏡像
這些超小的基礎(chǔ)鏡像,結(jié)合能生成靜態(tài)原生 ELF 文件的編譯語言,比如C/C++,比如 Go,特別方便構(gòu)建超小的鏡像。
cloudcomb-logo(C語言開發(fā)) 就是用到了該原理,才能構(gòu)建出 585 字節(jié)的鏡像。
redis 同樣使用 C語言 開發(fā),看來也有很大的優(yōu)化空間,下面這個實驗,讓我們介紹具體的操作方法。
lab-6:提取動態(tài)鏈接的 .so 文件
實驗上下文:
$?cat?/etc/os-release
NAME="Ubuntu"
VERSION="14.04.2?LTS,?Trusty?Tahr"
$?uname?-a
Linux?localhost?3.13.0-46-generic?#77-Ubuntu?SMP
Mon?Mar?2?18:23:39?UTC?2015
x86_64?x86_64?x86_64?GNU/Linux
隆重推出 ldd:打印共享的依賴庫
$?ldd??redis-3.0.0/src/redis-server
????linux-vdso.so.1?=>??(0x00007fffde365000)
????libm.so.6?=>?/lib/x86_64-linux-gnu/libm.so.6?(0x00007f307d5aa000)
????libpthread.so.0?=>?/lib/x86_64-linux-gnu/libpthread.so.0?(0x00007f307d38c000)
????libc.so.6?=>?/lib/x86_64-linux-gnu/libc.so.6?(0x00007f307cfc6000)
????/lib64/ld-linux-x86-64.so.2?(0x00007f307d8b9000)
將所有需要的 .so 文件打包:
$?tar?ztvf?rootfs.tar.gz
4485167??2015-04-21?22:54??usr/local/bin/redis-server
1071552??2015-02-25?16:56??lib/x86_64-linux-gnu/libm.so.6
?141574??2015-02-25?16:56??lib/x86_64-linux-gnu/libpthread.so.0
1840928??2015-02-25?16:56??lib/x86_64-linux-gnu/libc.so.6
?149120??2015-02-25?16:56??lib64/ld-linux-x86-64.so.2
再制作成 Dockerfile:
FROM?scratch
ADD??rootfs.tar.gz??/
COPY?redis.conf?????/etc/redis/redis.conf
EXPOSE?6379
CMD?["redis-server"]
執(zhí)行構(gòu)建:
$?docker?build??-t?redis-05??.
查看大?。?/p>Lab Base PL .red[*] Size (MB) ?? Memo 01 redis ubuntuC dyn 347.3 ?? base ubuntu 02 redis debianC dyn 305.7 ?? base debian 03 redis debianC dyn 151.4 ?? cmd chaining 04 redis debianC dyn 151.4 ?? docker-squash 05 redis scratchC dyn 7.73 ?? rootfs: .so
哇!顯著提高啦!
測試一下:
$?docker?run?-d?--name?redis-05?redis-05
$?redis-cli??-h??\
??$(docker?inspect?-f?'{{.NetworkSettings.IPAddress}}'?redis-05)
$?redis-benchmark??-h??\
??$(docker?inspect?-f?'{{.NetworkSettings.IPAddress}}'?redis-05)
總結(jié)一下:
用 ldd查出所需的 .so 文件將所有依賴壓縮成 rootfs.tar或rootfs.tar.gz,之后打進scratch基礎(chǔ)鏡像
lab-7:為 Go 應(yīng)用構(gòu)建精簡鏡像
Go 語言天生就方便用來構(gòu)建精簡鏡像,得益于它能方便的打包成包含靜態(tài)鏈接的二進制文件。
打個比方,你有一個 4 MB 大小的包含靜態(tài)鏈接的 Go 二進制,并且將其打進 scratch 這樣的基礎(chǔ)鏡像,你得到的鏡像大小也只有區(qū)區(qū)的 4 MB。這可是包含同樣功能的 Ruby 程序的百分之一啊。
這里再給大家介紹一個非常好用開源的 Go 編譯工具:golang-builder,并給大家實際演示一個例子
程序代碼:
package?main?//?import?"github.com/CenturyLinkLabs/hello"
import?"fmt"
func?main()?{
????fmt.Println("Hello?World")
}
Dockerfile:
FROM?scratch
COPY?hello?/
ENTRYPOINT?["/hello"]
通過 golang-builder 打包成鏡像:
docker?run?--rm?\
????-v?$(pwd):/src?\
????-v?/var/run/docker.sock:/var/run/docker.sock?\
????centurylink/golang-builder
查看鏡像大小(Mac下測試):
$?docker?images
REPOSITORY???TAG??????IMAGE?ID???????CREATED??????????VIRTUAL?SIZE
hello????????latest???1a42948d3224???24?seconds?ago???1.59?MB
哇!這么省力,就能創(chuàng)建幾 M 大小的鏡像,Go 簡介就是為 Docker 鏡像量身定做的!
總結(jié)
我們介紹了鏡像層的知識,并且通過實驗,介紹三種如何精簡鏡像的技巧。這里主要介紹了三種精簡方法:選用更精小的鏡像,串聯(lián) Dockerfile 運行指令,以及試著壓縮你的鏡像。通過這幾個技巧,已經(jīng)可以將 300M 大小的鏡像壓縮到 150M,壓縮率50%到98%,效果還是不錯。
優(yōu)化基礎(chǔ)鏡像 串接 Dockerfile 命令: 壓縮 Docker images 優(yōu)化程序依賴 選用更合適的開發(fā)語言
文章的最后,插播一個福利
雙十一快到了,阿里云也開始搞活動了,剛好我這邊可以帶大家白Piao?阿里云的服務(wù)器。
說白了就是大家?可以一分錢不花,就可以領(lǐng)到服務(wù)器,規(guī)格是 2c2m(2vcpu 2G memory) 的機器。
昨天在朋友圈發(fā)了下,已經(jīng)有 400 人報名參與了,今天借這篇文章再說一下,有想參加的朋友,可以加我下微信,備注『服務(wù)器』,我統(tǒng)一拉群,帶大家一起薅羊毛。
參考
scratch in Docker Hub[4]
Make FROM scratch a special cased 'no-base' spec[5]
vDSO (virtual dynamic shared object)[6]
Small Docker Images For Go Apps[7] (with golang-builder[8])
Building Docker Images for Static Go Binaries[9]
Dockerfile Best Practices - take 2[10] - by Michael Crosby, 2014-03-09.
Optimizing Docker Images[11] - by Brian DeHamer, 2014-07-28.
Squashing Docker Images[12] - by Jason Wilder, 2014-08-19.
參考資料
這些技術(shù): https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/
[2]一期: https://github.com/bingohuang/play-docker-images/tree/master/stage-01
[3]docker-squash: https://github.com/jwilder/docker-squash
[4]scratch in Docker Hub: https://registry.hub.docker.com/_/scratch/
[5]Make FROM scratch a special cased 'no-base' spec: https://github.com/docker/docker/pull/8827
[6]vDSO (virtual dynamic shared object): http://en.wikipedia.org/wiki/VDSO
[7]Small Docker Images For Go Apps: http://www.centurylinklabs.com/small-docker-images-for-go-apps/
[8]golang-builder: https://github.com/CenturyLinkLabs/golang-builder
[9]Building Docker Images for Static Go Binaries: https://medium.com/@kelseyhightower/optimizing-docker-images-for-static-binaries-b5696e26eb07
[10]Dockerfile Best Practices - take 2: http://crosbymichael.com/dockerfile-best-practices-take-2.html
[11]Optimizing Docker Images: http://www.centurylinklabs.com/optimizing-docker-images/
[12]Squashing Docker Images: http://jasonwilder.com/blog/2014/08/19/squashing-docker-images/
