设计背景:
由于项目所限定的运行平台生态所限,没有良好的容器镜像支持无父镜像可以依赖(不像X86、ARM平台),且项目对容器镜像的启动时间和镜像大小的要求,所以需要从零开始构建镜像。
docker的镜像结构如下所示,是通过分层来叠加构建的。
首先我们先编写一个构建image的Dockerfile,下面的Dockerfile是一个基于ubuntu构建出的拥有python flask框架环境的镜像,镜像的构建速度跟网络环境有关,这里我在构建过程中替换了软件源以加快构建速度。
FROM ubuntu:16.04
COPY sources.list sources.list
RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak \
&& mv sources.list /etc/apt/
RUN apt-get update && apt-get install -y --no-install-recommends \
python2.7 \
python-pip \
curl && \
pip install flask && \
rm -rf /var/lib/apt/lists/*
sources.list软件源如下:
deb http://mirrors.ustc.edu.cn/ubuntu/ xenial main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu/ xenial-security main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu/ xenial-updates main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu/ xenial-proposed main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu/ xenial-backports main restricted universe multiverse
deb-src http://mirrors.ustc.edu.cn/ubuntu/ xenial main restricted universe multiverse
deb-src http://mirrors.ustc.edu.cn/ubuntu/ xenial-security main restricted universe multiverse
deb-src http://mirrors.ustc.edu.cn/ubuntu/ xenial-updates main restricted universe multiverse
deb-src http://mirrors.ustc.edu.cn/ubuntu/ xenial-proposed main restricted universe multiverse
deb-src http://mirrors.ustc.edu.cn/ubuntu/ xenial-backports main restricted universe multiverse
通过docker命令构建镜像,命令如下所示,构建出来的镜像名为plserver_ubuntu。
docker build -f Dockerfile -t plserver_ubuntu .
我们来看看这个构建后的镜像的大小,大小为169MB,会发现要实现一个功能很简单的镜像都需要很大的size。
# docker images | grep plserver_ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
plserver_ubuntu latest 3da5e5f86c81 2 days ago 169MB
我们知道这个镜像里面有很多我们使用过程中用不到的东西,而这些东西都构建在了镜像里,占据了很大的空间。有没有什么办法可以压缩镜像大小呢?一个Dockerfile最终生产出的一个镜像。Dockerfile由若干个Command组成,每个Command执行结果都会单独形成一个layer。
接下来我们分析一下构建出来的镜像:
# docker history 3da5e5f86c81
IMAGE CREATED CREATED BY SIZE COMMENT
3da5e5f86c81 2 days ago /bin/sh -c apt-get update && apt-get install… 46.6MB
11045feb4abc 2 days ago /bin/sh -c mv /etc/apt/sources.list /etc/apt… 3.68kB
5896bd36d281 2 days ago /bin/sh -c #(nop) COPY file:0261bb62aae7979e… 913B
b9409899fe86 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
2 weeks ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
2 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 745B
2 weeks ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B
2 weeks ago /bin/sh -c #(nop) ADD file:6d0a70c6da1ad3872… 122MB
去掉那些Size为0的layer,我们看到还有很多size占比较大的layer,其中最大的占用为base image,即为ubuntu:16.04
# docker images | grep ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 16.04 b9409899fe86 2 weeks ago 122MB
镜像分层构成见下图:
同样对比最精简的操作系统Alpine Linux(仅包含musl libc 和 busybox ,还有包管理工具apk),它的大小虽然只有5MB,但是里面还有些东西可能是我们用不到的,而且这些操作系统都有一个局限,依赖于底层指令集架构。
一般应用开发者不会从scratch镜像从头构建自己的base image以及目标镜像的,开发者会挑选适合的base image。一些“蝇量级”甚至是“草量级”的官方base image的出现为这种情况提供了条件。
对于Go应用来说,我们可以采用静态编译的程序,但一旦采用静态编译,也就意味着我们将失去一些libc提供的原生能力,比如:在linux上,你无法使用系统提供的DNS解析能力,只能使用Go自实现的DNS解析器。
我们还可以采用基于alpine的builder image,golang base image就提供了alpine 版本。 我们就用这种方式构建出一个基于alpine base 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降为不到原来的十分之一。
由于上面的整个构建过程十分繁琐,我们需要准备两个Dockerfile、需要准备“胶水”命令、需要清理中间产物等。但幸运的是,从Docker 17.05.0-ce以后就有了Docker引擎对多阶段构建(multi-stage build)的支持。
于是,我们就按照“多阶段构建”的语法将上面的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"]
与之前Dockefile最大的不同在于在支持多阶段构建的Dockerfile中我们可以写多个“From baseimage”的语句了,每个From语句开启一个构建阶段,并且可以通过“as”语法为此阶段构建命名(比如这里的builder)。我们还可以通过COPY命令在两个阶段构建产物之间传递数据,比如这里传递的httpd应用,这个工作之前我们是使用“胶水”代码完成的。
我们知道,构建镜像的过程一般都是从父镜像开始的,但父镜像依赖于指令集架构。所以有没有可能我们跨越平台架构来构建精简的镜像,那就需要用到scratch了。
为镜像进一步减重,减到尽可能的小,把所有不必要的东西都拆掉:仅仅保留能支撑我们应用运行的必要库,命令,其余的一律不纳入目标镜像。当然不仅仅是Size上的原因,小镜像还有额外的优势,比如:内存占用小,启动速度快,更加高效;因为它不包含额外不必要的工具,所以它更安全,不会因为其他不必要的工具、库的漏洞而被攻击,减少了“攻击面”。
scratch镜像很小,因为它基本上是空的,除了有点被docker添加的metadata(元数据:描述数据的数据)。可以用以下命令构建这个scratch镜像(前官方描述)(scratch镜像不可以直接从docker官方拉取下来):
# tar cv -f /dev/null | docker import - scratch
tar: Cowardly refusing to create an empty archive
Try `tar --help' or `tar --usage' for more information.
sha256:b9ce8716e13f8daf717b5c1ef7fe5e99685008c26074d0b0143e98c56a2e8875
# docker images | grep scratch
REPOSITORY TAG IMAGE ID CREATED SIZE
scratch latest b9ce8716e13f 3 minutes ago 0B
官方对scratch的描述如下:an explicitly empty image, especially for building images “FROM scratch”
scratch构建镜像精简到可以只在scratch上面加一层二进制可执行文件。
那我们用scratch构建镜像需要注意什么呢?
在构建二进制可执行文件的时候,需要进行静态编译链接,因为scratch中没有我们需要的动态链接库
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o empty –a -ldflags '-s' emptyImageProject/
CGO_ENABLED环境变量表示使用cgo编译器,而不是go编译器。-a参数表示要重建所有的依赖。否则,还是以动态链接依赖为结果。-ldflags '-s' 一个不错的额外标志,它可以缩减生成的可执行文件约50%的大小,GOARCH可以指定你要生成的二进制文件是基于哪个指令集架构平台的,这是go语言特有的交叉编译的特性。
我们可以查看go语言支持的指令集架构有哪些:
# go tool dist list
android/386
android/amd64
android/arm
android/arm64
darwin/386
darwin/amd64
darwin/arm
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
nacl/386
nacl/amd64p32
nacl/arm
netbsd/386
netbsd/amd64
netbsd/arm
openbsd/386
openbsd/amd64
openbsd/arm
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
最后编译好二进制文件后,静态编译链接后的二进制文件为empty。根据Dockerfile构建镜像,基于scratch构建镜像的Dockerfile如下:
FROM scratch
ADD empty /
CMD ["/empty"]
在此附上二进制源代码地址:https://github.com/wangzy0327/emptyImageProject
到此,从零构建镜像就介绍完毕,如有当之处,还请大家批评指正。