Contents

Docker镜像缓存的优化

我们在用容器化技术来开发项目时,若用到dockerfile来构建容器镜像,那么想必经常需要在开发调试时不断重新构建镜像。然而我用docker这么久才发现缓存优化的重要性,实在惭愧——假如我们正确地编写dockerfile,那么build的时间或许可以缩减很多,从而加速了整个项目的开发过程。下面就聊一下我从官网上看到的一些重要的dockerfile优化技巧。

build cache的原理

我们直接看一下官网的例子

1
2
3
4
5
FROM ubuntu:latest
RUN apt-get update && apt-get install -y build-essentials
COPY main.c Makefile /src/
WORKDIR /src/
RUN make build

我们知道,dockerfile内的每一条命令都是镜像的一个layer,docker通过layer来对镜像的构建进行加速与空间节省,每次构建时,docker从上到下将layer进行应用,最后构建出整个镜像。在重复构建时,假如当前layer和上次构建的缓存一致,那么docker将直接使用缓存以节省时间,然而,当某个layer的内容变更,那么它以及它以后的所有layer都需要重新构建:

/posts/docker-layer-cache/images/layers.png

优化1:对命令重新排序

由上面build原理可知,命令排序是有讲究的:一个命令layer的变更,会导致它以及之后的所有layer全部需要重新构建。所以我们在编写dockerfile命令的时候,应该尽量把不容易改变的命令写在前面,而相对易变的命令则放到后面执行。如此发生改变时, docker只需要重新构建后面几个改动了的layer。

优化2:减少无谓的动作

考虑命令的必要性,让命令只执行需要的操作,比如命令包管理工具只安装必要的工具、比如拷贝文件时忽略无用的文件(可通过.dockerignore文件实现)

优化3:合并命令

/posts/docker-layer-cache/images/combine_run.png

多个RUN命令用&&来合并,使它们在一个layer中被执行。

优化4:使用多段式构建

想象下列场景,我们要从源码编译一个go程序并打包进一个容器,最后作为容器的entrypoint来运行这个程序, 在没有多段式构建之前,我们需要:

  1. 写一个dockerfile来负责编译go程序
  2. 写一个dockerfile来负责跑go程序
  3. 写一个脚本来生成docker1,然后从docker1拿出编译好的go程序以运行docker2

不考虑整个容器的体积或者源码安全性时,我们也可以把两个docker结合到一起,把项目的编译、测试和运行通通打包到一个dockerfile中运行,这明显是很笨重的。所以在docker v17.05后便有了多段式构建:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
FROM golang:alpine as builder
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld/
RUN go get -d -v github.com/go-sql-driver/mysql
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/go/helloworld/app .
CMD ["./app"]

在多段式构建下,每个阶段的构建都可以基于新的镜像,而下面的构建则可以引用上面镜像的生成的文件,如此,我们最后便能获得一个体积较小的,只用于运行go程序的镜像了。

Reference

官网文档

Docker容器的优化思路