理解镜像、容器和存储驱动

来源:互联网 发布:mac office2016机锋 编辑:程序博客网 时间:2024/04/28 10:47

理解镜像、容器和存储驱动

为了更有效地使用存储驱动,你必须理解Docker如何创建和存储镜像。接下来,需要理解容器是如何使用这些镜像的。最后,你需要一个对镜像和容器操作者都需要的技术简介。

 

镜像和图层layers

每一个Docker镜像都参考了一系列的只读层,这些层代表着文件系统的区别。层级是从底层开始,逐一建立组成容器的root文件系统。下面的图显示了Ubuntu镜像有4层:


Docker的存储驱动是负责堆放这些层级并且提供一个统一的视图。

当你创建一个新的容器,你会在底层栈上加入一个新的、稀疏的、可写的层。这个层级通常被称为“container layer”。容器运行中所有的变化----例如写入一个新文件,修改已经存在的文件、删除文件---都是写入到这个层级中。下面的图显示了一个基于Ubuntu的容器。


内容寻址存储

Docker 1.10版本引入了一个新的内容寻址存储模型。这是一个全新的方式在磁盘上定位镜像和数据层。原先版本中,镜像和数据层相互引用,并且使用一个随机生成的UUID存储。在新的模型中被替换成了一个安全的内容哈希。

新的模型改进了安全性,提供了一个内置的方式避免ID冲突,并且在pull、push、load、save操作中保证了数据完整性。它也可以使得不是来自一个bulid的镜像自由引用他们的层级。

下面的图中显示了Docker 1.10版本的更新。


可以看到的是,所有的镜像层级IDs是加密的哈希值,但是镜像ID仍旧是一个随机生成的UUID。

关于新版本有几点需要考虑:

1.       迁移已经有的镜像

2.       镜像和层级文件系统结构

(PS:忽略迁移过程,全部为最新版本)

容器和层级

容器和镜像的主要区别之一是最上层的可写层。容器所有写入/修改操作都会存储在这个可写层中。当一个容器删除时,这个可写层同时也被删除。底层的镜像仍旧保持不变。

因为每个容器都有他自己的稀疏可写层,并且所有容器的变更都会写入到这个层中,这意味着多个容器可以共享一个底层的镜像并且容器有自己的数据。下面的图显示了多个容器使用了同一个镜像。


copy-on-write策略

共享是优化资源的一个很好的方式。人们在日常生活中会本能的做这件事情。例如,两个双胞胎Jane和Joseph参加一个不同老师不同时间代数学课程,他们就可以共享练习本。现在假设Jane有一个作业,需要完成这本书第11页的家庭作业。这时候,Jane抄写了第11页,完成了作业并且上交了他的副本。原先的练习册并没有发生变化并且只有Jane有一份已经改变的11页。

Copy-on-write是一个类型共享和复制的策略。在这个策略中,需要相同数据的容器会共享他们所需的数据,不会每容器都保存一份。在某些时候,如果一个进程需要修改或者写入数据,只有这个时候操作系统才会复制一份数据给这个进程使用。只有这个进程对需要修改的数据副本有访问权限,其他所有进程继续使用原始数据。

Docker的镜像和容器都使用了copy-on-write技术。Cow策略优化了镜像使用的存储空间和容器启动花费的时间。下个章节将会看到镜像和容器是如何利用COW。

共享提升小镜像

这个章节将关注镜像层级和cow技术。所有在同一个主机本地存储镜像和容器层级都被存储驱动管理。在Linux系统通常位于/var/lib/docker/。

当进程pull和push操作时,Dcoker客户端会报告镜像的层级。下面的命令是下载docker镜像的输出:

$ docker pull ubuntu

Using default tag: latest

latest: Pulling from library/ubuntu

43db9dbdcb30: Pull complete

85a9cd1fcca2: Pull complete

c23af8496102: Pull complete

e88c36ca55d8: Pull complete

Digest: sha256:7ce82491d6e35d3aa7458a56e470a821baecee651fba76957111402591d20fc1

Status: Downloaded newer image forubuntu:latest

从这个输出可以看到实际上下载了4个镜像层级。每行都列出了一个镜像层级和他的UUID或者加密的哈希。这4个层级组成了ubuntu镜像。


每个层级都在主机本地存储有自己的目录存放。

在Docker 1.10之前版本,每个层级存放在以镜像层级ID命名的文件夹中。然而,Dokcer 1.10版本之后下载的镜像不会这样存储。例如,下图显示了版本1.9.1从Docker Hub下载一个镜像后,与之对应的主机文件夹情况:

$ docker pull ubuntu:15.04

 

15.04: Pulling from library/ubuntu

47984b517ca9: Pull complete

df6e891a3ea9: Pull complete

e65155041eed: Pull complete

c8be1ac8145a: Pull complete

Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e

Status: Downloaded newer image forubuntu:15.04

 

$ ls /var/lib/docker/aufs/layers

 

47984b517ca9ca0312aced5c9698753ffa964c2015f2a5f18e5efa9848cf30e2

c8be1ac8145a6e59a55667f573883749ad66eaeef92b4df17e5ea1260e2d7356

df6e891a3ea9cdce2a388a2cf1b1711629557454fd120abd5be6d32329a0e0ac

e65155041eed7ec58dea78d90286048055ca75d41ea893c7246e794389ecf203

注意上面四个目录的名称是符合层级ID的。现在对比下在Docker 1.10版本以上的区别。

$ docker pull ubuntu:15.04

15.04: Pulling from library/ubuntu

1ba8ac955b97: Pull complete

f157c4e5ede7: Pull complete

0b7e98f84c4c: Pull complete

a3ed95caeb02: Pull complete

Digest:sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e

Status: Downloaded newer image forubuntu:15.04

 

$ ls /var/lib/docker/aufs/layers/

1d6674ff835b10f76e354806e16b950f91a191d3b471236609ab13a930275e24

5dbb0cbe0148cf447b9464a358c1587be586058d9a4c9ce079320265e2bb94e7

bef7199f2ed8e86fa4ada1309cfad3089e0542fec8894690529e4c04a7ca2d73

ebf814eccfe98f2704660ca1d844e4348db3b5ccc637eb905d4818fbfb00a06a

可以查看到四个目录的名称与层级的ID并不符合。

尽管在版本1.10与之前版本之间存在镜像管理的区别,但是所有版本的Docker都可以允许镜像共享层级。例如,如果你进行pull一个镜像和其他已经下载的镜像共享部分层级,Docker会知道这个信息并且只会下载本地没有的部分。当这个镜像下载完成,这两个镜像会共享同样的镜像层级。

你可以自己举例说明。从刚才下载的ubuntu镜像开始,对他进行操作产生一个新的镜像。其中一个方法是使用Dockerfile和docker build。

下面过程略。

复制使得镜像更有效

在之前的章节你可以了解到容器是一个Docker镜像加上一个稀疏可写的容器层。下面的图显示了一个基于ubuntu镜像的容器。


容器中所有的写操作都存放在稀疏可写的容器层级中。其他层级是只读的镜像层级并且无法修改。这意味着多个容器可以安全地共享一个底层镜像。下面的图显示了多个容器共享一个ubuntu镜像。每个容器都有自己的可写层,但是他们都共享一个ubuntu镜像。

 

当镜像中的一个已经存在的文件被修改了,Dokcer使用存储驱动进行一次copy-on-write操作。具体操作实现细节由存储驱动决定。对于aufs和Overlay存储驱动,copy-on-write操作如下所示:

l   搜索整个镜像层级,查找需要更新的文件。这个进程从最高、最新的层级开始直到最底层。

l   对第一个找到的文件进行一个copy-up操作。Copy-up操作会把文件拷贝到容器自己的稀疏可写层。

l   在稀疏可写层修改这个文件。

Btrfs、ZFS和其他驱动copy-on-write都不相同。你可以在后续的文档中获取到关于这些驱动的具体描述。

写入很多数据的容器相比没有写入多少数据的容器会消耗更多的空间。这是因为多数的写入操作会在容器最高层的稀疏可写层,消耗更多的空间。如果容器需要写入很多数据,你需要考虑使用数据卷。

一个copy-up的操作会导致显著的性能瓶颈。这个瓶颈决定于使用的存储驱动。然而,大的文件、更多的层级和更深的目录树可能会有更多影响。幸运的是,这个操作仅会发生在特定文件第一次被修改的时候。对同一个文件后续的操作不会导致copy-up操作,可以直接在容器层直接操作已经复制的文件。

让我们来看下如果通过changed-ubuntu镜像启动5个容器的情况:


1.在一个Docker主机上,使用dockerrun命令5次:

这个操作会基于changed-ubuntu镜像启动5个容器。随着容器创建,Docker会自动增加一个可写的层级并且分配一个随机的UUID。这会返回给docker run命令。

2.运行docker ps命令确认5和容器都在运行

输出显示5个正在运行的容器,他们共享一个镜像。每个容器的CONTAINER ID是创建时UUID的一部分。

3. 列出本地存储区域的内容:

Docker的copy-on-write策略不仅减少了容器使用的空间,也会减少启动容器的时间。在启动时,Docker仅需要为每个容器创建一个稀疏可写层。下面的图显示了5个容器共享一个只读的镜像。

 

数据卷和存储驱动

当一个镜像被删除后,任何没有写入到数据卷的数据会随着镜像的删除而删除。

一个数据卷是Docker主机文件系统上的一个目录或文件直接挂载到一个容器。数据卷是不受存储启动的控制。对数据的读写会绕开存储驱动,直接在主机层操作。你可以挂载任意数量的数据卷到容器中。多个容器也可以共享一个或者多个数据卷。

下面的图显示了一个Docekr主机运行了两个容器。每个容器有存在自己的寻址空间,位于主机的本地存储/var/lib/docker/…目录。这里也有主机也有一个共享的/data目录作为容器的数据卷,mount到两个容器中。

数据卷主机本地存储的一部分,是不受到存储驱动控制的。当一个容器被删除了,任何存储在数据卷的数据都会持久存放在Docker主机上。

 

 

Summary

Docker的镜像是多个只读的层级组成,并且多个只读的层级是可以被多个镜像共享的,可以节省空间。

Docker容器在启动时,会在镜像上创建一个稀疏可写层,并使用copy-on-write策略进行更新。Cow的实现由存储驱动具体实现,常见的有aufs、device mapper等。

数据卷会跳过存储驱动,不受到存储驱动影响,效率会更好。

 

 

 

 

0 0