镜像打包这么慢,你该反省一下你的dockerfile姿势了

镜像打包这么慢,你该反省一下你的dockerfile姿势了

使用Dockerfile构建镜像是云原生的第一步。 在企业级Devops中又强调高效快速构建镜像。

今天给出两个思路 优化Dockerfile构建镜像的效能。

使用Docker Layer Caching 优化构建速度

基于Docker的多阶段构建优化镜像大小

1. Docker Layer Caching

应用程序打包过程是很慢的(下载并安装框架&第三方依赖包、生成assets),这在Docker中也不例外。

1.1 什么是Docker Layer Caching

Docker使用层layer创建镜像,Dockerfile中每一个命令都会创建一个新的层,每层都包含执行命令前后的状态之间镜像的文件系统更改。

为了加快构建速度,Docker实现了缓存:

如果Dockerfile和相关文件未更改,则重建(rebuild)时可以重用本地镜像缓存中的某些现有层。

但是,为了利用此缓存,您需要了解它的工作方式,这就是我们将在本文中介绍的内容。

1.2 docker layer caching 算法

当您构建Dockerfile时,Docker将查看它是否可以使用先前构建的缓存结果:

对于大多数命令,如果命令文本未更改,则将使用缓存中的版本。

对于COPY,它还会检查您要复制的文件是否未更改。

我们来看一个使用以下Dockerfile的示例:

FROM python:3.7-slim-buster

COPY . .

RUN pip install --quiet -r requirements.txt

ENTRYPOINT ["python", "server.py"]

第一次运行时,所有命令都会运行:

$ docker build -t example1 .

Sending build context to Docker daemon 5.12kB

Step 1/4 : FROM python:3.7-slim-buster

---> f96c28b7013f

Step 2/4 : COPY . .

---> eff791eb839d

Step 3/4 : RUN pip install --quiet -r requirements.txt

---> Running in 591f97f47b6e

Removing intermediate container 591f97f47b6e

---> 02c7cf5a3d9a

Step 4/4 : ENTRYPOINT ["python", "server.py"]

---> Running in e3cf483c3381

Removing intermediate container e3cf483c3381

---> 598b0340cc90

Successfully built 598b0340cc90

Successfully tagged example1:latest

第二次构建时,因为没有任何改变,docker构建将使用镜像缓存:

$ docker build -t example1 .

Sending build context to Docker daemon 5.12kB

Step 1/4 : FROM python:3.7-slim-buster

---> f96c28b7013f

Step 2/4 : COPY . .

---> Using cache

---> eff791eb839d

Step 3/4 : RUN pip install --quiet -r requirements.txt

---> Using cache

---> 02c7cf5a3d9a

Step 4/4 : ENTRYPOINT ["python", "server.py"]

---> Using cache

---> 598b0340cc90

Successfully built 598b0340cc90

Successfully tagged example1:latest

请注意,上面显示的Using cache加快了构建速度(无需从网络下载任何pip依赖包)

如果我们删除镜像,则后续构建将从头开始(没有层缓存了):

$ docker image rm example1

Untagged: example1:latest

Deleted: sha256:598b0340cc90967501c5c51862dc586ca69a01ca465f48232fc457d3ab122a73

Deleted: sha256:02c7cf5a3d9af1939b9f5286312b23898fd3ea12b7cb1d7a77251251740a806c

Deleted: sha256:d9e9602d9c3fd7381a8e1de301dc4345be2eb2b8488b5fc3e190eaacbb2f9596

Deleted: sha256:eff791eb839d00cbf46d139d8595b23867bc580bb9164b90253d0b2d9fcca236

Deleted: sha256:53d34b2ead0a465d229a4260fee2a845fb8551856d4019cd2e608dfe0e039e77

$ docker build -t example1 .

Sending build context to Docker daemon 5.12kB

Step 1/4 : FROM python:3.7-slim-buster

---> f96c28b7013f

Step 2/4 : COPY . .

---> 63c32b9b1af6

...

Taking advantage of caching

缓存算法还有一个更重要的规则:

如果某层无法应用层缓存,则后续层都不能从层缓存加载

在以下示例中,虽然前后两次C层构建均未更改,但是由于上层B并不是从层缓存中加载,因此后置的C层仍然无法从缓存中加载:

层缓存对下面的Dockerfile意味着什么?

FROM python:3.7-slim-buster

COPY requirements.txt .

COPY server.py .

RUN pip install --quiet -r requirements.txt

ENTRYPOINT ["python", "server.py"]

如果COPY命令的任何文件改变了,则会使后续所有层缓存失效:我们需要重新运行pip install。

但是,如果server.py更改了,但requirements.txt却没有更改,为什么我们必须重做pip安装?毕竟,pip安装仅使用requirements.txt。

推及到现代编程语言:前端的依赖包文件packaage.json, dotnet的项目库管理文件demo.csproj等,一般很少变更; 我们最好利用这些不变的管理文件,使用缓存加载难缠的库文件; 对于可能变动的业务代码,延后进入镜像,减小层缓存失效的可能(后续层每次都要重新下载&安装依赖)。

因此,你要做的是仅复制需要运行的下一步文件,以最大程度地减少缓存失效的机会。

FROM python:3.7-slim-buster

COPY requirements.txt .

RUN pip install --quiet -r requirements.txt

COPY server.py .

ENTRYPOINT ["python", "server.py"]

由于server.py仅在pip安装后才复制到构建上下文,因此,只要requirements.txt不变,仍然可以从缓存加载由pip安装创建的层。

1.3 利用layer caching优化Dockerfile

若你想通过重用之前缓存的层来进行快速构建,则需要适当地编写Dockerfile:

仅复制下一步所需的文件,以最大程度地减少构建过程中的缓存失效。

尽量将文件可能变更的新增(ADD命令)、拷贝(COPY命令) 延迟到Dockerfile的后部。

DotnetCore福利时间

观看Visual Studio web项目默认脚手架形成的Dockerfile:

正是基于Docker Layer Caching思维,做了两次COPY命令。

2. Docker多阶段构建优化构建镜像的大小

docker 基于联合文件系统, 这个union file system的特征是: 对于文件系统的修改可作为 一次提交来一层层的叠加。

注意,在layer中尝试 rm -rf asserts 并不会减少镜像大小。

每一层的叠加,最终形成了整个镜像的大小,可使用 docker hisrory [option] image 来分析镜像的构建。

docker history nginx

IMAGE CREATED CREATED BY SIZE COMMENT

3f8a4339aadd 7 years ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B

7 years ago /bin/sh -c #(nop) STOPSIGNAL [SIGTERM] 0B

7 years ago /bin/sh -c #(nop) EXPOSE 80/tcp 0B

7 years ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx… 22B

7 years ago /bin/sh -c set -x && apt-get update && apt… 53.2MB

7 years ago /bin/sh -c #(nop) ENV NJS_VERSION=1.13.8.0.… 0B

7 years ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.13.8-… 0B

7 years ago /bin/sh -c #(nop) LABEL maintainer=NGINX Do… 0B

7 years ago /bin/sh -c #(nop) CMD ["bash"] 0B

7 years ago /bin/sh -c #(nop) ADD file:f30a8b5b7cdc9ba33… 55.3MB

基于这个理论,很多时候我们只希望镜像包含一个运行环境+ 可执行文件+ 配置。

但目前镜像构建阶段 的层可能产生了不少产物, 于是多阶段构建是镜像构建的必要方式。

第一阶段主做编译, 产生可执行文件+ 配置文件

第二阶段: 基于运行时镜像,从第一阶段拷贝 可执行文件+配置文件。整个镜像的大小就由第二阶段的行为决定。

多阶段构建 避免了构建中 layer的中间产物, 也能规避 rm-rf 不能减少镜像大小的弊端。

养生小贴士

浑身发抖是什么原因引起的
💡 小知识

浑身发抖是什么原因引起的

📅 08-31 👍 983
中国企业为什么热衷选择在纳斯达克上市?
射鱼弹弓
💡 小知识

射鱼弹弓

📅 07-18 👍 642