共计 6788 个字符,预计需要花费 17 分钟才能阅读完成。
简介:云原生时代下软件的构建和部署离不开容器技术。提到容器,简直大家下意识都会联想到 Docker。而 Docker 中有两个十分重要的概念,一个是 Image(镜像),一个是 Container(容器)。前者是一个动态视图,打包了利用的目录构造、运行环境等;后者是一个动静视图(过程),展现的是程序的运行状态(cpu、memory、storage)等信息。接下来的文章次要分享的是如何编写能使 Dockerfile 构建过程更疾速、构建镜像更小的技巧。
大家好,我是陈泽锋,我在云效负责 Flow 流水线编排、任务调度引擎相干的工作。在云效的产品体系下,咱们服务了各种研发规模、技术深度的的企业用户,收到了十分多的用户反馈。对于应用 Flow 进行云上构建的用户来说,构建速度是大家广泛关怀的要害因素,在深入分析用户案例的过程中,咱们发现了许多通用问题,只须要批改优化本人的我的项目或工程配置,就能够大大晋升构建的性能,从而进一步减速 CICD 的效率。明天咱们会以容器镜像构建作为切入点,总结一些在理论工程中,十分实用的优化技巧。
云原生时代下软件的构建和部署离不开容器技术。提到容器,简直大家下意识都会联想到 Docker。而 Docker 中有两个十分重要的概念,一个是 Image(镜像),一个是 Container(容器)。前者是一个动态视图,打包了利用的目录构造、运行环境等;后者是一个动静视图(过程),展现的是程序的运行状态(cpu、memory、storage)等信息。接下来的文章次要分享的是如何编写能使 Dockerfile 构建过程更疾速、构建镜像更小的技巧。
镜像定义
首先咱们先来理解一下 Docker 镜像,它由多个只读层重叠到一起,每一层是上一层的增量批改。基于镜像创立新容器时,将在根底层的顶部增加一个新的可写层。该层通常称为“容器层”。下图展现了一个基于 docker.io/centos 根底镜像构建的利用镜像,创立出容器时的视图。
从图中咱们能够看到镜像构建、容器启动的过程。
- 首先是拉取根底镜像 docker.io/centos;
- 基于 docker.io/centos 来启动一个容器,运行指令 yum update 后进行 docker commit 提交出一个新的只读层 v1(能够了解为生成了一个新的长期镜像 A,只不过用户并不会间接援用到它);
- 基于长期镜像 A 启动新的容器,运行装置和配置 http server 等软件后,提交出一个新的只读层 v2,也生成了这里最终被开发者援用的镜像版本 B;
- 基于镜像版本 B 运行的容器,会再追加一层读写层(对容器的文件创建、批改、删除等操作,都在这一层失效);
镜像起源
镜像次要是 Docker 通过读取、运行 Dockerfile 的指令来生成。举官网上的一个 Dockerfile 例子:
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py
它的外围逻辑是定义援用的根底镜像 base image,执行如 COPY 指令从上下文 context 里复制文件到容器中,运行 RUN 执行用户自定义构建脚本,最初定义容器启动的 CMD 或 ENTRYPOINT。构建更高效的镜像也要围绕上述波及到的概念进行优化。
Dockerfile 优化技巧
应用国内的根底镜像
Flow 作为云上构建产品,每次构建都会给用户提供全新的构建环境,以防止环境污染导致带来过高运维老本。正因为如此,Flow 每次构建都会从新去下载 Dockerfile 中指定的根底镜像。
如果 Dockerfile 中指定根底镜像来源于 Docker Hub,则有可能因为网络延时问题导致下载迟缓,比方:
- From Nginx
- From java:8
- FROM openjdk:8-jdk-alpine
典型景象如下:
能够将本人的根底镜像文件转存至国内镜像仓库,并批改本人的 Dockerfile 文件,操作步骤如下:
- 将境外镜像在 pull 到本地。docker pull openjdk:8-jdk-alpine;
- 将根底镜像 push 到阿里云镜像仓库(cr.console.aliyun.com)的国内 region(比方北京、上海等)。docker tag openjdk:8-jdk-alpine registry.cn-beijing.aliyuncs.com/yournamespace/openjdk:8-jdk-alpinedocker push registry.cn-beijing.aliyuncs.com/yournamespace/openjdk:8-jdk-alpi;
- 批改你的 dockerfile 中 FROM,从你本人的镜像仓库下载镜像。From registry.cn-beijing.aliyuncs.com/yournamespace/openjdk:8-jdk-alpine;
尽量小的、够用的根底镜像
大镜像除了占用更多的磁盘空间外,在利用部署时也会占用更多的网络耗费,导致更长的服务启动耗时。应用更小的根底镜像,例如应用 alpine 作为 base image。这里咱们看一个打包 mysql-client 二进制的镜像,基于 alpine 和 ubuntu 的镜像大小比照。
FROM alpine:3.14
RUN apk add --no-cache mysql-client
ENTRYPOINT ["mysql"]
FROM ubuntu:20.04
RUN apt-get update \
&& apt-get install -y --no-install-recommends mysql-client \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["mysql"]
由此能够看到应用尽量小的 base 镜像有利于大幅度缩小镜像的大小。
缩小上下文关联目录文件
docker 是 c/s 的架构设计,当用户执行 docker build 时并不是在 client 间接进行构建,而是将 build 指定的目录作为上下文传递到 server 端,再执行上述提到的镜像构建的过程。如果执行镜像构建的上下文中关联大量不必要的文件,那能够应用 .dockerignore 来疏忽这些文件(与 .gitignore 相似,定义的文件不会被跟踪、传输)。
以下举一个官网上的例子,通过构建日志能够察看看 context 的大小只有几十 byte:
mkdir myproject && cd myproject
echo "hello" > hello
echo -e "FROM busybox\nCOPY / /\nRUN cat /hello" > Dockerfile
docker build -t helloapp:v1 --progress=plain .
#7 [internal] load build context
#7 sha256:6b998f8faef17a6686d03380d6b9a60a4b5abca988ea7ea8341adfae112ebaec
#7 transferring context: 26B done
#7 DONE 0.0s
当咱们在 myproject 下搁置一个与程序无关的大文件(或无关小文件,如利用构建的依赖包等)时,从新构建 helloapp:v3 时发现须要传输 70 MB 的内容到服务端,并且镜像大小到 71MB。
#5 [internal] load build context
#5 sha256:746b8f3c5fdd5aa11b2e2dad6636627b0ed8d710fe07470735ae58682825811f
#5 transferring context: 70.20MB 1.0s done
#5 DONE 1.1s
缩小层的数量、管制层的大小
如果把镜像构建的简略等同为 bash 等脚本指令执行的过程,往往就会踩中镜像层过多,镜像层蕴含无用文件的坑。上面让咱们看三个 dockerfile 的写法和它们别离构建进去的镜像大小。
- 首先是 centos_git_nginx:normal 镜像,它基于 centos 根底镜像减少了两层,别离装置了 git 和 nginx 两个二进制,能够看到镜像的大小大略在 402MB。
FROM centos
RUN yum install -y git
RUN yum install -y nginx
- 接着咱们对 dockerfile 做一下优化,将它改成以下只减少一层的写法,能够看到镜像的大小缩减到 384 MB,证实了层的缩小能缩小镜像的大小。
FROM centos
RUN yum install -y git && yum install -y nginx
因为 yum install 过程会生成一些缓存数据,这些在利用运行过程中是不须要的,咱们在装置完软件后立刻将其删除后观察镜像再次放大到 357 MB。
FROM centos
RUN yum install -y git && \
yum install -y nginx && \
yum clean all && rm -rf /var/cache/yum/*
TIPS: 咱们晓得了镜像构建过程生成每一层为只读层是不能再被批改的,以下的写法并不能对缩小镜像的大小起到作用,反而还减少了一层无用镜像层。
FROM centos
RUN yum install -y git && \
yum install -y nginx
RUN yum clean all && rm -rf /var/cache/yum/*
须要留神的是过于谋求档次的少也不肯定是好的做法,这样会使得构建或拉取镜像时缩小了层被缓存的概率。
将不变层放到后面,可变层放到前面
当咱们在同个工夫内屡次执行 docker build 能够发现,在构建完一次镜像后再次构建,docker 会利用缓存中的镜像数据间接进行复用。
事实上 Docker 会逐渐实现 Dockerfile 中的指令,并按指定的程序执行每个指令。在查看每条指令时,Docker 在其缓存中查找能够重用的现有镜像。Docker 从缓存中已存在的父镜像开始,将下一条指令与从该根本镜像派生的所有子镜像进行比拟,以查看其中是否有一条是应用完全相同的指令生成的。否则,缓存将有效。
举个例子,咱们能够将简略、常常被依赖到的根本软件如 git、make 等不常变动却罕用的指令放到后面执行,这样镜像构建的过程层就能间接利用后面生成的缓存,而不是反复的下载软件,即节约带宽又耗费工夫。
这里咱们对两种写法进行比照,首先初始化相干目录与文件:
mkdir myproject && cd myproject
echo "hello" > hello
- 第一种 dockerfile 的写法为先 COPY 文件,再进行 RUN 装置软件操作。
FROM ubuntu:18.04
COPY /hello /
RUN apt-get update --fix-missing && apt-get install -y \
aufs-tools \
automake \
build-essential \
curl \
dpkg-sig \
libcap-dev \
libsqlite3-dev \
mercurial \
reprepro \
ruby1.9.1 \
&& rm -rf /var/lib/apt/lists/*
通过对 time docker build -t cache_test -f Dockerfile . 进行镜像构建,构建胜利再屡次执行能够发现后续构建间接命中缓存生成镜像。
time docker build -t cache_test -f Dockerfile .
[+] Building 59.8s (8/8) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 35B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:18.04 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 26B 0.0s
=> [1/3] FROM docker.io/library/ubuntu:18.04 0.0s
=> CACHED [2/3] COPY /hello / 0.0s
=> [3/3] RUN apt-get update && apt-get install -y aufs-tools automake build-essential curl dpkg-sig && rm -rf /var/lib/apt/lists/* 58.3s
=> exporting to image 1.3s
=> => exporting layers 1.3s
=> => writing image sha256:5922b062e65455c75a74c94273ab6cb855f3730c6e458ef911b8ba2ddd1ede18 0.0s
=> => naming to docker.io/library/cache_test 0.0s
docker build -t cache_test -f Dockerfile . 0.33s user 0.31s system 1% cpu 1:00.37 total
time docker build -t cache_test -f Dockerfile .
docker build -t cache_test -f Dockerfile . 0.12s user 0.08s system 34% cpu 0.558 total
批改 hello 文件的内容,echo “world” >> hello,再次执行 time docker build -t cache_test -f Dockerfile . , 此时镜像构建的耗时又回到了 1 分钟左右。
- 第二种写法的 dockerfile 如下,咱们将根本不变的根底软件装置放到下面,将可能变动的 hello 文件放到上面。
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y \
aufs-tools \
automake \
build-essential \
curl \
dpkg-sig \
&& rm -rf /var/lib/apt/lists/*
COPY /hello /
通过对 time docker build -t cache_test -f Dockerfile . 进行镜像构建,第一次构建耗时在 1 分钟左右(构建胜利再屡次执行一样命中缓存生成镜像)。
批改 hello 文件的内容,date >> hello,再次执行 time docker build -t cache_test -f Dockerfile . , 此时镜像构建的耗时在 1s 内,即胜利复用第二层构建过的缓存层。
应用多阶段来拆散 build 和 runtime
这里举一个 golang 的例子,首先将 example 代码库 https://github.com/golang/exa… clone 到本地,增加一个 dockerfile 进行构建利用镜像。
FROM golang:1.17.6
ADD . /go/src/github.com/golang/example
WORKDIR /go/src/github.com/golang/example
RUN go build -o /go/src/github.com/golang/example/hello /go/src/github.com/golang/example/hello/hello.go
ENTRYPOINT ["/go/src/github.com/golang/example/hello"]
咱们能够看到镜像的大小是 943 MB,程序失常输入 Hello, Go examples!
接着让咱们应用多阶段构建和尽量小的 runtime 来优化以上的过程。
FROM golang:1.17.6 AS BUILDER
ADD . /go/src/github.com/golang/example
RUN go build -o /go/src/github.com/golang/example/hello /go/src/github.com/golang/example/hello/hello.go
FROM golang:1.17.6-alpine
WORKDIR /go/src/github.com/golang/example
COPY --from=BUILDER /go/src/github.com/golang/example/hello /go/src/github.com/golang/example/hello
ENTRYPOINT ["/go/src/github.com/golang/example/hello"]
能够看到目前的镜像大小只有 317 MB。通过多阶段构建将利用构建和运行时依赖进行拆散,只有将 runtime 依赖的软件会最终打到利用镜像中去。
原文链接
本文为阿里云原创内容,未经容许不得转载。