上一节简单介绍了build时的分层以及缓存, 本节将会介绍如何统计每一层的构建时间, 以及一些加快构建的策略, 另外还有一些大家可以遵守的规则.

统计每一行构建时间

如果我们想要优化Dockerfile的构建. 量化Dockerfile的每一句是很有必要的. 很可惜, Docker build时, 并没有提供参数显示每一行的构建时间. 我贴一下自己平时使用的语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 截取自: https://stackoverflow.com/a/52191994
# 你可以在这里找到其他的脚本.
docker build . -t test --no-cache | while read line ; do echo "$(date)| $line"; done;

Fri 08 Nov 2019 09:20:54 AM HKT| Sending build context to Docker daemon 8.192kB
Fri 08 Nov 2019 09:20:55 AM HKT| Step 1/5 : FROM node:10.17.0-alpine
Fri 08 Nov 2019 09:20:55 AM HKT| ---> 0aa7bb41deca
Fri 08 Nov 2019 09:20:55 AM HKT| Step 2/5 : COPY main.js main.js
Fri 08 Nov 2019 09:20:55 AM HKT| ---> 05c746d8951a
Fri 08 Nov 2019 09:20:55 AM HKT| Step 3/5 : COPY full-run.sh /run.sh
Fri 08 Nov 2019 09:20:55 AM HKT| ---> 3a44ba811766
Fri 08 Nov 2019 09:20:55 AM HKT| Step 4/5 : RUN chmod +x /run.sh
Fri 08 Nov 2019 09:20:56 AM HKT| ---> Running in 2ceec4735960
Fri 08 Nov 2019 09:20:57 AM HKT| Removing intermediate container 2ceec4735960
Fri 08 Nov 2019 09:20:57 AM HKT| ---> 8baf4c5d9284
Fri 08 Nov 2019 09:20:57 AM HKT| Step 5/5 : ENTRYPOINT ["/run.sh"]
Fri 08 Nov 2019 09:20:57 AM HKT| ---> Running in 1ebf29aa5b85
Fri 08 Nov 2019 09:20:57 AM HKT| Removing intermediate container 1ebf29aa5b85
Fri 08 Nov 2019 09:20:57 AM HKT| ---> 9cb776220122
Fri 08 Nov 2019 09:20:57 AM HKT| Successfully built 9cb776220122
Fri 08 Nov 2019 09:20:57 AM HKT| Successfully tagged test:latest

如果你想进一步处理, 可以把$(date)改为$(date +%s), 这样展示的就是时间戳了.

镜像构建优化

镜像构建优化的一个原则就是将公共层语句放在靠前的位置, 这样构建使得能够尽量的利用缓存.

官方文档使用缓存一节, 介绍了何时会复用构建缓存

  1. 大部分情况下, Dockerfile中的语句不变, 该层的缓存就会被复用
  2. 当使用了ADD和COPY命令时, 会检查每个文件的checksum, 完全相同时则会被复用.

我们一起看看下面这个栗子:

1
2
3
4
5
# 拷贝代码, 假装有好多文件
COPY main.py main.py

COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
1
2
3
4
5
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/

# 拷贝代码, 假装有好多文件
COPY main.py main.py

大家可以考虑下, requirements.txt的改变频率和代码的改变频率相比要小的多. 如果我们按照第二种形式, 将requirements.txt语句提前, 那么该层会复用, 并且, pip install的这一层也会被复用, 这样就避免了每次构建镜像时安装依赖.

希望大家看过之后可以举一反三, 例如, node的package.json, 或者java的mvn或是gradle配置文件, 都可以将其语句提前, 避免每次安装依赖.

一些应该遵守的规则

Git tag的使用

有些朋友在书写Dockerfile时, 喜欢使用git clone代码, 这件事并没有什么问题. 但是我希望这样的朋友能够小心一点. 如果你不指定版本, 像是这样

1
RUN git clone https://github.com/tornadoweb/tornado.git

如果你真的这么写了, 那么恭喜你, 每次构建镜像得到的tornado版本, 不是最新的就是最旧的. 写Dockerfile的时候, 请一定要注意, 避免将自己的代码中存在不确定的状态, git本身提供了发布分支以及开发分支, clone的时候完全可以增加tag来将版本固定, 书写Dockerfile时, 请一定要注意.

1
RUN git clone -b 'v6.0.3' https://github.com/tornadoweb/tornado.git

最精简版本

这里所说的最精简就是不要添加任何其他的依赖, 其实主要是讲编译型语言, 例如C++, Java, Golang, 理论上在得到可执行文件后, 构建工具等的依赖是可以丢弃的, 一般可能我们会想到把这些依赖删掉, 后来Docker提供了多阶段构建这样的工具, 允许我们在一个Dockerfile里写多个FROM语句, 可以将上述的二进制文件从一个镜像在构建时就拷贝至另一个镜像.

这里是多阶段构建的官方文档, 我认为在解释型语言类似PythonNodeJS中没有必要取用多阶段构建, 除非你是需要其他依赖.

1
2
3
4
5
6
7
8
9
10
11
FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

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

总结

这篇博客主要介绍了有关Dockerfile的优化策略, 以及应该遵守的规则, 最后简要介绍了多阶段构建, 不同项目, 不同语言的Dockerfile优化策略不尽相同. 请大家书写Dockerfile时把握下面几点:

  1. 将公有依赖提前
  2. 保证代码确定性
  3. 保证依赖最少