论文笔记《Wharf_Sharing_Docker Images_in_a_Distributed_File_System》

《论文笔记》:Wharf: Sharing Docker Images in a Distributed File System

会议:SoCC’ 18

原文:https://doi.org/10.1145/3267809.3267836

梗概

Wharf:通过分布式文件系统进行 Docker 镜像文件的共享,而不是一个物理机一个镜像文件。 Wharf将Docker的运行时状态划分为本地和全局部分,并有效地同步对全局状态的访问。

思维导图

论文笔记《Wharf_Sharing_Docker Images_in_a_Distributed_File_System》_第1张图片

1. 介绍

四个问题

现状:在Docker中,每个守护进程都是不共享的,也就是说,它在本地存储中提取和存储图像,然后从本地副本启动容器。

主要针对目前的 Docker 守护进程的管理方式的现状提出4个主要问题

  1. 如果容器从不同节点上的相同镜像启动,则相同的镜像会存在于所有节点上。这就浪费了储存空间;
  2. 大规模节点同时拉取同一镜像,浪费网络带宽并且拖延容器启动时间
  3. 研究表明容器启动只需要6.4%的镜像数据,但每个节点均拉取完整镜像,这进一步浪费了存储空间和网络带宽
  4. 有一些集群仅提供共享存储,而计算节点无磁盘,这限制了容器的使用

三个贡献

  1. 分析了将Docker存储模型移植到分布式存储中的意义
  2. 为Docker设计了一个共享的镜像存储库,可以在分布式文件系统中存储镜像
  3. 文章实现了 Wharf,并应用了细粒度的层锁,这是一种利用Docker图像分层结构的同步机制,以减少同步开销

wharf 的特点与优势

分布式存储

Wharf使分布式Docker守护进程能够协同检索和存储共享存储中的容器映像,并从共享映像创建容器。通过在中央存储中为所有守护进程只保留一个映像的副本,Wharf可以显著降低网络和存储开销。

分层拉取,层锁

Wharf利用Docker图像的结构来降低同步开销。图像由几个层组成,可以并行下载。Wharf实现了一个细粒度的层锁来协调对共享映像存储的访问。这允许守护进程并行地拉取同一映像的不同层和不同的映像,从而提高网络利用率并避免过度阻塞。Wharf将每个守护进程的数据和元数据分割为全局和本地状态,以最小化必要的同步。另外,使用层锁,Wharf确保每个守护进程只能看到一致的层状态,并防止在单个守护进程失败时整个集群失败。

用户透明和兼容性

Wharf被设计为对用户透明,并允许重用现有的Docker图像而不做任何更改。Wharf独立于分布式存储层,可以部署在任何存储系统上,提供简易的语义。Wharf目前支持两种常见的Docker即写即拷存储驱动程序AUFS和overlay2。此外,虽然我们为Docker实现了Wharf,但所提出的设计也适用于其他容器管理框架。

2. Docker 背景知识

这里仅提取一些关键点

层是一组文件,表示容器文件系统树的一部分。

Docker 容器通过hash 在本地实现了共享层缓存,这节省了存储空间并提高了容器的启动时间。

Docker push 时将镜像分层并压缩归档为 tar,然后附带一份 manifest 文件用于 pull 的时候的镜像检索

2.3 容器创建和图驱动

当创建一个新容器时,Docker 守护进程的创建步骤:

  1. 首先检查所需的镜像是否已经在本地可用
  2. 如果没有,则从注册表中检索清单,通过首先读取清单,然后下载并提取对应的层到本地存储,
  3. 如果守护进程检测到其中一个层已经在本地存在,它将不会下载该特定层
  4. 默认情况下,守护进程并行下载三层。

COW(写时复制):

检索映像之后,守护进程将创建容器。因此,它首先在一个挂载点上联合只读层,然后在其上创建一个可写层。对容器根文件系统所做的任何更改都通过写时复制(COW)机制存储在可写层中。这意味着,在从只读层修改文件之前,将其复制到可写层,并对该副本进行所有更改。一旦容器退出,可写层通常被丢弃。

Docker 的图形驱动

具体的创建联合挂载点与COW能力取决于 Docker 的图形驱动程序;目前应用最广泛的为层叠驱动和专用驱动

层叠驱动:如 AUFS,这些文件系统位于现有文件系统之上,并拦截文件和目录上的操作。在使用时,多个只读文件系统合并到一个逻辑文件系统中,任何更改都通过COW写入到一个可写文件系统中。

专用驱动:专用驱动程序采用相同的COW原理,但使用文件系统或块设备的特殊本机功能来实现层模型和COW。 这些驱动程序需要特殊的块设备,或者需要使用专用文件系统格式化的块设备。

3. Docker 分布式存储

3.1 分布式原生解决方式

将Docker 进行分布式分区或多节点,有三个缺点:

  1. 它不能解决大规模工作负载下的冗余pull问题。每个守护进程仍然必须分别提取一个映像并将其存储在其分区中。这会导致网络和存储I/O开销,增加容器启动延迟;
  2. 它过度利用存储空间,因为必须为每个守护进程存储映像的副本。由于镜像垃圾收集仍然具有挑战性,对于Docker用户来说,由于未使用的图像/容器未被删除而耗尽磁盘空间是一个常见的问题。这会迅速导致存储耗尽
  3. 由于守护进程和分布式存储之间的额外数据传输,与Docker在本地存储上的延时相比,镜像 pull延时会高得多——在实验中长达4倍

3.2 设计目标

五个目标:

  1. 避免冗余:分布式存储与本地单机存储行为表现一致,镜像文件单例化;镜像只存取一次,并且当缺少镜像时,也仅下载一次
  2. 协同合作: 多个守护进程可以并行地拉取图像,以提高拉取速度,因为有更多的资源可用。此外,如果其他守护进程仍在使用映像,则需要协调以防止单个守护进程删除映像。
  3. 高效同步:当多个守护进程访问相同的存储时,需要用锁和同步防止竞态。为了最小化对容器启动时间的影响,同步应该是轻量级的,利用Docker的镜像分层,将锁和同步在层粒度上使用,而不是在镜像文件粒度上。
  4. 避免远程访问:由于数据现在是从共享存储层远程访问的,读/写操作会导致额外的延迟。在最坏的情况下,当连接由于网络故障而下降时,整个容器可能会停止工作,直到连接恢复。因此,必须减少远程访问的数量。
  5. 容错:即使一个或几个守护进程失败,系统作为一个整体也必须保持可运行状态,即一个失败的守护进程不应该破坏全局状态,挂起的操作应该由其余的守护进程完成。

4. Wharf

4.1 系统架构

Wharf的核心思想是将图形驱动程序内容分解为全局状态和局部状态,并同步访问全局状态。

全局状态

包含跨守护进程访问的数据,如:镜像和层数据,以及像层和镜像 manifest 的静态元数据

它还包括运行时状态,例如,当前传输层的进度,运行的容器以及拉取的图像和层之间的关系

全局状态存放在分布式文件系统里,可以被所有守护进程访问

局部状态

局部状态与在单个守护进程下运行的容器相关,比如网络配置、附加卷的信息和容器插件,比如Nvidia GPU插件,它简化了部署支持GPU的容器的过程。

该数据只需要由其对应的守护进程访问,因此,每个守护进程分别存储该数据。

局部状态既可以存储在守护进程的本地存储中,也可以存储在分布式文件系统中的单独位置上。

这样就保证了和镜像有关的信息只存储一次并解决了目标(1)

wharf 架构设计

架构图如下:

论文笔记《Wharf_Sharing_Docker Images_in_a_Distributed_File_System》_第2张图片

包含三个主要组件:wharf 守护进程,镜像管理接口,数据与元数据共享存储

wharf 守护进程:Wharf Daemon

wharf 守护进程运行在独立的Docker主机上,并为它们的本地容器管理它们自己的本地状态。

当守护进程收到创建容器的请求时,它设置容器根文件系统,这样可写层存储在一个私有的、非共享的位置,只读层从分布式文件系统读取。

守护进程通过更新分布式文件系统上的全局状态来交换信息,但彼此之间并不直接通信。(类似于共享内存通信,这里是共享一个全局文件

镜像管理接口:Distributed Image Managment Interface

更像是一个网关,是各本地守护进程访问全局状态的网关

作用是当某个守护进程更新全局状态时,通过锁定部分全局状态临界区,来确保状态同步

镜像管理接口提供了实现分布式锁的不同方式,如:zookeeper 或 etcd;如果底层文件系统支持,甚至会使用底层文件锁定接口fcntl()进行同步,该方法不需要额外服务,具有高度可移植性

共享存储:Shared Storage

托管全局状态,并分为两个部分

  • 共享内容存储(Shared Storage):存储只读层数据和根文件系统
  • 共享元数据存储(Shared Metadata Store):保存层和镜像的元数据;每份元数据存储可以被单独锁定,元数据存储可以多读但只能单写

守护进程要从共享内容存储访问某个层时,都需要首先读取共享元数据存储并检查该层的状态,即该层是否已经存在、当前是否被拉出,或者是否被正在运行的容器引用。

由于元数据的写操作是被保护的,故守护进程可以被保证读取到共享内容存储的一致性视图

4.2 层级别锁

为了满足目标(2)和(3),Wharf 在镜像管理接口中实现了层级锁;层级锁保证写操作只需写单层,而不需要写或添加整个文件,读操作不需要同步,这在并发操作上意味着守护进程可以:

  1. 并发拉取一个镜像的不同层
  2. 只锁定共享存储的一小部分, 并行操作不受影响
  3. 不会在同一时间拉取和删除同一层
  4. 可以并发读

下图为层锁情况下的并发拉取镜像,在 Docker 中,默认每个守护进程有3个线程进行并发拉取镜像

注意:下图以及文中说明Wharf守护进程的工作方式,引入了一些额外的数据结构如线程队列、map等以及不同情况下对应的处理逻辑,拉取流程部分自我感觉有点晦涩难懂

论文笔记《Wharf_Sharing_Docker Images_in_a_Distributed_File_System》_第3张图片

本地守护进程数据结构:

  • 本地运行线程队列:Local Running Transfers(LRT)
  • 本地等待线程队列:Local Waiting Transfers(LWT)
  • 工作者:Master/Dummy Transfer(M/D T)
  • 观察者,报告进展:Watcher

共享传输存储数据结构:

  • 全局完成传输映射:Global Complete Transfers(GCT)
  • 全局运行传输映射:Global Running Transfers(GRT)
  • 全局等待传输映射:Global Waiting Transfers(GWT)

Wharf 守护进程,拉取层的流程如下:

  1. 当守护进程收到一个层拉取请求时,它将创建一个MT,并在一个本地映射数据结构(LRT)中注册它。
  2. 如果所有的拉取线程都很忙,守护进程将把请求添加到 LWT 队列中,以便稍后进行处理直到线程可用。
  3. 如果另一个客户端试图拉出一个已经正被守护进程拉出的层,守护进程将创建一个观察者Watcher 向客户端报告拉出的进展。

Wharf 除了协调本地多线程还需要协调多远程进程调用。因此在全局存储中定义了三个额外的映射数据结构:GCT、GRT、GWT;全局写操作被锁保护

  1. 当 Wharf 守护进程尝试拉取层时,会首先检查 GRT map 查看是否请求的层正在被其他进程拉取
  2. 如果没有,将在守护进程中创建 MT 工作者并将该任务添加到 LRT 和 GRT 中
  3. 如果有,则将任务添加到 LWT 和 GWT。一旦守护进程开始检索相应的层,它们就负责从 GWT 中删除等待的传输。

我理解为上面的操作为分布式守护进程,下面的操作为本地多个守护进程

  1. 如果守护进程发现层已经被其他进程拉取了,则会创建一个 DT 工作者,并将任务添加入 LRT 中,但是 DT 工作者只是一个仿真占位符提醒客户端正在拉取层但是实际上不占用线程工作;
  2. 每个守护进程后台都使用一个线程进行定期检查,检查 MT 拉取的对应的 DT 的层是否已完成。因此,DT 将轮询 GCT 查看对应的已完成的 MT 工作

对于删除操作,Wharf 为每层使用全局引用计数,只有当全局引用计数为0时,才可以删除

4.3 本地暂时写操作

Docker 原生COW

在典型的Docker部署中,容器的根文件系统是短暂的,也就是说,当容器停止运行时,写入文件系统的更改将被丢弃。这转化为底层存储驱动程序移除可写层。

用户应用程序经常在根文件系统中创建大量的中间数据,这会使可写层膨胀。此外,覆盖存储驱动程序,如overlay2和aufs,会将整个文件复制到可写层,即使只有文件的一小部分被修改。与用户写操作相比,这将导致文件系统级的写扩展。

Docker通常将可写和可读层存储在同一文件系统中。 在分布式文件系统的情况下,这会转换为跨存储网络传输的大量临时容器数据,从而增加了网络和远程存储服务器的开销。

Wharf 可写层设置

与Docker不同,Wharf在两个不同的位置存储可写和可读的层。 具体来说,Wharf将运行容器的可写层放在本地连接的存储中,而将只读层存储在共享存储中,以便任何守护程序都可以访问它。 该设计通过减少对分布式文件系统的写入量来解决目标(4)。

4.4 一致性与容错

对于全局数据和元数据而言,读可以并行操作,并且不允许进行本地元数据缓存。排他写结合不缓存状态提供了跨守护进程全局状态的必要的强一致性,并确保守护进程总是对相同的元数据视图进行操作。

一致性

若守护进程需要更新镜像数据,如拉取新镜像或层,则会遵循如下三个阶段:

  1. 进入元数据阶段:锁定全局状态中的必要部分并注册其行为;如,若没有其他守护进程正在做同样的操作,则拉取层l1,然后释放锁
  2. 数据阶段:接着检索实际数据
  3. 再次进入元数据阶段:若操作成功,则再次获取必要的元数据锁并更新元数据。

由于数据阶段总是在元数据阶段之前,因此没有两个守护进程可以执行相同的动作两次。

疑问:这里的更新流程在字面意思的理解上感觉有锁的bug,类似于 mysql 的读未提交?

根据上述的字面意思,对于两个守护进程 A 和 B:

阶段\进程 A B
t1 获取元数据锁
锁定部分全局状态
注册行为:拉取对应层
释放锁
t2 检索实际数据 获取元数据锁
锁定部分全局状态
注册行为
释放锁
t3 获取锁
更新元数据
释放锁
检索实际数据
t4 获取锁
更新元数据
释放锁

t3 更新了的元数据难道不会被t4的更新覆盖掉吗?因为也没开源,或许是字面意思理解错误或翻译错误,此处不再深究

容错

Wharf 需要处理两种类型的错误

  1. 当守护进程持有锁的时候崩溃了,整个系统将会因此而停止运转;对应解决方案是使用锁超时机制:在锁超时后将会自动释放,由于守护进程只需要锁住访问元数据,锁时长一般会十分短暂,所以超时机制可行并可设置为1s等
  2. 当守护进程拉取层时崩溃,将永远不能完成拉取操作;对应解决方案是使用心跳机制:守护进程周期性发送最近一次的心跳时间戳,并存储在 CRT map中,这允许其他进程进行检查,若存在崩溃,则使用新的进程继续上次工作

5. Wharf 实现

在 Docker 的基础上,添加 2800行代码

要运行Wharf,用户只需运行带有共享根参数集的Docker守护进程。

5.1 共享状态

Wharf主要解决状态上的两个问题:全局分布式访问同步和本地内存缓存状态

原生 Docker 守护进程使用三个主要结构体存储全局状态:

  • LayerStore:用于存取可用层的信息(只读和可写层的列表)
  • ImageStore:用于本地镜像信息的存取(存储后端和镜像元数据)
  • ReferenceStore:包括对所有可用镜像的引用

原生Docker:每个守护进程都在内存中保存这些数据结构的副本,并可以通过重新加载负责保存守护进程持久状态的目录来生成它们。为了容纳多个并发客户端,Docker守护进程通过互斥锁保护它的内存状态。

Wharf:通过序列化扩展原生实现,将它们存储在共享存储中,并使用分布式读写锁定机制来同步访问。读可并发,但写访问则需要一个占用局部状态资源的独占锁。

分布式读写锁实现

默认情况下,Wharf使用fcntl系统调用来实现锁定。由于大多数分布式文件系统都支持fcntl,该方法无需额外的软件和库依赖即可使用。通过Wharf的镜像管理界面,允许使用第三方内存键值对存储组件来替换默认的基于文件的锁定。

全局状态与局部状态的同步

由于多个守护进程现在可以更新全局状态,单个守护进程的内存状态可能无效。因此,在读取局部内存状态数据之前,守护进程必须检查全局状态是否已经更新。

为了确保守护进程在处理任何操作之前总是拥有最新版本的元数据,Wharf应用了一个延迟更新机制,它只在操作需要元数据访问时更新单个守护进程的缓存。Wharf通过从分布式存储中反序列化二进制文件来更新单个守护进程的内存数据结构,并将检索到的数据写入其内存数据。

5.2 并发镜像检索

Wharf 通过新增两个组件,对 Docker 的层级传输者(layer transfer)进行拓展去实现多进程并发拉取层

  • SharedTransferManager:是Docker 的 TransferManager 的分布式版本
  • SharedTransferStore:是全局状态的一部分,也序列化到共享存储

下图是当多个 Wharf 进程同时拉取相同镜像时,对于 Transfer 的镜像拉取行为的表现流程图

论文笔记《Wharf_Sharing_Docker Images_in_a_Distributed_File_System》_第4张图片

起点是Start Transfer,终点是Transfer Done,当一次Transfer 完成之后,又会立即去 Waiting Transfer 中查看等待中的任务,故会无限循环下去

每个守护进程以拉取镜像清单和提取层信息开始

若本地其他线程已经拉取层了,守护进程将创建一个 watcher 观察者去监控传输进度;否则查看是否被其他不同节点拉取,该信息可以在SharedTransferStore中被提取,若确实被拉取,则会创建一个dummy transfer去监控master transfer

一旦传输完成,守护进程会更新SharedTransferStore,由于该结构体被共享访问,所以dummy transfers能获取到相应资源的进度

5.3 图形驱动和文件系统

问题:Docker 原生支持两种层叠驱动:aufsoverlay2,然而,在分布式文件系统上使用overlay2驱动并不常见

Wharf 选择采用 NFS 和 IBM Spectrum Scale 联合作为分布式文件系统;但这其中又有其他问题出现

NFS

Wharf可以与NFS以及aufs和overlay2图形驱动程序一起使用

由于在NFS文件上设置了system.nfs4_acl扩展属性,在使用NFSv4和overlay2时会存在问题;OverlayFS无法将扩展属性复制到上层文件系统(可写层),即存储已更改文件的文件系统(本例中为ext4)

论文中认为,这是由于NFSv4 ACL与上层文件系统的ACL之间不兼容所致。 虽然可以使用noacl选项挂载NFSv4,但我们发现Linux发行版不支持此功能。

Spectrum Scale

还可以在IBM Spectrum Scale并行文件系统之上运行Wharf,但同样遇到了问题。

当aufs驱动程序正常工作时,使用overlay2 会抛出问题。

即使配置了对ext4的本地写入,Docker仍尝试在Spectrum Scale上为OverlayFS创建上层文件系统。 这会导致错误(“filesystem on ’/path’ not supported as upperdir”)。

论文目前不知道此行为的确切原因,因为能够在Spectrum Scale上手动创建OverlayFS挂载点,故计划将来做调查解决。

由于OverlayFS是主线Linux内核的一部分,因此提供了更好的可移植性,论文将overlay2和NFSv3结合在一起用于我们对Wharf的实验。

6. 评估

6.1 拉取层和网络负载

Benchmark 测试部分,将 Wharf 和其他两种策略作对比:

  1. DockerLocal,使用本地磁盘去存储容器镜像
  2. DockerNFS,使用NFS 作为后端存储但用分别的文件夹存储每个守护进程的数据

测试维度采用五个变量,并控制变量:

  • 层数
  • 每层大小
  • 每层文件数
  • 网络带宽
  • 守护进程数量

论文中的实验结果表明 Wharf 性能与带宽变化和节点数量没有太大关系,性能稳定;

由于Wharf最小化了到注册表的网络流量,它提供了稳定的性能,并且不受注册表连接带宽变化的影响。

也由于Wharf 只拉取一个镜像一次

6.2 容器运行时性能

虽然从分布式存储共享图像可以减少存储和网络开销,但在Wharf 中容器现在必须远程访问数据。这可能会给工作负载中的单个任务增加额外的延迟。

Wharf 和 本地 Docker 运行做控制变量实验

实验表明运行计算量大的容器任务时,其额外开销是很轻微的;总的来说,两种系统设置的任务执行时间分布相似,因此我们得出结论,使用Wharf不会导致显著的运行时性能下降。

总结

Wharf 从镜像存储形式出发,采用分布式共享存储,从不同维度很巧妙地解决了存储和镜像分发的问题,之前看的 FID 和 阿里 dragonfly 等都是聚焦在 P2P 镜像分发方式;Wharf 聚焦在 Registry 和镜像的存储方式,从而影响分发和运行,这是一个切入点。

你可能感兴趣的:(Docker,文献,镜像存储,Wharf)