【转载】Docker 镜像优化与最佳实践

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 从Docker镜像存储的原理开始,针对镜像的存储、网络传输,介绍如何在构建中对这些关键点进行优化;并介绍Docker最新的多阶段构建的功能,以解决构建依赖的中间产物问题。

阿里云高级研发工程师御坂在云栖TechDay41期的线下沙龙活动中分享了Docker镜像优化与最佳实践。本文为沙龙内容回顾。

从Docker镜像存储的原理开始,针对镜像的存储、网络传输,介绍如何在构建中对这些关键点进行优化。并介绍Docker最新的多阶段构建的功能,以解决构建依赖的中间产物问题。

镜像概念

镜像是什么?

从一个比较具体的角度去看,镜像就是一个多层存储的文件,相较于普通的ISO系统镜像来说,分层存储会带来两个优点:

  • 一个是分层存储的镜像比较容易扩展,比如我们可以基于一个Ubuntu镜像去构建我们的Nginx镜像,这样我们只需要在Ubuntu镜像的基础上面做一些Nginx的安装配置工作。一个Nginx镜像工作就算制作完成了,我们不需要从头开始去制作各种镜像。
  • 另一点是可以优化镜像存储空间,假如我们有两个镜像,Tag1.0镜像和 Tag2.0镜像,我们如果以传统方式去传这两个镜像,每个镜像大概130多兆,但如果我们以分层的方式去存储两个镜像,我们通过下面两个紫色的才能共享,可以节约大量的空间,两个镜像加起来只需要140多兆的空间就可以存下来。这样一是节省了存储空间,二是可以减少网络上的开销,比如我们已经把下面镜像下载了,我们要去下载上面镜像的时候,我们只需要去下10M的部分。

如果从抽象的角度去看,Docker镜像其实是Docker提供的一种标准化的交付手段传统应用在交付的时候其实是交付一个可执行文j件。问题在于传统方式的这个可执行文件不包括它的运行环境,我们可能会因为32位系统或64位系统,或者开发测试使用1.0软件,结果交付时候发现用户的环境是2.0等各种各样的问题,导致我们要去花时间去排查;但是,如果我们以Docker镜像的标准化形式去交付,我们就会避免掉这些问题。

## 镜像基本操作与存储方式

我们的一个镜像会有一个坐标,一个镜像坐标基本上会由四个部分组成,前面会有一个镜像服务域名,每一个服务提供商都会有不同的域名,当我们确定服务提供商给我们的域名之后,我们一般会要到服务提供商那里去申请自己的命名空间,仓库名称一般是标识镜像的用途,比如说Ubuntu镜像、CentOS镜像,标签一般是用于去区分镜像版本,比如我们对Ubuntu镜像可能会打一些16.04的包,在我们确定了一个镜像服务域名以及在云服务商申请命名空间之后,我们就可以对镜像做一些操作了。

  1. 首先我们需要去登陆,我们会用第一条命令去登陆;
  2. 然后,当我们在本地准备好一个镜像想要上传的时候,我们先要对这个镜像进行打标,把它的坐标变成我们现在需要上传镜像的坐标;
  3. 接下来,再去做一些推送拉取的动作;
  4. 最后针对Docker还提供两个额外命令去做镜像交付,如果我们是特殊的环境,没有办法网络连通的时候,我们可以将这个镜像打包成一个普通文件进行传输。比如我们和公安合作,他们没有办法通过我们的Registry下载镜像,我们可能要把它打成一个普通文件,然后以U盘的方式去交付。

镜像存储细节

Docker镜像是存在联合文件系统的,每一个镜像其实是分层存储的,比如在第一层我们添加了三个新文件,然后在这一层基础上我们又增加了一层,添加了一个文件,第三层可能会需要做一些修改,我们把File3做了一个修改移到上面来,然后删掉了File4,这里就会引到联合文件系统里面的写时复制机制,当我们要去修改一个文件的时候,镜像依赖底层都是只读的,我们不能去直接修改,比如我们想去修改File3,我们不能直接去修改这个文件,我们需要在修改的时候把文件复制到当前这一层,比如说L3层,然后再去修改它。

一个镜像做好之后,当我们想要知道镜像里面有哪一些内容的时候,我们其实会有一个视图概念,我们从联合文件系统的角度去看镜像的时候,其实我们不会看到L1、L2、L3,我们会最后看到结果,File1、File2、File3,File4就看不到了,然后在我们了解原理之后,我们就可以去理解容器运行起来是一个什么样的情况。容器运行起来和上面形成是类似的,图中下半部分,同样也是L1、L2、L3的三层镜像,当容器运行起来的时候,Docker daemon会动态生成一层可写层作为容器的运行层,然后当容器里面需要去修改一些文件,比如File2,也是copy on write机制把文件复制上来,然后做一些修改,新增文件的时候也是一样,然后容器在运行的时候也会有一个视图,当我们把容器停掉的时候,视图一层就没有了,它会被销毁,但是容器层读写层还会保留,所以我们把容器停掉再启动的时候,我们依旧会看到我们之前在容器里面的一些操作。

常见的存储驱动主要有AUFS、OverlayFS,还有Device Mapper,前两种驱动都是基于文件,它的原理就是需要修改一个文件的时候把整个文件复制上去做修改, Device Mapper更偏底层一点,它是基于块设备的,它的好处在于当我想要修改一个文件的时候,我不会将整个文件拷上去,我会将文件修改的一些存储块拷上去做一些修改,当我有一些大文件想要修改的时候,Device Mapper会比AUFS、OverlayFS好很多。所以AUFS和OverlayFS就比较适合传统的WEB应用,它的文件操作不会很多,但是它可能对我们的应用启动速度会有一些要求,比如我可能经常要发布,我希望能够启动比较快,但是对于文件修改的一些效率我不是很关心,那可以使用基于文件的驱动,当我们是一些计算密集型的应用时候,我们就可以选择Device Mapper,虽然启动比较慢,但是它的运行效率相对表现要好一些。

### 镜像自动化构建

我们构建一个镜像的时候,Docker其实提供了一个标准化的构建指令集,当我们去用这些构建指令去写类似于脚本,这种脚本我们称之为DockerFile,Docker可以自动解析DockerFile,并将其构建成一个镜像,所以你就可以简单的认为这是一个标准化的脚本。DockerFile在做一些什么?首先第一行FROM指令表示要以哪一个镜像作为基础镜像进行构建,我们用了openJDK的官方镜像,以JAVA环境作为基础,我们在镜像上面准备跑一个JAVA应用,然后接下来两条LABLE是对镜像进行打标,标下镜像版本和构建日期,然后接下来的六个RUN是做了一个maven安装,maven是JAVA的一个生命周期管理工具,接下来将一些源代码从外面的环境添加到镜像里面,然后两条RUN命令做了打包工作,最后写了一个启动命令。

总的来说DockerFile写的还可以,至少思路是很清晰的,一步一步从基础镜像选择到编译环境,再把源代码加进去,然后再到最后的构建,启动命令写好,可读性、可维护性都可以,但是还是可以进行优化的。

我们可以减少镜像的层数, Docker对于Docker镜像的层数是有一定要求的,除掉最上面在容器运行时候的读写层以外,我们一个镜像最多只能有127层,如果超过可能会出现问题,所以第二行命令LABLE就可以把它合成一层,减少了层数,下面六个RUN命令做了maven的安装工作,我们也可以把它做成一层,把这些命令串起来,后面的构建我们也可以把它合成一层,这样我们一下就把镜像层数从14层减少到7层,减掉了一半。

我们在做镜像优化的时候,我们希望能够尽量减少镜像的层数,但是和它相对应的是我们DockerFile的可读性,我们需要在这两者之间做折中,我们在保证可读性不受很大影响的情况下去尽量减少它,其实六条RUN命令在做一件事,就是做maven环境打结,做编译环境的准备工作。

接下来我们继续对镜像进行优化,我们可以做一些什么工作呢?在安装maven构建工具的时候我们多加了一行,我们把安装包和展开目录删掉了,我们清理了构建的中间产物,我们要去注意每一个构建指令执行的时候,尽量把垃圾清理掉,我们通过apt-get去装一些软件的时候,我们也可以去做这样的清理工作,就是把这些软件包装完之后就可以把它删掉了,这样可以尽量减少空间,通过增加一行命令,我们可以把镜像的大小从137M削减到119M。

通过apt-get去装软件或者命令基本上是所有编写DockerFile的人都去写的,所以官方已经在debian、Ubuntu的仓库镜像里面默认加了Hack,它会去帮助你在install自动去把源代码删掉。

我们可以利用构建的缓存,Docker构建默认会开启缓存,缓存生效有三个关键点,镜像父层没有发生变化,构建指令不变,添加文件校验和一致。只要一个构建指令满足这三个条件,这一层镜像构建就不会再执行,它会直接利用之前构建的结果,根据构建缓存特性我们可以加一行RUN,这里是以JAVA应用为例,一般一个JAVA应用的pom文件都是描述JAVA的一些依赖,而在我们平常的开发过程中这些依赖包发生变化的频率比较低,那么我们就可以把POM加进来,把POM文件依赖全部都准备好,然后再去下源代码,再去做构建工作,只要我们没有把缓存关掉,我们每次构建的时候就不需要重新下安装包,这样可以节省大量时间,也可以节省一些网络流量。

现在阿里云的容器镜像服务其实已经提供了构建功能,我们在统计用户失败案例的时候就会发现,网络原因导致的失败占90%,比如如果用户通过node开发NPM在安装一些软件包的时候经常卡在中间。所以我们建议加一个软件源,我们把阿里云maven地址加到里面去,我们把配置项加到阿里云的软件地址,加阿里云的maven源作为软件包的下载目标,时间直接少了40%,这样对一个镜像构建的成功率也是有帮助的。

### 多阶段构建

DockerFile最终需要做到的产物其实是JAVA应用,我们对于构建、编译、打包或者安装这些事情都不关心,我们要的其实是最后的产物。所以,我们可以采取分步的方式去做镜像构建,首先我们将之前遇到的所有问题全部都做成基础镜像,上面FROM镜像其实已经改了新的,镜像里面已经把软件源的地址改成了Maven,缓存都已经做好了。我们会去利用缓存,然后添加源代码,我们把前面构建的事情做成了镜像,让镜像去完成构建,然后我们才会去完成把JAVA包拷进去,启动工作,但是两个DockerFile其实是两个镜像,所以我们需要一段脚本去辅助它,第一行的shell脚本是做第一个构建指令,我们指定以Bulid的DockerFile去启动构建,然后生成一个APP Bulid镜像,接下来两行脚本是把镜像生成出来,把里面的构建产物拷出来,然后我们再去做构建,最后把我们需要的JAVA应用给构建出来,这样我们的DockerFile相比之前就更加清晰了,而且分步很简单。

Docker在17.05之后官方支持了多阶段构建,我们把下面的脚本去掉了,我们不需要一段辅助脚本,我们只需要在后面申明基础镜像的地方标记,我们第一阶段的构建产物名字叫什么,我们就可以在第二个构建阶段里面用第一个构建阶段的产物。比如我们第一阶段把JAVA应用构建好,把Maven包里面的target下面的JAVA架包拷到新的镜像里面,然后在所有优化做完之后效果如图,我们在第一次构建的时候,优化前102秒,在Docker构建优化后只花55秒就完成了,主要优化在网络上面。当我们修改了JAVA文件重新进行构建,第二次构建花了86秒,因为Maven安装那一块被缓存了,我们利用了构建缓存,所以少掉20多秒,优化后只花了8秒,因为所有的源代码前面的一些软件包下载全部被缓存了,我们直接拉新的镜像,然后依赖没有变,直接进行构建,所以8秒基本上是完整构建时间。

我们再来看一下存储空间上面的优化,第一次构建我们在优化前把镜像打出来有137M,但是在我们整个优化之后,只有81M了,这里的基础镜像由JDK改成JRE,为什么?因为之前我们把所有流程都放在一个镜像里面时,我们是需要去做构建的,构建时需要去RUN Maven,这种情况下没有JDK环境是RUN不起来的,但是如果我们分阶段,把构建交给Maven镜像来做,把真正运行交给新的镜像来做,就没必要用JDK了,我们直接用JRE,优化之后镜像少了将近50%。当我们修改源代码重新进行构建的时候,由于镜像成共享的原因,第二次构建在优化前其实多加了两层到三层,一共有9M,但是优化后的第二次构建只增加1.93KB,这样我们针对DockerFile的优化就已经做完了。

镜像优化有哪些重要的点呢? 具体如下:

  • 减少镜像的层数,尽量把一些功能上面统一的命令合到一起来做;
  • 注意清理镜像构建的中间产物,比如一些安装包在装完之后就把它删掉;
  • 注意优化网络请求,我们去用一些镜像源,去用一些网络比较好的开源站点,这样可以节约时间、减少失败率;
  • 尽量去用构建缓存,我们尽量把一些不变的东西或者变的比较少的东西放在前面,因为不变的东西都是可以被缓存的;
  • 多阶段进行镜像构建,将我们镜像制作的目的做一个明确,把我们的构建和真正的一些产物做分离,构建就用构建的镜像去做,最终产物就打最终产物的镜像。

容器镜像服务

最后介绍一下阿里云容器镜像服务。这个服务已经公测一年了,现在我们的服务公测是全部免费的。

现在在全球的12个Region都已经部署了我们的服务,每个Region其实都有内网服务和VPC网络服务,如果ECS也在同样的Region,那么它的服务是非常快的。

同时,团队管理和组织帐号功能也已经上线了,镜像购建和镜像消息通知其实都是一些DevOps能力;针对一些镜像优化我们提供了一些镜像层信息浏览功能,我们后续也会提供分析,推出镜像安全扫描、镜像同步。

相关实践学习
基于函数计算快速搭建Hexo博客系统
本场景介绍如何使用阿里云函数计算服务命令行工具快速搭建一个Hexo博客。
目录
相关文章
|
1月前
|
前端开发 关系型数据库 MySQL
IDEA集成Docker插件打包服务镜像与运行【附Docker命令汇总】
IDEA集成Docker插件打包服务镜像与运行【附Docker命令汇总】
|
10天前
|
应用服务中间件 Docker 容器
docker 镜像常用命令
docker 镜像常用命令
30 0
|
10天前
|
Linux Shell 虚拟化
linux 部署docker容器虚拟化平台(二)--------docker 镜像制作方法
linux 部署docker容器虚拟化平台(二)--------docker 镜像制作方法
14 0
|
17天前
|
存储 Kubernetes API
Docker拉取镜像或者kubectl出现的这个解决方案x509: certificate signed by unknown authority
Docker拉取镜像或者kubectl出现的这个解决方案x509: certificate signed by unknown authority
48 2
|
18天前
|
Linux Docker 容器
Linux彻底卸载Docker包括运行拉取的镜像
Linux彻底卸载Docker包括运行拉取的镜像
22 1
|
20天前
|
运维 Kubernetes 持续交付
构建高效自动化运维体系:基于Docker和Kubernetes的最佳实践
在现代云计算环境中,自动化运维成为保障系统稳定性与提升效率的关键。本文深入探讨了如何利用Docker容器化技术和Kubernetes容器编排工具构建一个高效、可靠的自动化运维体系。文中不仅介绍了相关的技术原理,还结合具体案例分析了实施过程中的常见问题及解决方案,为读者提供了一套行之有效的最佳实践指南。
|
22天前
|
NoSQL 关系型数据库 MySQL
安装Docker&镜像容器操作&使用Docker安装部署MySQL,Redis,RabbitMQ,Nacos,Seata,Minio
安装Docker&镜像容器操作&使用Docker安装部署MySQL,Redis,RabbitMQ,Nacos,Seata,Minio
99 1
|
23天前
|
Docker 容器
docker删除镜像
docker删除镜像
51 0
|
1月前
|
网络安全 Docker 容器
docker 拷贝本地镜像
【2月更文挑战第27天】
|
1月前
|
SQL 关系型数据库 数据库
docker如何进入镜像其他的SQL
【2月更文挑战第25天】