基于scratch构建轻量快速镜像

设计背景:

由于项目所限定的运行平台生态所限,没有良好的容器镜像支持无父镜像可以依赖(不像X86、ARM平台),且项目对容器镜像的启动时间和镜像大小的要求,所以需要从零开始构建镜像。

docker的镜像结构如下所示,是通过分层来叠加构建的。

基于scratch构建轻量快速镜像_第1张图片

镜像里面到底装了些什么?

首先我们先编写一个构建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

镜像分层构成见下图:

基于scratch构建轻量快速镜像_第2张图片

追求最小镜像

同样对比最精简的操作系统Alpine Linux(仅包含musl libc 和 busybox ,还有包管理工具apk),它的大小虽然只有5MB,但是里面还有些东西可能是我们用不到的,而且这些操作系统都有一个局限,依赖于底层指令集架构。

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

基于scratch构建轻量快速镜像_第3张图片

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

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

分步构建

基于scratch构建轻量快速镜像_第4张图片

我们新建两个用于 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

到此,从零构建镜像就介绍完毕,如有当之处,还请大家批评指正。

你可能感兴趣的:(docker)