admin管理员组

文章数量:829199

Docker镜像

概述

如果曾经做过 VM 管理员,则可以把 Docker 镜像理解为 VM 模板,VM 模板就像停止运行的 VM,而 Docker 镜像就像停止运行的容器;而作为一名研发人员,则可以将镜像理解为类(Class)。
首先需要先从镜像仓库服务中拉取镜像。常见的镜像仓库是Docker Hub,但是也存在其他镜像仓库服务。
拉取操作会将镜像下载到本地 Docker 主机,可以使用该镜像启动一个或者多个容器。
镜像由多个层组成,每层叠加之后,从外部看来就如一个独立的对象。镜像内部是一个精简的操作系统(OS),同时还包含应用运行所必须的文件和依赖包。
因为容器的设计初衷就是快速和小巧,所以镜像通常都比较小。
前面多次提到镜像就像停止运行的容器(类)。实际上,可以停止某个容器的运行,并从中创建新的镜像。
在该前提下,镜像可以理解为一种构建时(build-time)结构,而容器可以理解为一种运行时(run-time)结构,如下图所示。

镜像和容器关系

上图从顶层设计层面展示了镜像和容器间的关系。通常使用docker container run和docker service create命令从某个镜像启动一个或多个容器。
一旦容器从镜像启动后,二者之间就变成了互相依赖的关系,并且在镜像上启动的容器全部停止之前,镜像是无法被删除的。尝试删除镜像而不停止或销毁使用它的容器,会导致出错。

容器通常比较小

容器目的就是运行应用或者服务,这意味着容器的镜像中必须包含应用/服务运行所必须的操作系统和应用文件。
但是,容器又追求快速和小巧,这意味着构建镜像的时候通常需要裁剪掉不必要的部分,保持较小的体积。
例如,Docker 镜像通常不会包含 6 个不同的 Shell 让读者选择——通常 Docker 镜像中只有一个精简的Shell,甚至没有 Shell。
镜像中还不包含内核一一容器都是共享所在Docker主机的内核。所以有时会说容器仅包含必要的操作系统(通常只有操作系统文件和文件系统对象)。

镜像仓库

Docker镜像存储在镜像仓库服务(Image Registry)当中。
Docker客户端的镜像仓库服务是可配置的,默认使用Docker Hub。
镜像仓库服务包含多个镜像仓库(Image Repositroy);镜像仓库中可以包含多个镜像。
下图展示了包含 3 个镜像仓库的镜像仓库服务,其中每个镜像仓库都包含一个或多个镜像。如图:

官方和非官方镜像仓库

Docker Hub 也分为官方仓库(Official Repository)和非官方仓库(Unofficial Repository)。
顾名思义,官方仓库中的镜像是由 Docker 公司审查的。这意味着其中的镜像会及时更新,由高质量的代码构成,这些代码是安全的,有完善的文档和最佳实践。
非官方仓库更像江湖侠客,其中的镜像不一定具备官方仓库的优点,但这并不意味着所有非官方仓库都是不好的!非官方仓库中也有一些很优秀的镜像。
大部分流行的操作系统和应用在 Docker Hub 的官方仓库中都有其对应镜像。这些镜像很容易找到,基本都在 Docker Hub 命名空间的顶层。

阿里的容器镜像服务

介绍这个的目的是,我们日常针对于我们开发的应用来说,如果要同时部署多台服务器,部署一台服务其实是很累的,每台服务器都要从本地打成镜像到服务器上,而别的服务器很多公共的应用都是一样的,这个时候就没有必要一个个的去打成镜像到每台服务器上,我们可以把这些公共的镜像抽取出来由单独的一个容器仓库去管理,只需要一份,其它服务器上的镜像都是第一台服务器上镜像的复制品,这样容错性更大,更方便。
可以购买阿里的容器镜像服务,让本地创建镜像并推送到阿里的仓库。

拉取镜像

Docker主机安装后,本地并没有镜像。

docker image pull xxx

是下载镜像的命令,镜像从远程镜像仓库服务的仓库中下载。
默认情况下,镜像会从Docker Hub的仓库中拉取。

docker image pull alpine:latest

该命令会从Docker Hub的alpine仓库中拉取标签为latest的镜像。

Linux Docker主机本地镜像仓库通常位于:
/var/lib/docker/

查看镜像

docker images 或者 docker image ls

镜像命名和标签

镜像的命名是:名称:tag
从官方仓库拉取镜像时,格式如下:

docker image pull <repository>:<tag>

查看tag的方式可以直接访问默认的docker镜像仓库地址去查找:

例如:

如果不指定tag,默认将拉取最新的,等同于:

docker image pull <repository>:latest

镜像结构解析

可以通过 docker image inspect 命令查看,
例如:
docker image inspect redis:latest

"RootFS": {
"Type": "layers",
"Layers": [
"sha256:d000633a56813933cb0ac5ee3246cf7a4c0205db6290018a169d7cb096581046",
"sha256:bdad86443e47c5665683ac41c1f24f28479d830d7e3cc47d0a337ee5166c7714",
"sha256:6a7992ac480029d82b7dbb757d16fe5d023aa283ed32b52267cd1fe9e6b73c49",
"sha256:be43d2475cf809c0f2ec31950e849d0f888f3121970fd99196a11a903f8c3820",
"sha256:be5818ef2907adfe19be14bf66647b5fb5a2029143f9297f8ce1ff1fd1d35753"
]
}

输出显示该镜像包含 5 个镜像层。只不过输出内容中使用了镜像的 SHA256 散列值来标识镜像层。
其实docker镜像由一些松耦合的只读镜像层组成,如下所示。

镜像层如何产生

Docker镜像都起始于一个基础镜像层,当进行修改或增加新的内容时,就会在当前镜像层之上再创建的镜像层。
举一个简单的例子,假如基于 Ubuntu Linux 16.04 创建一个新的镜像,这就是新镜像的第一层;如果在该镜像中添加 Python 包,就会在基础镜像层之上创建第二个镜像层;如果继续添加一个安全补丁,就会创建第三个镜像层。

在天津额外的镜像层的同时,镜像始终保持是当前所有镜像层的组合,下图例举一个简单的例子,每个镜像层包含3个文件,二镜像包含了来自两个镜像层的6个文件。

下图中展示了一个稍微复杂的三层镜像,在外部看了镜像只有6个文件,这是因为最上层的文件7是文件5的一个更新版本。

这种情况下,上层镜像层中的文件覆盖了底层镜像层中的文件。这样就使得文件的更新版本作为一个新镜像层添加到镜像当中。
Docker通过存储引擎(新版本采用快照机制)的方式来实现镜像层堆栈,并保证多镜像层对外展示为统一的文件系统。
Linux上可以的存储引擎有AUFS、Overlay2、Device Mapper、Btrfs以及ZFS。每种存储引擎都基于Linux中对应的文件系统或者块设备技术,并且每种存储引擎都有其独有的性能特点。

Docker 在 Windows 上仅支持 windowsfilter 一种存储引擎,该引擎基于 NTFS 文件系统之上实现了分层和 CoW。
下图展示了与系统显示相同的三层镜像。所有镜像层堆叠并合并,对外提供统一的视图。

镜像散列值(摘要)

  • 从Docker1.10版本开始,镜像就是一系列松耦合的独立层的集合。
  • 镜像本身就是一个配置对象,其中包含了镜像层的列表以及一些元数据信息。
  • 镜像层才是实际数据存储的地方(比如文件等,镜像层之间是完全独立的,并没有从属某个镜像集合的概念)。
  • 镜像的唯一标识是一个加密ID,即配置对象本身的散列值。每个镜像层也由一个加密ID区分,其值为镜像层本身内容的散列值。
  • 这意味着修改镜像的内容或其中任意的镜像层,都会导致加密散列值的变化。所以,镜像和其镜像层都是不可变的,任何改动都能很轻松的被辨别。
  • 这就是所谓的内容散列(Content Hash)。
    到目前为止,事情都很简单。但是接下来的内容就有点儿复杂了。
  • 在推送和拉取镜像的时候,都会对镜像层进行压缩来节省网络带宽以及仓库二进制存储空间。
  • 但是压缩会改变镜像内容,这意味着镜像的内容散列值在推送或者拉取操作之后,会与镜像内容不相符!这显然是个问题。
    例如,在推送镜像层到 Docker Hub 的时候,Docker Hub 会尝试确认接收到的镜像没有在传输过程中被篡改。
    为了完成校验,Docker Hub 会根据镜像层重新计算散列值,并与原散列值进行比较。
    因为镜像在传输过程中被压缩(发生了改变),所以散列值的校验也会失败。
    为避免该问题,每个镜像层同时会包含一个分发散列值(Distribution Hash)。这是一个压缩版镜像的散列值,当从镜像仓库服务拉取或者推送镜像的时候,其中就包含了分发散列值,该散列值会用于校验拉取的镜像是否被篡改过。
    这个内容寻址存储模型极大地提升了镜像的安全性,因为在拉取和推送操作后提供了一种方式来确保镜像和镜像层数据是一致的。
    该模型也解决了随机生成镜像和镜像层 ID 这种方式可能导致的 ID 冲突问题。

多架构镜像

Docker 最值得称赞的一点就是使用方便。例如,运行一个应用就像拉取镜像并运行容器这么简单。无须担心安装、依赖或者配置的问题。开箱即用。

但是,随着 Docker 的发展,事情开始变得复杂——尤其是在添加了新平台和架构之后,例如 Windows、ARM 以及 s390x。

这是会突然发现,在拉取镜像并运行之前,需要考虑镜像是否与当前运行环境的架构匹配,这破坏了 Docker 的流畅体验。

多架构镜像(Multi-architecture Image)的出现解决了这个问题!

Docker(镜像和镜像仓库服务)规范目前支持多架构镜像。这意味着某个镜像仓库标签(repository:tag)下的镜像可以同时支持 64 位 Linux、PowerPC Linux、64 位 Windows 和 ARM 等多种架构。
简单地说,就是一个镜像标签之下可以支持多个平台和架构。下面通过实操演示该特性。

为了实现这个特性,镜像仓库服务 API 支持两种重要的结构:Manifest 列表(新)和 Manifest。
Manifest 列表是指某个镜像标签支持的架构列表。其支持的每种架构,都有自己的 Mainfest 定义,其中列举了该镜像的构成。
下图使用 Golang 官方镜像作为示例。图左侧是 Manifest 列表,其中包含了该镜像支持的每种架构。

Manifest 列表的每一项都有一个箭头,指向具体的 Manifest,其中包含了镜像配置和镜像层数据。

在具体操作之前,先来了解一下原理。

假设要在 Raspberry Pi(基于 ARM 架构的 Linux)上运行 Docker。

在拉取镜像的时候,Docker 客户端会调用 Docker Hub 镜像仓库服务相应的 API 完成拉取。

如果该镜像有 Mainfest 列表,并且存在 Linux on ARM 这一项,则 Docker Client 就会找到 ARM 架构对应的 Mainfest 并解析出组成该镜像的镜像层加密 ID。
然后从 Docker Hub 二进制存储中拉取每个镜像层。
下面的示例就展示了多架构镜像是如何在拉取官方 Golang 镜像(支持多架构)时工作的,并且通过一个简单的命令展示了 Go 的版本和所在主机的 CPU 架构。
需要注意的是,两个例子都使用相同的命令 docker container run。不需要告知 Docker 具体的镜像版本是 64 位 Linux 还是 64 位 Windows。

示例中只运行了普通的命令,选择当前平台和架构所需的正确镜像版本是有由 Docker 完成的。

64 位 Linux 示例如下。$ docker container run --rm golang go versionUnable to find image 'golang:latest' locally
latest: Pulling from library/golang
723254a2c089: Pull complete
<Snip>
39cd5f38ffb8: Pull complete
Digest: sha256:947826b5b6bc4...
Status: Downloaded newer image for golang:latest
go version go1.9.2 linux/amd6464 位 Windows 示例如下。PS> docker container run --rm golang go versionUsing default tag: latest
latest: Pulling from library/golang
3889bb8d808b: Pull complete
8df8e568af76: Pull complete
9604659e3e8d: Pull complete
9f4a4a55f0a7: Pull complete
6d6da81fc3fd: Pull complete
72f53bd57f2f: Pull complete
6464e79d41fe: Pull complete
dca61726a3b4: Pull complete
9150276e2b90: Pull complete
cd47365a14fb: Pull complete
1783777af4bb: Pull complete
3b8d1834f1d7: Pull complete
7258d77b22dd: Pull complete
Digest: sha256:e2be086d86eeb789...e1b2195d6f40edc4
Status: Downloaded newer image for golang:latest
go version go1.9.2 windows/amd64

前面的操作包括从 Docker Hub 拉取 Golang 镜像,以容器方式启动,执行 go version 命令,并且输出 Go 的版本和主机 OS / CPU 架构信息。

每个示例的最后一行都展示了 go version 命令的输出内容。可以看到两个示例使用了完全相同的命令,但是 Linux 示例中拉取的是 linux/amd64 镜像,而 Windows 示例中拉取的是 windows/amd64 镜像。

所有官方镜像都支持 Manifest 列表。但是,全面支持各种架构的工作仍在推进当中。

创建支持多架构的镜像需要镜像的发布者做更多的工作。同时,某些软件也并非跨平台的。在这个前提下,Manifest 列表是可选的——在没有 Manifest 列表的情况下,镜像仓库服务会返回普通的 Manifest。

删除镜像

当读者不再需要某个镜像的时候,可以通过:

docker image rm

命令从 Docker 主机删除该镜像。其中,rm 是 remove 的缩写。
删除操作会在当前主机上删除该镜像以及相关的镜像层。这意味着无法通过 docker image ls 命令看到删除后的镜像,并且对应的包含镜像层数据的目录会被删除。
但是,如果某个镜像层被多个镜像共享,那只有当全部依赖该镜像层的镜像都被删除后,该镜像层才会被删除。

Docker镜像常用命令总结

docker image pull是下载镜像的命令。镜像从远程镜像仓库服务的仓库中下载。

默认情况下,镜像会从 Docker Hub 的仓库中拉取。

docker image pull alpine:latest命令会从 Docker Hub 的 alpine 仓库中拉取标签为 latest 的镜像。

docker image ls列出了本地 Docker 主机上存储的镜像。可以通过 --digests 参数来查看镜像的 SHA256 签名。

docker image inspect命令非常有用!该命令完美展示了镜像的细节,包括镜像层数据和元数据。

docker image rm用于删除镜像。

docker image rm alpine:latest命令的含义是删除 alpine:latest 镜像。当镜像存在关联的容器,并且容器处于运行(Up)或者停止(Exited)状态时,不允许删除该镜像。

本文标签: Docker镜像