N个技巧,编写更高效 Dockerfile|云效工程师指北

简介:云原生时代下软件的构建和部署离不开容器技术。提到容器,几乎大家下意识都会联想到 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 文件,操作步骤如下:

  1. 将境外镜像在 pull 到本地。docker pull openjdk:8-jdk-alpine;
  2. 将基础镜像 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;
  3. 修改你的 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 myprojectecho "hello" > helloecho -e "FROM busybox\nCOPY / /\nRUN cat /hello" > Dockerfiledocker 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 myprojectecho "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.0sdocker 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/example 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.goFROM 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 依赖的软件会最终打到应用镜像中去。

原文链接

本文为阿里云原创内容,未经允许不得转载。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/511510.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TDA-04D8变送器数据上报阿里云

简介:本文将以TDA-04D8变送器作为采集对象,使用海创微联采集控制系统对TDA-04D8变送器进行采集,然后将设备上的毛重、净重、皮重数据采集上传到阿里云物联网平台,阿里云物联网平台将数据实时可视化。 文章分为3部分: …

http ,怎么优雅的拒绝你

作者 | 奇伢来源 | 奇伢云存储典型问题:服务端优雅的拒绝今天分享一个后端编程的实际经验。这个问题来源于对象 S3 后端协议实现的技巧思考。场景:服务端不想接收 http 的 body 的时候,该怎么优雅的拒绝呢?什么意思?对…

linux iio 设备驱动,Linux设备驱动之IIO子系统——IIO框架数据读取-Go语言中文社区...

IIO DATA ACCESS IIO数据获取只有两种方法可以使用IIO框架访问数据; 通过sysf通道进行一次性捕获,或通过IIO字符设备进行连续模式(触发缓冲)。One-shot capture:单次获取一次性数据捕获通过sysfs接口完成。 通过读取与通道对应的sysfs条目,您…

企业物联网平台新版公共实例升级企业实例教程

简介:2021年7月30日企业物联网平台重磅升级,发布的新版公共实例支持一键升级企业版实例,本文将为大家介绍一键升级教程 一、企业版实例,企业用户首选 企业物联网平台 提供设备上云必备的基础服务,用户无需自建物联网…

【全观测系列】Elasticsearch应用性能监控实践

简介:本文介绍了应用性能监控的应用价值以及解决方案等。 1、什么是全观测? 要了解全观测,我们先看看传统运维存在哪些问题。 数据孤岛,分散在不同部门,分析排查故障困难;多个厂商的多种工具&#xff0c…

Gartner发布当前至2024年的五大隐私趋势

到2024年,全球75%人口的个人数据将得到隐私法规的保护。 供稿 | Gartner 出品 | CSDN云计算 根据Gartner的研究,随着全球隐私法规数量的不断增加,企业机构应关注五项重大隐私趋势,以应对保护个人数据和遵守监管要求方面的挑战。 …

linux下qt生成可安装的程序,linux – 如何为Qt应用程序创建“安装”包?

您可以从项目中创建debian包.据我所知,你想创建一个用于分发的包,所以我建议你从你的项目中创建一个debian包.Here是Debian Packaging系统的介绍.在文章中,他们在某些时候描述了如何创建一个“规则”文件,它是构建过程的核心.以下是我通常用于Qt / KDE项目的示例:#!…

es实战-使用IK分词器进行词频统计

简介:通过IK分词器分词并生成词云。 本文主要介绍如何通过 IK 分词器进行词频统计。使用分词器对文章的词频进行统计,主要目的是实现如下图所示的词云功能,可以找到文章内的重点词汇。后续也可以对词进行词性标注,实体识别以及对…

IC Nansha|AMD高级副总裁、大中华区总裁潘晓明:制程、架构、平台优化突破计算边界

6月25日,中国南沙国际集成电路产业论坛在广州南沙顺利举行。AMD高级副总裁、大中华区总裁潘晓明出席了本次会议,并在高峰论坛环节中以《高性能计算的未来》为主题发表了演讲。 (AMD高级副总裁、大中华区总裁 潘晓明) 作为一家深耕…

linux 输出后面几列内容,Linux下使用awk如何获取关键字所在字段号,第几列,以及打印这个字段之后的所有字段?...

netstat 的原始结果如下:[rootTest tmp]# netstat -tunp| grep ESTABLISHEDtcp 0 0 10.251.101.163:6379 10.251.101.163:51602 ESTABLISHED 2115/redis-server 1tcp 0 0 127.0.0.1:58657 127.0.0.1:3306 ESTABLISHED 13354/rpc_time_servtcp 0 0 10.251.101.163:52…

nltkdata路径设置linux,NLTK data路径设置

安装nltk后用这个命令:from nltk.book import *按照正常的,应该出现以下:>>> from nltk.book import **** Introductory Examples for the NLTK Book ***Loading text1, ..., text9 and sent1, ..., sent9Type the name of the text…

爱数SMART 2022峰会开启,分享数据战略与建设数据驱动型组织方法论

6月28日,爱数SMART 2022线上峰会全球直播正式开启。主论坛上,爱数正式提出了企业制定数据战略以及建设数据驱动型组织的方法论,并推出开源计划与数字伙伴计划2.0,共创数据驱动型组织。 通过清晰的数据战略,从容加速数据…

云原生时代开发者工具变革探索与实践

简介:本篇内容分享了原生时代开发者工具变革探索与实践。 分享人:马洪喜 行云创新CEO 正文:本篇内容将通过三个部分来介绍云原生时代开发者工具变革探索与实践。 一、云原生模块化开发概览 二、软件模块化开发特点 三、ADD产品简介 一、…

喜马拉雅 Apache RocketMQ 消息治理实践

简介:本文通过喜马拉雅的RocketMQ治理实践分享,让大家了解使用消息中间件过程中可能遇到的问题,避免实战中踩坑。 作者:曹融,来自喜马拉雅,从事微服务和消息相关中间件开发。 本文通过喜马拉雅的RocketMQ治…

Linux里怎么进行路由跟踪,[Linux] traceroute 路由跟踪指令用例

traceroute是用来跟踪数据包到达网络主机所经过的路由工具。在Linux系统中,称之为traceroute,在Windows中称为tracert。一条路径上的每个设备traceroute要测3次。输出结果中包含每次测试的时间(ms)和设备的名称及其IP。1、命令格式:tracerout…

Docker 容器为什么傲娇?全靠镜像撑腰!

作者 | 飞向星的客机来源 | CSDN博客🌟 前言Docker 镜像是 Docker 容器的基石,容器是镜像的运行实例,有了镜像才能启动容器。Docker 镜像是一个只读的模板,一个独立的文件系统,包括运行一个容器所需的数据,…

HBase读链路分析

简介:HBase的存储引擎是基于LSM-Like树实现的,更新操作不会直接去更新数据,而是使用各种type字段(put,delete)来标记一个新的多版本数据,采用定期compaction的形式来归档合并数据。这种数据结构…

设置linux文件系统密码,busybox 文件系统设置 登陆 login 密码 password shadow

用busybox做文件系统的很多介绍,这里就不啰嗦了。说几点:1、etc目录下有inittab,则系统按此文件规则来启动和运行,内容为:::sysinit:/etc/init.d/rcS#把respawn改成askfirst就会出现按enter才能进去的提示::respawn:-/…

PolarDB for PostgreSQL 开源路线图

简介:作者:蔡乐 本文主要分享一下Polar DB for PG的开源路线图,虽然路线图已经拟定,但是作为开源产品,所有参与者都能提出修改意见,包括架构核心特性的技术以及周边生态和工具等,希望大家能够踊…

5分钟入门Lindorm SearchIndex

简介:SearchIndex是Lindorm宽表的二级索引,主要用来帮助业务实现快速的检索分析。本篇文章介绍如何通过简单的SQL接口操作SearchIndex。 一、引言 云原生多模数据库Lindorm,支持海量数据的低成本存储和弹性按需付费,提供宽表、时…