我追踪了一下为什么Docker镜像的大小没有减小

首先

我之前对Docker镜像大小有一个误解,所以我想写一下关于这个问题。举个例子,考虑使用Go语言编写的程序来访问Redis。

解决这个问题的正确方法似乎是使用从Docker 17.05开始添加的多阶段构建(谢谢AkihiroSuda)。

尝试获取Multi-build的图像历史,情况如下。在第二个FROM之前的部分完全消失了,感觉好像是从某个地方复制了可执行二进制文件过来。

$ docker history goredistest_goclient_msb
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
d66839f3edc3        25 minutes ago      /bin/sh -c #(nop)  CMD ["./gclient"]            0B
bac5f184aa19        25 minutes ago      /bin/sh -c #(nop) COPY file:5a43c9652e61e9...   6.03MB
053cde6e8953        2 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>           2 weeks ago         /bin/sh -c #(nop) ADD file:1e87ff33d1b6765...   3.97MB

本次主题包括了包括多个构建的Dockerfile在内的内容。

题材说明

准备两个容器。一个是运行Redis的容器,另一个是运行访问该容器的程序的容器。首先,docker-compose.yml文件如下所示。

version: "3"
services:
  redis:
    image: redis:latest

  goclient:
    build:
      context: ./gclient
    links:
      - redis

所以,在./gclient目录下放有Go源文件和Dockerfile,分别如下所示。

package main

import "fmt"
import "github.com/go-redis/redis"

func main() {
        var err error

        client := redis.NewClient(&redis.Options{
                Addr:     "redis:6379",
                Password: "", // no password set
                DB:       0,  // use default DB
        })

        pong, err := client.Ping().Result()
        fmt.Println(pong, err)

        err = client.Set("key", "value", 0).Err()
        if err != nil {
                panic(err)
        }

        val, err := client.Get("key").Result()
        if err != nil {
                panic(err)
        }
        fmt.Println("key", val)

        val2, err := client.Get("key2").Result()
        if err == redis.Nil {
                fmt.Println("key2 does not exists")
        } else if err != nil {
                panic(err)
        } else {
                fmt.Println("key2", val2)
        }
        // Output: key value
        // key2 does not exists
}
FROM alpine:3.6
MAINTAINER Kenichi Sato <ksato9700@gmail.com>

RUN apk --update add go git musl-dev

RUN mkdir /gclient
COPY main.go /gclient
WORKDIR /gclient
RUN go get -u github.com/go-redis/redis
RUN go build

RUN apk del go git musl-dev && \
    rm -rf ~/go /var/cache/apk/*

CMD ["./gclient"]

Go的源代码基本上就是go-redis的示例,只是根据docker-compose.yml中的写法将其链接到了redis,将redis的主机名修改为了”redis”。而在Dockerfile中,则采用了以下方式进行处理。

    • 最小構成が4MBくらいしかないAlpine Linuxをベースに。

 

    • プログラムをコンパイルするのに必要なgoとmusl-dev(Alpineで採用しているlibcであるmuslの開発ライブラリ)、そしてredisにアクセスするライブラリ(go-redis)を引っ張ってくるのに必要なgitをインストール。

go getでライブラリをインストールしてからgo buildで本体をコンパイル。
コンパイルに必要だったパッケージをアンインストールしてキャッシュも全消去。
で、コンパイルしたバイナリを実行

试着构建并执行一下,以实际验证。

$ docker-compose build
$ docker-compuse up
Starting goredistest_redis_1 ...
Starting goredistest_redis_1 ... done
Starting goredistest_goclient_1 ...
Starting goredistest_goclient_1 ... done
Attaching to goredistest_redis_1, goredistest_goclient_1
redis_1     | 1:C 16 Nov 12:33:54.184 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis_1     | 1:C 16 Nov 12:33:54.184 # Redis version=4.0.2, bits=64, commit=00000000, modified=0, pid=1, just started
redis_1     | 1:C 16 Nov 12:33:54.184 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
redis_1     | 1:M 16 Nov 12:33:54.186 * Running mode=standalone, port=6379.
goclient_1  | PONG <nil>
goclient_1  | key value
goclient_1  | key2 does not exists

显示为goclient_1的是访问Redis的客户端。但是结果与预期一致。奇怪的是,即使在编译后卸载了Go包,它仍然能够正常运行。看起来Go几乎是通过静态链接来创建可执行文件的。下面进行确认。

$ docker run -it --rm  goredistest_goclient ldd ./gclient
    /lib/ld-musl-x86_64.so.1 (0x7f0db59a2000)
    libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f0db59a2000)

只有musl(libc)是动态链接的。所以可以删除go包和安装在~/go目录下的库也没问题。

イメージのサイズを確認してみよう

不要なものはがっつり消しているのでかなりイメージサイズが小さくなっているだろうなと期待して確認してみる。

$ docker images goredistest_goclient
REPOSITORY             TAG                 IMAGE ID            CREATED             SIZE
goredistest_goclient   latest              4bfef36a6d56        15 minutes ago      313MB

「哇,300MB?」

下意识地,我不由自主地突然发出了奇怪的声音。

当我心里想着是否忘记了什么东西时,我查看了里面的内容。

$ docker run -it --rm goredistest_goclient du / | sort -n | tail
72  /usr/share
72  /var
180 /usr/bin
220 /sbin
280 /usr
292 /etc
812 /bin
2668    /lib
5904    /gclient
10292   /

哎呀,只有10MB。

所以,仔细想一想,Docker引入了层次化的文件系统,在每个构建步骤上一层层地堆叠。尝试使用history命令进行确认。

$ docker history goredistest_goclient
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
4bfef36a6d56        2 hours ago         /bin/sh -c #(nop)  CMD ["./gclient"]            0B
fa3fdb7464df        2 hours ago         /bin/sh -c apk del go git musl-dev &&     ...   18.7kB
8b97eb8df841        47 hours ago        /bin/sh -c go build                             6.03MB
8d91581ab916        47 hours ago        /bin/sh -c go get -u github.com/go-redis/r...   5.09MB
b1f5b21016b5        47 hours ago        /bin/sh -c #(nop) WORKDIR /gclient              0B
3e5acd08fa48        47 hours ago        /bin/sh -c #(nop) COPY file:4d3c94ee002426...   704B
d6f9fc3c5595        47 hours ago        /bin/sh -c mkdir /gclient                       0B
b27657866a42        47 hours ago        /bin/sh -c apk --update add go git musl-dev     298MB
1cb4b532ed14        2 days ago          /bin/sh -c #(nop)  MAINTAINER Kenichi Sato...   0B
053cde6e8953        12 days ago         /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>           12 days ago         /bin/sh -c #(nop) ADD file:1e87ff33d1b6765...   3.97MB

果然。无论是通过apk add安装包的方式,还是容量都会大幅增加。每添加一个层次,都会被覆盖,也就是说只是追加而已。所以,即使在下一个步骤中删除了已经堆叠的层次,镜像的大小也不会减小。

借鉴以上的基础上,尝试重新编写Dockerfile。

FROM alpine:3.6
MAINTAINER Kenichi Sato <ksato9700@gmail.com>
RUN mkdir /gclient
COPY main.go /gclient
WORKDIR /gclient

RUN apk --update add go git musl-dev && \
    go get -u github.com/go-redis/redis && \
    go build && \
    apk del go git musl-dev && \
    rm -rf ~/go /var/cache/apk/*

CMD ["./gclient"]

做的事情基本上是一样的,但是顺序被调换了,并且安装包、编译以及卸载包和清除缓存被在一个运行步骤中完成。

我用这个版本重新构建了一下,看一下大小。

$ docker images goredistest_goclient
REPOSITORY             TAG                 IMAGE ID            CREATED             SIZE
goredistest_goclient   latest              869660660f23        30 minutes ago      10MB

哦,几乎就是和内容一样的大小。现在总算满意了。

最后

话虽如此,如果每次都从安装软件包开始操作,那肯定会很慢。但是,将操作步骤分成层级,有利于缓存和加速构建,这也意味着将步骤合并在一起实际上有利有弊。

也许,在开发时,对于耗时的处理可以分别放在另一个步骤中,以便享受构建加速的好处,当到达实际部署阶段时再汇总处理可能是一个好方法。如果在这个改变过程中发生了错误,那就是得不偿失,所以最好能够自动完成,但是对于关注容器或镜像大小的项目来说,即使需要人工耗费一些时间也是可行的。

然后,也许可以考虑在容器中不需要构建整个构建过程。 将编译本身放在另一个容器中进行,然后只需复制在其他地方构建的二进制文件并运行即可。 尽管配置管理可能会变得更加复杂,但我认为这是可行的。

bannerAds