接触过容器云或者用过容器的用户,想必都遇到容器镜像占用空间很大的问题,遇到此类问题的时候,大部分人可能更加习惯于为容器的镜像仓库增加磁盘空间。当然这种方式无可厚非,毕竟这种方式可以帮助我们快速的解决掉手里的问题。
除了上面扩磁盘的解决方式,其实我们还可以 采用缩减容器镜像的方式 。此种方式不但可以帮助我们节省添加新磁盘的开支,还可减少我们制作镜像和传输镜像的时间。优化的比较好的镜像占用的空间,基本和应用的文件占用的空间相当,基本不会占用太多的额外存储空间。
下文中,我们会具体看看如何快速精简容器云镜像。
接触过 Docker 的同学都知道,Dockerfile 是由一些指令的组成,且 Dockerfile 文件中的每条指令对应着 Linux 操作系统中的一条命令,当我们构建镜像时,Docker 程序会将这些 Dockerfile 指令翻译成 Linux 可执行的命令。
Dockerfile 中每一条指令都会创建一个镜像层,随着指令的执行继而会增加镜像整体的大小。
常用 Dockerfile 指令
Dockerfile 文件有自己的书写格式和支持的命令,常用的 Dockerfile 指令有:
FROM 指定基镜像。
MAINTAINER 设置镜像的作者信息,如作者姓名、邮箱等。
COPY 将文件从本地复制到镜像,拷贝前需要保证本地源文件存在。
ADD 与 COPY 类似,复制文件到镜像。不同的是,如果文件是归档文件(tar, zip, tgz, xz 等),会被自动解压。
ENV 设置环境变量,格式: ENV key=value 或 ENV key value,运行容器后,可直接在容器中使用。
EXPOSE 暴露容器中指定的端口,只是一个声明,主要用户了解应用监听的端口。
VOLUME 挂载卷到容器,需要注意的是,保存镜像时不会保存卷中的数据。
WORKDIR 设置当前工作目录,后续各层的当前目录都被指定。
RUN 在容器中运行指定的命令。
CMD 容器启动时运行的命令。Dockerfile 中可以有多个 CMD 指令,但只有最后一个生效。CMD 可以被 docker run 之后的参数替换。
ENTRYPOINT 设置容器启动时运行的命令。Dockerfile 中可以有多个 ENTRYPOINT 指令,但只有最后一个生效。CMD 或 docker run 之后的参数会被当做参数传递给 ENTRYPOINT,这个是与 CMD 的区别。
精简镜像的好处不言而喻,可以节省存储存储空间,更可以减少镜像传输时间,减少带宽的消耗,加快传输。
容器镜像的基本理论
在开始制作镜像之前,我们先了解一下容器镜像的基本理论知识。
容器镜像中最重要的概念就是 layers,即镜像层。
镜像层依赖于一系列的底层技术,比如文件系统(filesystems)、写时复制(copy-on-write)、联合挂载(union mounts)等技术,这些技术的细节在此我们不再赘述,感兴趣的同学可以直接直接查看 Docker 官方文档(https://docs.docker.com/storage/storagedriver/)进行学习。
总的来说,精简镜像我们最需要记住的一句话是:
“在 Dockerfile 中,每条指令都会创建一个镜像层,继而会增加镜像整体的大小。”
下面我们以一个示例来说明一下:
我们 pull 一个镜像,以 busybox 为例:
[root@work ~]# docker pull hub.c.163.com/library/busybox:latest
Trying to pull repository hub.c.163.com/library/busybox ...
latest: Pulling from hub.c.163.com/library/busybox
aab39f0bc16d: Pull compl`ete
Digest: sha256:662af1d642674367b721645652de96f9c147417c2efb708eee4e9b7212697762
Status: Downloaded newer image for hub.c.163.com/library/busybox:latest
复制代码
# 看下镜像大小
[root@work ~]# docker images | grep busybox
hub.c.163.com/library/busybox latest d20ae45477cb 18 months ago 1.129 MB
复制代码
从上面结果看我们 pull 下来的镜像大小只有 1.129MB。
下面我们编写一个 Dockerfile 文件,文件中我们新建一个目录,目录中新建一个 100MB 的文件,最后我们删掉新建的文件。
Dockerfile 内容如下:
#基镜像
FROM hub.c.163.com/library/busybox:latest
#新建目录
RUN mkdir /tmp/dir1
#新建一个100MB的文件
RUN dd if=/dev/zero of=/tmp/dir1/file1 bs=1M count=100
#删除文件
RUN rm /tmp/dir1/file1
复制代码
从 Dockerfile 内容看,其实我们基本什么都没干。
然后我们用这个 Dockerfile 构建新建的镜像,并查看新镜像的大小:
[root@work ~]# docker build -t busybox:v1 .
Sending build context to Docker daemon 1.307 GB
Step 1 : FROM hub.c.163.com/library/busybox:latest
---> d20ae45477cb
Step 2 : RUN mkdir /tmp/dir1
---> Running in 63fa5f27c779
---> da95ea8ae5ee
Removing intermediate container 63fa5f27c779
Step 3 : RUN dd if=/dev/zero of=/tmp/dir1/file1 bs=1M count=100
---> Running in d3e8bbb4f151
100+0 records in
100+0 records out
104857600 bytes (100.0MB) copied, 0.247500 seconds, 404.0MB/s
---> 42b721238144
Removing intermediate container d3e8bbb4f151
Step 4 : RUN rm -rf /tmp/dir1/file1
---> Running in 6b51b633fb21
---> 04096cc5d718
Removing intermediate container 6b51b633fb21
Successfully built 04096cc5d718
复制代码
# 查看镜像信息
[root@work ~]# docker images | grep busybox
busybox v1 04096cc5d718 58 seconds ago 106 MB
hub.c.163.com/library/busybox latest d20ae45477cb 18 months ago 1.129 MB
复制代码
从上面的结果可以看出,虽然在 Dockerfile 中我们将新建的 100MB 的文件删除了,但新镜像的大小仍大于 100MB。
多出了 100 多 MB,这是为何?其实这点和 git 类似,Docker 镜像和 git 都用到了写时复制技术,git 每次提交时都会保存一个文件的版本,Dockerfile 每行指令都会增加整体镜像的大小,即使我们什么都没做。
如何进行容器镜像精简
下面我们开始说下本文的重点: 镜像精简 。
我们将以最常见的 nosql 数据库 Redis 为例,一步步来介绍如何制作更精简的 Docker 镜像。
首先我们来编写一下构建 Redis 镜像的 Dockerfile 文件,具体内容如下:
FROM hub.c.163.com/library/ubuntu:trusty
#redis 版本
ENV VER 3.0.0
ENV TARBALL http://download.redis.io/releases/redis-$VER.tar.gz
RUN apt-get update
#安装依赖的工具
RUN apt-get install -y curl make gcc
#下载redis源码包并解压
RUN curl -L $TARBALL | tar zxv
#进入解压后的目录
WORKDIR redis-$VER
#编译redis源码
RUN make
#安装redis
RUN make install
WORKDIR /
#清理前面安装的依赖工具
RUN apt-get remove -y --auto-remove curl make gcc
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/* /redis-$VER
#运行redis
CMD ["redis-server"]
复制代码
然后再利用上面的 Dockerfile 构建镜像:
[root@work ~]# docker build -t redis:3.0.0 .
……
……
Removing intermediate container b55656487022
Successfully built 7df9c7899ae3
复制代码
查看构建出的镜像大小:
[root@work ~]# docker images | grep redis
redis 3.0.0 7df9c7899ae3 10 hours ago 359.7 MB
复制代码
从结果看构建出优化前的镜像约为 360MB。
下面我们将开始逐步优化。
1. 选用更小的基镜像
常用的 linux 系统一般有 CentOS、Debian、Ubuntu,三者中 Debian 更轻量,且 Debian 系统镜像中提供的功能一般也是够用的,三个系统镜像尺寸对比如下:
在此我们以上面最小的镜像 debian:wheezy 作为即镜像,重新进行构建:
Dockerfile 内容:
FROM hub.c.163.com/library/debian:wheezy
#redis 版本
ENV VER 3.0.0
ENV TARBALL http://download.redis.io/releases/redis-$VER.tar.gz
RUN apt-get update
#安装依赖的工具
RUN apt-get install -y curl make gcc
#下载redis源码包并解压
RUN curl -L $TARBALL | tar zxv
#进入解压后的目录
WORKDIR redis-$VER
#编译redis源码
RUN make
#安装redis
RUN make install
WORKDIR /
#清理前面安装的依赖工具
RUN apt-get remove -y --auto-remove curl make gcc
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/* /redis-$VER
#运行redis
CMD ["redis-server"]
复制代码
构建新镜像:
[root@work ~]# docker build -t redis:3.0.0-v2 .
……
……
Removing intermediate container 3498689792ce
Successfully built 4faa1aa0936d
复制代码
对比两个镜像大小:
从结果看,更换基镜像后的新镜像减少了 37%,精简效果还算可以,但精简效果并未达到我们的目标。
如果仔细看的话我们会发现,原本只有 85MB 大小的 debian 基镜像,在构建后增加到了 228MB,可见此处还有很大的优化空间。后续的优化就需要用到我们在上文中说到的镜像层相关的知识了。
2. 合并 Dockerfile 中指令
Dockerfile 中指令的合并一般是指 RUN 指令的合并。
我们可以通过 &&符号和/ 将 Dockerfile 中的多个 RUN 指令合并成一条 RUN 指令,此种方式一般精简效果较好。
优化后的 Dockerfile 内容如下:
FROM hub.c.163.com/library/debian:wheezy
#redis 版本
ENV VER 3.0.0
ENV TARBALL http://download.redis.io/releases/redis-$VER.tar.gz
RUN apt-get update && \
apt-get install -y curl make gcc &&\
curl -L $TARBALL | tar zxv && \
cd redis-$VER && \
make && \
make install && \
cd / && \
apt-get remove -y --auto-remove curl make gcc && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /redis-$VER
#运行redis
CMD ["redis-server"]
复制代码
构建新镜像:
[root@work ~]# docker build -t redis:3.0.0-v3 .
……
……
Removing intermediate container 9e5cffcd8bdb
Successfully built dafd91993dfb
复制代码
查看镜像大小:
从结果看镜像大小约缩减 72%,可见合并 Dockerfile 指令的方式精简效果较明显,新镜像只比基镜像增加约 10MB。
合并 Dockerfilec 指令精简镜像这种方式是最常用的精简镜像尺寸的方式。
3. 使用最精简的基镜像
上文中第 1 步中,我们使用的基镜像为 Debian 镜像,约 89MB,但如果我们只是安装 Redis 服务的话不一定非得使用这么大的系统镜像,我们可以借助一些更小的镜像,如 scratch、busybox、alpine 等,这些镜像大小往往小于 5MB,因此我们可以直接以此作为基镜像来构建新的 Redis 镜像。
此处我们以 scratch 作为基镜像构建 Redis。scratch 镜像往往只有 1~5MB 大小。
Scrach 是一个空镜像,只能用于构建镜像。在构建一些基础镜像,如 debian、busybox 时非常有用。Scrach 也常用于构建超小的镜像,如构建一个只包含所有库的二进制文件。
但使用最精简的基镜像,我们还需要做些额外的工作,具体过程见下文。
4. 提取.so 库
了解过 Redis 源码的话大家会知道 Redis 开发语言为 C 语言,会依赖一些.so 库,因此我们需要先准备好编译 Redis 所需的.so 文件。
我们通过前面构建好的 redis:3.0.0-v3 镜像运行容器,然后进入容器中获取下 redis 依赖的.so 文件。
# 后台运行容器:
[root@work ~]# docker run --name redisv3 -d redis:3.0.0-v3
ab361e7fc2e70b5b45fa1545917ee92158bb859e833c3f7fcfb80e43bb69cb0c
复制代码
# 查看容器运行状态
# 进入容器
[root@work ~]# docker exec -ti redisv3 /bin/bash
root@ab361e7fc2e7:/#
复制代码
查看 redis-server 二进制文件位置
root@ab361e7fc2e7:/# which redis-server
/usr/local/bin/redis-server
复制代码
# 查看 redis-server 依赖的.so 文件
root@ab361e7fc2e7:/# ldd /usr/local/bin/redis-server
linux-vdso.so.1 => (0x00007ffedfd01000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f0de7a5e000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0de785a000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f0de763d000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0de72b0000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0de7ce4000)
复制代码
将编译 Redis 需要的所有依赖打包:
root@ab361e7fc2e7:/# mkdir so
root@ab361e7fc2e7:/# cp usr/local/bin/redis-server so/
root@ab361e7fc2e7:/# cp lib/x86_64-linux-gnu/libm.so.6 so/
root@ab361e7fc2e7:/# cp lib/x86_64-linux-gnu/libpthread.so.0 so/
root@ab361e7fc2e7:/# cp lib/x86_64-linux-gnu/libc.so.6 so/
root@ab361e7fc2e7:/# cp lib64/ld-linux-x86-64.so.2 so/
root@ab361e7fc2e7:/# cd so
root@ab361e7fc2e7:/# tar zcvf so.tar.gz ./*
so/
so/redis-server
so/libm.so.6
so/libpthread.so.0
so/libc.so.6
so/ld-linux-x86-64.so.2
复制代码
# 将打包好的文件从容器拷贝出来:
[root@work ~]# docker cp redisv3:/so/so.tar.gz .
复制代码
编写 Dockerfile 文件,具体内容如下:
FROM scratch
# 添加依赖的库文件
ADD so.tar.gz /
# redis 配置文件,需要自己准备一份
COPY redis.conf /etc/redis/redis.conf
# 暴露的端口
EXPOSE 6379
CMD ["redis-server"]
复制代码
构建新镜像:
[root@work ~]# docker build -t redis:3.0.0-v4 .
Sending build context to Docker daemon 1.316 GB
Step 1 : FROM scratch
--->
Step 2 : ADD so.tar.gz /
---> Using cache
---> 82b2b6def214
Step 3 : COPY redis.conf /etc/redis/redis.conf
---> 3f382da261be
Removing intermediate container 60af6a5ab042
Step 4 : EXPOSE 6379
---> Running in 78c541686668
---> 043ed6cf87e0
Removing intermediate container 78c541686668
Step 5 : CMD redis-server
---> Running in 2c8b9fb0547d
---> 75d828ebf3aa
Removing intermediate container 2c8b9fb0547d
Successfully built 75d828ebf3aa
复制代码
对比镜像大小:
从结果我们可以看出,精简效果非常显著,基于 scratch 构建的新镜像大小只有 6.9MB,相比之前的 359MB、228MB、102MB,新镜像空间占用已经很少。
结语
以上即是本文精简 Docker 镜像的整个过程。
除了上面我们介绍的精简方法之外,还有一些常见的精简方式,如使用镜像压缩工具 docker-squash,但此种方式压缩效果并不明显,因此在此我们并未做详细介绍,感兴趣的朋友也可以自己尝试噢。
评论