云原生制品那些事(1):容器镜像

题图摄于北京颐和园

(未经授权,请勿转载本公众号文章)

上篇文章和大家说到 Kubernetes 无法根本性移除 Docker的影响,原因是 Docker 发明的镜像格式极具革命性,无可替代。不管 Kubernetes 那边风吹浪打,Docker 我自巍然不动。从本篇开始和大家说说镜像那些事,共分四次连载,从《Harbor权威指南》一书节选的纯技术干货,敬请关注、转发和收藏。

第一篇:容器镜像的结构

第二篇:OCI 镜像规范

第三篇:OCI 制品

第四篇:Registry 的作用原理

《Harbor权威指南》目前当当网低于半价优惠中,点击下图直接购买。

相关活动:

2020云原生生态大会,最值得期待的技术盛会!

容器镜像的结构

容器有不可改变性(immutability)和可移植性(portability)。容器把应用的可执行文件、依赖文件及操作系统文件等打包成镜像,使应用的运行环境固定下来不再变化;同时,镜像可在其他环境下重现同样的运行环境。这些特性给运维和应用的发布带来极大的便利,这要归功于封装应用的镜像。

1.1  镜像的发展

2013 年,Docker推出容器管理工具,同时发布了封装应用的镜像。这是 Docker与之前各种方案的重大区别,也是 Docker 得以胜出和迅速流传的主要原因。可以说,镜像体现了 Docker 容器的核心价值。

2014年,Docker 把其镜像格式归纳和定义为 Docker 镜像规范v1。在这个规范中,镜像的每个层文件(layer)都包含一个存放元数据的 JSON 文件,并且用父ID来指明上一层镜像。这个规范有两个缺点:镜像的 ID 是随机生成的,可近似认为具有唯一性,可以用来标识镜像,但是用相同内容构建出来的层文件的ID并不一样,通过ID无法确认完全相同的层,不利于层的共享;每层都绑定了父层,紧耦合的结构不利于独立存放层文件。(本文来自公众号:亨利笔记)

2016年,Docker 制定了镜像规范v2,并在 Docker 1.10 中实现了这个规范。镜像规范v2分为 Schema 1和 Schema 2。Schema 1主要兼容使用 v1 规范的 Docker 客户端,如 Docker 1.9 及之前的客户端。Schema 2 主要实现了两个功能:支持多体系架构的镜像和可通过内容寻址的镜像,其中最大的改进就是根据内容的SHA256 摘要生成 ID,只要内容相同,ID 就是一样的,可区分相同的层文件(即可内容寻址)。从2017年2月起,镜像规范v1不再被 Registry 支持,用户需要把已有的v1镜像转化为v2镜像才能推送到Registry中。

OCI 在 2017 年 7 月发布了 OCI 镜像规范1.0。因为 Docker v2 的镜像规范已经成为事实上的标准,OCI 镜像规范实质上是以 Docker 镜像规范v2为基础制定的,因此二者在绝大多数情况下是兼容或相似的。

1.2  Docker 镜像的结构

Docker 容器镜像主要包含的内容是应用程序所依赖的根文件系统(rootfs)。这个根文件系统是分层存储的,基础层通常是操作系统的文件,然后在基础层上不断叠加新的层文件,最终将这些层组合起来形成一个完整的镜像。当通过镜像启动容器时,镜像所有的层都转化成容器里的只读(read only)文件系统。同时,容器会额外增加一个读写层,给应用程序运行时读写文件使用。这样的层文件结构可由联合文件系统(UnionFS)实现。(本文来自公众号:亨利笔记)

Docker容器镜像通常由 “docker build” 命令依照 Dockerfile 构建的,Dockerfile 描述了镜像包含的所有内容和配置信息(如启动命令等)。下面是一个简单的Dockerfile 例子:

FROM ubuntu:20.04

RUN apt update && apt install -y python

RUN apt install -y python-numpy

ADD myApp.py /opt/

在这个例子中,容器镜像的基础镜像是操作系统 Ubuntu 20.04,然后安装Python 软件包,再安装 Python 库 NumPy,最后增加应用程序 myApp。在镜像构建完成之后会有4个层文件,如下图所示。

图中的镜像层在容器创建时作为只读文件系统加载到容器中,此外,容器运行时会为每个容器实例都创建一个可读写层,叠加在文件系统的最上层,用于应用读写文件。容器的不可改变性就是通过镜像的镜像层(只读)实现的。另外,无论镜像在哪种环境下启动,始终有相同的镜像层,从而实现了应用的可移植性。

Docker使用分层来管理镜像,有以下好处。(本文来自公众号:亨利笔记)

(1)方便基础层和依赖软件层的共享(如包含操作系统文件、软件包等),不同的镜像可以共享基础层或软件层,在同一台机器上存放公共层的镜像时只需保存一份层文件,可以大大减少文件存储空间。

(2)在构建镜像时,已构建过的层会被保存在缓存中,再次构建时如果下面的层不变,则可以通过构建缓存来缩短构建时间。

(3)因为很多时候同一个应用的镜像更新时变化的只是最上层(应用层),所以分层可以减少同种镜像的分发时间。

(4)分层可以更加方便地跟踪镜像的变化,因为每一层都是和构建命令关联的,所以可以更好地管理镜像的变化历史。

Docker容器的文件系统分层机制主要靠联合文件系统(UnionFS)来实现。联合文件系统保证了文件的堆叠特性,即上层通过增加文件来修改依赖层文件,在保证镜像的只读特性时还能实现容器文件的读写特性。

1.3  Docker镜像的仓库存储结构

Docker 容器镜像的存储分为本地存储和镜像仓库(Registry)存储。其中,本地存储指镜像下载到本地后是如何在本地文件系统中存储的;镜像仓库存储指镜像以什么方式存储在远端的镜像仓库中。

镜像存储的本质还是分层存储,但是本地存储和镜像仓库存储的方式不完全一样,最大的区别是,镜像仓库存储的核心是方便镜像快速上传和拉取,所以镜像存储使用了压缩格式,并且按照镜像层独立压缩和存储,然后使用镜像清单(manifest)包含所有的层,通过镜像摘要(digest)和Tag关联起来;镜像在本地存储的核心是快速加载和启动容器,镜像层存储是非压缩的(即源文件)。另外,容器在启动时需要将镜像层按照顺序堆叠作为容器的运行环境,所以镜像在本地存储中需要使用非压缩形式存放。(本文来自公众号:亨利笔记)

在说明镜像的存储格式之前,先介绍拉取同一个 Docker 镜像时可使用的两种不同命令格式。如下所示,latest 是镜像的 Tag,“sha256:46d659…a3ee9a”是镜像的摘要,在支持 Docker 镜像规范 v2 Schema 2 的镜像仓库中,二者都标识同一个镜像:

$ docker pull debian:latest

$ docker pull \ debian:@sha256:46d659005ca1151087efa997f1039ae45a7bf7a2cbbe2d17d3dcbda632a3ee9a

在镜像仓库上存储容器镜像的简化结构如下图所示,主要由三部分组成:清单文件(manifest)、镜像配置(configuration)和层文件(layers)。上面命令中的镜像摘要就是依据镜像清单文件内容计算 SHA256 哈希值而来的,在镜像清单文件中存放了配置文件的摘要和层文件的摘要,这些摘要都是通过具体的文件内容计算而来的,所以镜像存储也叫作内容寻址。

这样做的好处是,除了可以唯一标识不同的文件,还可以在传输过程中通过摘要做文件校验。在文件下载完成后,计算所下载文件的摘要值,然后与下载时的摘要标识进行对比,如果二者一致,即可判断下载的文件是正确的。需要指出的是,由于文件在镜像仓库端是以压缩形式存放的,所以摘要值也是基于压缩文件计算而来的。

最后简单说明镜像的 Tag 的作用。镜像的 Tag 主要用于对镜像赋予一定的标记,格式是 “<repository>:<Tag>”,可以标识镜像的版本或其他信息,也可以标识一个镜像,如 ubuntu:20.0、centos:latest 等。(本文来自公众号:亨利笔记)

Tag 在镜像仓库中可与镜像清单或者镜像索引关联,多个 Tag 可以对应同一个镜像清单或镜像索引,由镜像仓库维护着它们的映射关系,可参考上图(图中未包含镜像索引)。当客户端拉取镜像时,既可用 Tag,也可用镜像摘要获取同样的镜像。

1.4  Docker镜像的本地存储结构

Docker客户端从镜像仓库拉取一个镜像并存储到本地文件系统的过程大约如下。

(1)向镜像仓库请求镜像的清单文件。

(2)获取镜像ID,查看镜像ID是否在本地存在。

(3)若不存在,则下载配置文件 config,在 config 文件中含有每个层文件未压缩的文件摘要DIFF_ID。

(4)检查层文件是否在本地存在,若不存在,则从镜像仓库中拉取每一层的压缩文件。

(5)拉取时,使用镜像清单中压缩层文件的摘要作为内容寻址下载。

(6)下载完一层的文件后,解压并按照摘要校验。

(7)当所有层文件都拉取完毕时,镜像就下载完成了。

下载镜像后,在本地查看镜像 debian:latest 的信息,结果如下:

$ docker images debian:latest

REPOSITORY        TAG          IMAGE ID          CREATED          SIZE

debian            latest      1b686a95ddbf     2 weeks ago        114MB

在 IMAGE ID(镜像ID)列显示的 1b686a95ddbf 是本地镜像的唯一标识ID,可以在“Docker”命令中使用。这个 ID 和镜像仓库中镜像摘要(sha256:46d659…a3ee9a)的形式类似,但是数值不一样,这是因为该ID是镜像配置文件的摘要,所以和镜像仓库使用的清单文件摘要不同。(本文来自公众号:亨利笔记)

使用配置文件的摘要作为本地镜像的标识,主要是因为本地镜像存放的文件都是非压缩的文件,而镜像仓库存放的是压缩文件,因此层文件在本地和镜像仓库中有不同的摘要值。因为压缩文件的内容会受到压缩算法等因素的影响,所以同样内容的层无法保证压缩后摘要的唯一性,而镜像清单文件包含压缩层文件的摘要(参考上文示例),因此通过镜像清单文件的摘要(即镜像摘要)无法确定镜像的唯一性。配置文件则不同,其中包含的层信息是未压缩的摘要值,因此相同镜像的各层内容必然相同,配置文件的摘要值是唯一确定的。

(未完待续,欢迎点“再看”或转发、分享、收藏)

《Harbor权威指南》目前当当网低于半价优惠中,点击上图直接购买。

要想了解云原生、区块链和人工智能等技术原理,请立即长按以下二维码,关注本公众号亨利笔记 ( henglibiji ),以免错过更新。