DADI: Block-Level Image Service for Agile and Elastic Application Deployment

发表时间: 2020-07 · USENIX ATC 2020 (Alibaba Group)

原文: https://www.usenix.org/system/files/atc20-li-huiba.pdf

  • 文章标题: DADI: 用于敏捷和弹性应用部署的块级镜像服务
  • 作者/机构: Huiba Li, Yifan Yuan, Rui Du, Kai Ma, Lanzheng Liu, and Windsor Hsu, Alibaba Group

A1 主要贡献

本文针对容器因镜像下载和解包过程而导致的启动缓慢问题,提出了一种名为 DADI 的块级镜像服务,旨在实现应用的敏捷和弹性部署。

  • 核心问题:传统的容器启动模型(下载镜像、解包镜像、启动容器)是瀑布式的,在创建或更新大型容器集群时,镜像的下载和解包过程非常缓慢,导致容器启动延迟高,影响了业务的敏捷性和弹性。例如,拉取镜像层占用了约80%的启动时间。

  • 研究目标:最大限度地减少容器的冷启动延迟,以满足现代云计算平台(如无服务器计算)对高弹性部署的需求,并解决在公有云和混合云场景中,由于运行时环境异构(如在Linux主机上运行Windows容器)而导致的部署效率问题。

  • 核心创新点

    1. 基于块的层级镜像模型:本文观察到,层级镜像的优势并不依赖于将镜像层表示为文件变更集。DADI 的核心思想是采用基于块的层级 (block-based layers)。每个镜像层虽然仍对应一组文件变更,但在物理上表示为特定文件系统下块级别的变更集合。
    2. 文件系统和平台无关性:这种设计使得镜像服务与具体的文件系统和平台解耦。镜像服务只负责管理和分发物理镜像,而由主机或容器内的文件系统来解释镜像内容。这增强了应用运行时环境的一致性,并实现了“一次构建,随处运行”。
    3. OverlayBD 模块:DADI 设计并实现了一个名为 Overlay Block Device (OverlayBD) 的核心构件,它为一系列基于块的层级提供了合并视图,功能上类似于联合文件系统(Union File System),但设计更简单。
    4. 一系列优化:基于块级层模型的简洁性,DADI 实现了一系列优化,包括:
      • 细粒度的按需数据传输:实现容器的即时启动。
      • 高效的在线解压:使用 lz4 等高效编解码器。
      • 基于追踪的预取:进一步减少启动延迟。
      • P2P 传输:在大型集群中平衡网络流量,应对突发工作负载。
      • 灵活的文件系统选择:支持 cgroups、QEMU 等多种运行时,并可轻松扩展。
      • 高效的大文件修改:支持跨层块引用。
      • 与容器生态系统的轻松集成
  • 部署与成果:DADI 已在阿里巴巴的生产环境中大规模部署,服务于全球最大的电子商务平台之一。性能测试结果表明,DADI 可以在 4 秒内于 1000 台主机上冷启动 10000 个容器。


图 1: 分层的容器镜像。镜像层(L1, L2)是只读的,由多个容器(C1, C2)共享,而容器层(LC)是私有的可写层。

A3 背景知识/关键Observation/设计原则

2.1 容器镜像

  • 层级化与共享:容器镜像由多个增量层组成,以实现增量分发。每个层本质上是一个包含文件差异(增、删、改)的 tar 包。主机上相同的层只需下载一次,并可被多个容器共享。如图 1 所示,每个容器都有一个专用的可写层(容器层),用于存储其对镜像的私有差异。对镜像中文件的写操作可能会触发写时复制(Copy-on-Write, CoW),将整个文件复制到可写层。

  • 联合文件系统:为了向容器提供根文件系统,容器引擎通常依赖于联合文件系统,如 overlayfs、aufs 等。这些文件系统为物理上存储在不同目录中的各层提供了一个合并视图。此外,容器系统也可以利用逻辑卷管理器(LVM)的精简卷(thin-provisioned volumes),将每个层映射到一个快照。

  • 容器镜像仓库:容器系统有一个标准的用于镜像上传和下载的 Web 服务,称为容器镜像仓库(Container Registry)。它使用基于 HTTP(S) 的协议提供服务,结合层的增量特性,使得容器镜像的分发比虚拟机镜像更为便捷。

2.2 远程镜像

  • 传统分发模型的瓶颈:镜像分发操作会消耗大量网络和文件系统资源,尤其在创建或更新大型容器集群时,很容易使用户/租户的服务容量饱和,导致容器启动延迟很长。镜像层接收后还需解包,这个过程同时是 CPU、内存(用于页面缓存)和 I/O 密集型的,常常会影响主机上其他容器的运行,甚至导致它们停滞。

  • 远程镜像概念的引入:当前容器镜像服务在某种程度上是十年前虚拟机镜像也需要下载到主机上这一模式的倒退。分布式块存储【26. URSA: Hybrid Block Storage for Cloud-Scale Virtual Disks, EuroSys 2019; 28. Blizzard: Fast, Cloud-Scale Block Storage for Cloud-Oblivious Applications, NSDI 2014; 29. Sheepdog: Distributed Storage System for QEMU/KVM, LCA 2010; 38. Ceph: A Scalable, High-Performance Distributed File System, OSDI 2006】已经通过将镜像存储在远程服务器并以细粒度按需方式通过网络获取数据,解决了类似问题。这种模型被称为“远程镜像”,容器领域也有多个研究呼吁采用此模型【18. Making Containers Lazy with Docker and CernVM-FS, Journal of Physics: Conference Series 2018; 21. CRFS: Container Registry Filesystem, Google Inc.】。

  • 远程镜像的合理性:其基本原理是在容器的典型生命周期中,只有部分镜像数据被实际需要,而在启动阶段需要的数据更少。根据【19. Slacker: Fast Distribution with Lazy Docker Containers, FAST 2016】的研究,启动阶段仅使用了镜像的 6.4%。因此,远程镜像通过避免预先暂存整个镜像,节省了大量时间和资源。借助操作系统的数据预取或应用程序的异步数据加载,可以进一步有效减少从远程镜像启动的感知时间。

  • 格式转换的必要性:然而,远程镜像需要对层内容进行随机读访问,而标准的层 tar 包是为顺序读取和解包设计的,不支持随机读取。因此,必须改变其格式。

2.3 基于文件系统的远程镜像

  • 现有方案:CRFS【21. CRFS: Container Registry Filesystem, Google Inc.】是一个可以直接从容器镜像仓库挂载镜像的只读文件系统,它引入了支持随机读取的 Stargz 格式。CernVM-FS【18. Making Containers Lazy with Docker and CernVM-FS, Journal of Physics: Conference Series 2018】则将每层的文件解压并存储在可按需访问的仓库中。CFS【27. CFS: A Distributed File System for Large Scale Container Platforms, SIGMOD 2019】、Wharf【41. Wharf: Sharing Docker Images in a Distributed File System, SoCC 2018】、Slacker【19. Slacker: Fast Distribution with Lazy Docker Containers, FAST 2016】和 Teleport【9. Project Teleport, Microsoft Azure】通过 NFS 或 CIFS/SMB 提供解压后的层文件。

  • 挑战与局限:由于文件系统语义的复杂性,基于文件系统的镜像服务面临多重挑战。例如,跨虚拟化边界传递文件系统会限制性能,I/O 栈涉及多个复杂组件(如 virtio-fs【10. virtio-fs】、FUSE【36. To FUSE or Not to FUSE: Performance of User-Space File Systems, FAST 2017】、overlayfs【8. Overlay Filesystem】),使其健壮性和优化变得困难。与块设备相比,文件系统暴露了更大的攻击面,可能降低公有云的安全性。此外,对于非 POSIX 工作负载(如无服务器应用)和在 Linux 主机上运行 Windows 的场景,POSIX 兼容性成为负担和障碍。Unikernel【5. gVisor; 25. OSv– Optimizing the Operating System for Virtual Machines, USENIX ATC 2014; 39. KylinX: A Dynamic Library Operating System for Simplified and Efficient Cloud Virtualization, USENIX ATC 2018】等应用倾向于使用极简文件系统(如 FAT【4. File Allocation Table】),这些多样化的需求难以被预定义的文件系统满足。同时,当前方案也难以支持 XFS【11. XFS】、Btrfs【2. Btrfs】、ZFS【12. ZFS: The Last Word in Filesystems】等流行文件系统的高级特性,甚至高效支持硬链接和文件修改等标准特性也很困难。

2.4 基于块快照的远程镜像

  • 基本思路:现代块存储【26. URSA: Hybrid Block Storage for Cloud-Scale Virtual Disks, EuroSys 2019; 28. Blizzard: Fast, Cloud-Scale Block Storage for Cloud-Oblivious Applications, NSDI 2014; 29. Sheepdog: Distributed Storage System for QEMU/KVM, LCA 2010; 38. Ceph: A Scalable, High-Performance Distributed File System, OSDI 2006】通常有写时复制快照(snapshot)的概念,这与容器的层(layer)相似。Cider【16. Cider: A Rapid Docker Container Deployment System through Sharing Network Storage, HPCC 2017】和 Slacker【19. Slacker: Fast Distribution with Lazy Docker Containers, FAST 2016】尝试利用这种相似性,将镜像层分别映射到 Ceph 和 VMstore【17. Logical Synchronous Replication in the Tintri VMstore File System, FAST 2018】的快照上。

  • 概念差异:然而,容器镜像层和块存储快照并非等同概念。快照是磁盘的某个时间点视图,其实现往往与特定的块存储系统相关。在许多系统中,快照属于磁盘,磁盘删除后快照也随之删除。而层则指相对于某个状态(可能是另一个镜像的状态)的增量变化,它强调在不同镜像、甚至不同用户间的共享,并有标准格式以便于广泛分发。

2.5 其他相关工作

  • 文件系统变更集:Exo-clones【33. Exo-clones: Better Container Runtime Image Management across the Clouds, HotStorage 2016】通过可导出的文件系统变更集高效地实现卷克隆。DADI 镜像在概念上是带有块级增量的 exo-clones,且不与任何特定文件系统绑定。

  • P2P 下载:多个系统【20. Dragonfly: An Open-source P2P-based Image and File Distribution System, Alibaba Inc.; 22. Introducing Kraken, an Open Source Peerto-Peer Docker Registry, Uber Inc.; 24. FID: A Faster Image Distribution System for Docker Platform, FAS*W 2017; 30. Tupperware: Containerized Deployment at Facebook, 2014; 37. LargeScale Cluster Management at Google with Borg, EuroSys 2015】允许容器主机以 P2P 方式下载镜像层,显著减少了大型环境中的下载时间。VMThunder【40. VMThunder: Fast Provisioning of Large-Scale Virtual Machine Clusters, IEEE Transactions on Parallel and Distributed Systems 2014】采用树状 P2P 覆盖网络为大型虚拟机集群按需提供细粒度数据块。DADI 在其可选的 P2P 子系统中重用了这一思想,并进行了精炼设计和生产级实现。

  • 镜像瘦身:为了减少数据拉取量和启动时间,DockerSlim【6. Minify and Secure Your Docker Containers】通过静态和动态分析生成更小的镜像,只包含核心应用所需的文件。Cntr【35. CNTR: Lightweight OS Containers, USENIX ATC 2018】通过基于 FUSE 的虚拟文件系统允许在罕见情况下动态访问被裁剪的文件,从而改进了这一方法。

  • 容器存储配置:容器镜像的层级特性给存储配置带来了新的复杂性。【34. In Search of the Ideal Storage Configuration for Docker Containers, FAS*W 2017】展示了 Docker 存储配置对性能的影响。

  • 虚拟机镜像:标准的虚拟机镜像格式(如 qcow2, vmdk, vhd)是块级格式,技术上可用于容器。其主要缺点是无层级结构。通过重复使用 QEMU 的 backing-file 特性可以模拟层级,但这会给读操作带来显著的性能开销。此外,如3.1节所述,这些标准虚拟机镜像格式的转换表也比 DADI 所需的要大得多。

A2 方法细节

3 DADI 镜像服务

DADI 的设计目标是成为容器生态系统的一部分的通用解决方案。其核心(3.1-3.4节)是一个继承了容器镜像分层模型的远程镜像设计,并通过遵循 OCI-Artifacts【31. OCI Artifacts, OCI】标准与镜像仓库保持兼容。DADI 独立于传输协议,因此可以插入一个可选的 P2P 传输模块(3.5节)来应对大规模应用。DADI 也独立于底层存储系统,用户可以选择合适的存储系统(如 HDFS、NFS、CIFS 等)来构建一个完全网络化的解决方案。DADI 采用块级接口,这最小化了攻击面,对于虚拟化安全容器而言,这是一个尤为重要的设计点。


图 2: DADI 镜像。DADI 镜像层(L1, L2)由修改过的数据块组成。DADI 使用一个覆盖块设备为每个容器(C1, C2)提供其层的合并视图。

3.1 DADI 镜像

  • DADI 的镜像模型:如图 2 所示,DADI 将一个镜像建模为一个虚拟块设备,其上部署了一个常规文件系统(如 ext4)。在块级别没有文件的概念,文件系统是构建在 DADI 镜像之上的更高层抽象。当一个客户应用读取文件时,请求首先由常规文件系统处理,该系统将请求转换为对虚拟块设备的一个或多个读操作。块读取请求被转发到用户空间的 DADI 模块,然后被翻译成对镜像层的一个或多个随机读操作。

  • DADI 层的定义:DADI 将镜像建模为虚拟块设备,同时保留了分层特性。每个 DADI 层是在文件系统下,对应于该层所添加、修改或删除的文件的一组修改过的数据块的集合。DADI 通过一个覆盖块设备(OverlayBD)模块为容器引擎提供各层的合并视图。本文后续将交替使用“层”和“变更集”(changeset)以使表述更清晰。读写的块大小(粒度)为 512 字节,与真实块设备相似。覆盖变更集的规则很简单:对于任何块,最新的变更生效。在任何层中都未被改变(写入)的块被视为空全零块。

  • 层 blob 格式与索引设计:用户写入的原始数据连同一个指向原始数据的索引构成了层 blob。DADI 的层 blob 格式还包括一个头部和一个尾部。为了减少内存占用并提高部署密度,我们设计了一种基于可变长度段(segment)的索引,如图 3 所示。一个段记录了变更在镜像逻辑块地址(LBA)空间中的起止位置,以及最新数据在层 blob 文件偏移空间中的存储位置。在这种设计中,连续的相邻段可以合并成一个更大的段以减小索引大小。段结构可以记录小至 1 个块的变更,这是块设备的最小写入单位。这避免了写时复制操作,有助于产生一致的写性能。

  • 索引结构与查找:索引是一个按偏移排序的非重叠段数组。根据我们生产环境的统计数据,索引的段数少于 4.5K(详见第 5 节),这仅对应 72KB 的内存。相比之下,QEMU 的 qcow2 镜像格式默认使用 64KB 的固定块大小,并采用基于基数树的索引。QEMU 默认分配数 MB 的内存来缓存其索引的最热部分。

  • 读取时的索引查询:为了实现读取,DADI 在索引中执行范围查找,以确定从 blob 的哪个位置读取。该问题可以正式描述为:给定 LBA 空间中的一组不相交的段,找出要读取的范围内所有的段和“空洞”(holes)。这个问题如图 4 所示。为了提高效率,该算法直接处理可变长度的段,而不将其扩展为固定大小的块。由于索引是有序且只读的,我们简单地使用二分查找进行高效查询,如算法 1 所示。B 树可以实现更高的效率,但鉴于索引在实践中只有几千个条目,我们将此优化作为未来可能的工作。


图 4: 读取 DADI 镜像时的索引查找。查找操作是在一组有序、非重叠的可变长度段上进行范围查询,每个段指向其原始数据在层 blob 中的位置。

3.2 层的合并视图

  • 多层查找的性能问题:当存在多个层时,如果查找过程逐一遍历各层,时间复杂度为 $O(n \cdot \log m)$,其中 $n$ 是层数,$m$ 是每层的平均段数。换言之,成本随 $n$ 线性增加。我们通过在加载索引时预先计算一个合并索引来优化此问题,从而将复杂度降低到 $O(\log M)$,其中 $M$ 是合并索引中的段总数。合并问题如图 5 所示。

  • 索引合并算法:要合并索引,我们将它们放入一个从 1 到 $n$ 索引的数组中,其中 $n$ 是层数,并按基底层靠前的顺序排列。算法 2 展示了合并指定范围索引的递归过程。为了整体合并它们,该算法针对整个镜像范围被调用。我们在最终的合并索引中使用 pos 字段来指示一个段来自哪个层。有了合并索引,随机读操作(pread)可以很容易地实现为算法 3,假设我们有一个表示有序层 blob 的文件对象数组。

  • 生产环境数据分析:我们分析了来自生产环境中 205 个核心应用的 1,664 个 DADI 镜像层,以提取合并索引的大小。统计数据汇总在图 6 中。它们显示索引的段数不超过 4.5K,因此合并索引的算法效率足够高,可以在镜像启动时运行。同时观察到,段的数量与层数不相关。这表明 DADI OverlayBD 的性能不会随着层数的增加而下降。图 7 绘制了单个 CPU 核心上的索引查询吞吐量。观察到,在索引大小为 4.5K 段时,单个 CPU 核心每秒可以执行超过 600 万次索引查询。在 5.4 节中,我们发现 LVM 和 DADI 的 IOPS 最高都接近 120K,这表明 DADI 在索引查找上花费的 CPU 不超过一个核心的 1/50。

输入: 要查找的范围 (offset, length)
end ← offset + length;
i ← index.binary_search_first_not_less(offset);
if i < index.size() then
    delta ← offset - index[i].offset;
    if delta > 0 then // 裁剪并产生第一个段
        s ← index[i];
        s.offset ← offset;
        s.moffset += delta;
        s.length -= delta;
        yield s;
        offset ← s.end();
        i++;
    end
end
while i < index.size() and index[i].offset < end do
    len ← index[i].offset - offset;
    if len > 0 then // 产生一个空洞
        yield Hole(offset, len);
        offset ← index[i].offset;
    end
    s ← index[i]; // 产生下一个段
    s.length ← min(s.length, end - offset);
    yield s;
    offset ← s.end();
    i++;
end
if offset < end then // 产生最后一个空洞
    yield Hole(offset, end - offset); // 修正:原文为 end - begin
end

算法 1: 索引查找。在指定的范围(offset, length)内生成一系列段,其中 i 初始化为索引中不小于 offset 的第一个元素,Hole 是一种特殊的段类型,表示从未被写入的范围。

输入: 索引数组 indices[1..n]; 本次递归的索引数组下标 i; 要合并的范围 (offset, length)
for s in indices[i].lookup(offset, length) do
    if s is NOT a Hole then
        s.pos ← i;
        yield s;
    else if i > 0 then // 忽略一个真正的空洞
        indices_merge(indices, i-1, s.offset, s.length);
    end
end

算法 2: 递归方式的索引合并。

输入: 文件对象数组 blobs[0..n]; 要预读的范围 (offset, length)
for s in merged_index.lookup(offset, length) do
    // s.pos == 0 对应 Hole 段
    // blobs[0] 是一个特殊的虚拟文件对象
    // 在预读时产生全零内容
    blobs[s.pos].pread(s.moffset, s.length); // 修正:原文为 pread(s.offset, s.length),根据图4应为moffset
end

算法 3: 基于合并索引的读取。


图 5: 索引合并。


图 6: 生产环境中应用的索引大小。


图 7: 单个 CPU 核心上的索引性能。

3.3 压缩与在线解压

  • ZFile 格式的设计:标准的压缩文件格式如 gz、bz2、xz 等不支持高效的随机读操作。为了同时支持层 blob 的压缩和远程镜像,DADI 引入了一种新的压缩文件格式,称为 ZFile。ZFile 包含以固定大小的块(chunk)逐个压缩的源文件和一个压缩后的索引。要读取 ZFile 中的某个偏移量,首先查找索引以找到相应压缩块的偏移和长度,然后只解压这些块。ZFile 支持多种高效的压缩算法,包括 lz4、zstd、gzip 等,并可以额外存储一个字典来帮助某些压缩算法实现更高的压缩比和效率。图 8 说明了 ZFile 的格式。

  • ZFile 的索引与查找:存储在 ZFile 中的索引是一个 32 位整数数组,每个整数表示相应压缩块的大小。该索引使用与数据块相同的压缩算法进行压缩。加载到内存后,索引被解压缩并累加成一个 64 位整数数组,表示 ZFile blob 中压缩块的偏移量。转换后,索引查找变成了一个简单的数组寻址操作 offset/chunk_size

  • 性能权衡:由于块的固定大小特性和底层存储设备的对齐特性,ZFile 可能会读取和解压比用户请求更多的数据。解压本身也是相对于传统 I/O 栈的额外成本。然而在实践中,即使在配备高速 NVMe SSD 的服务器上,ZFile 也能改善用户感知的 I/O 性能。对于较慢的存储(如 HDD 或镜像仓库),优势更为明显。这是因为,使用 lz4 压缩算法时,读取较小压缩数据所节省的时间超过了解压数据所花费的时间。详细结果见 5.4 节。

  • 压缩比分析:为了支持在线解压,可以使用快速压缩算法,但这会牺牲一些压缩比。我们在部署中通常使用 lz4。单独压缩原始文件的块也会影响压缩比。因此,DADI 镜像通常比相应的 .tgz 镜像大,但增幅不大。我们分析了生产环境中 205 个核心应用的 blob 大小。图 9 显示了各种格式相对于其 .tar 格式大小的 blob 大小。总的来说,DADI 未压缩格式(.dadi)产生的 blob 比 .tar 大,这归因于镜像文件系统(本例中为 ext4)的开销,但对于大于 10MB 的层,开销通常小于 5%。ZFile blob 往往比其 .tgz 对应物更大。

  • 进一步优化:通过遵循容器镜像的层级模型,DADI 镜像能够共享层。为了进一步节省空间和网络流量,可以在 DADI 镜像的块级别执行去重,然后对唯一的块进行压缩。


图 8: ZFile 格式。


图 9: 相对层 Blob 大小。

3.4 DADI 容器层

  • 可写层的实现:与其他远程镜像系统(如【18. Making Containers Lazy with Docker and CernVM-FS, Journal of Physics: Conference Series 2018; 21. CRFS: Container Registry Filesystem, Google Inc.】)不同,DADI 实现了可写的容器层。这个可写层不仅是构建新镜像层的便捷方式,还提供了一个消除对联合文件系统依赖的选项。我们将可写层基于日志结构化(log-structured)【32. The Design and Implementation of a Log-Structured File System, TOCS 1992】设计,因为这使得 DADI 几乎可以在所有类型的存储系统上运行,包括那些不支持随机写的系统(如 HDFS)。日志结构化的可写层在技术上也是只读层的自然扩展。

  • 可写层的结构与操作:如图 10 所示,可写层由一个用于原始数据的文件和一个用于索引的文件组成。这两个文件都是开放式的,可以随时接受追加操作。当可写层中发生覆盖写时,这些文件会包含垃圾数据和索引记录。当垃圾过多时,DADI 会启动一个后台线程通过将活动数据复制到新文件,然后删除旧文件来回收垃圾。当可写层被提交时,DADI 会将活动数据块和索引记录复制到一个新的层格式文件中,并根据它们的 LBA 进行排序和可能的合并。

  • 读写与 TRIM 操作:可写层的索引在内存中维护为一个红黑树,以高效支持查找、插入和删除。写入时,DADI 在可写层的索引中添加一条新记录。读取时,DADI 首先查找可写层的索引。对于要读取范围内的每个空洞(没有数据写入的段),DADI 会进一步查找底层只读层的合并索引。DADI 通过在可写层中添加一个标记了范围包含全零内容的索引记录来支持 TRIM 操作。


图 10: DADI 的可写层。

3.5 P2P 数据传输

  • P2P 的动机:虽然远程镜像可以大大减少需要传输的镜像数据量,但在某些情况下仍需进一步改进。特别是在我们的生产环境中,一些关键应用部署在数千台服务器上,其镜像层可达几 GB。这些应用的部署给镜像仓库和网络基础设施带来了巨大压力。

  • DADI 的 P2P 方案:为了更好地处理这类大型应用,DADI 在每台主机的本地磁盘上缓存最近使用的数据块。DADI 还提供了以点对点(P2P)方式直接在主机间传输数据的选项。考虑到所有对等节点在启动期间需要大致相同的数据集且顺序相似,DADI 采用了类似于 VMThunder【40. VMThunder: Fast Provisioning of Large-Scale Virtual Machine Clusters, IEEE Transactions on Parallel and Distributed Systems 2014】的树状覆盖拓扑来实现应用层多播,而不是 P2P 下载工具【15. Incentives Build Robustness in BitTorrent, Workshop on Economics of Peer-to-Peer systems 2003; 20. Dragonfly: An Open-source P2P-based Image and File Distribution System, Alibaba Inc.; 22. Introducing Kraken, an Open Source Peerto-Peer Docker Registry, Uber Inc.】中常用的“最稀有优先”策略。

  • P2P 架构组件:如图 11 所示,每个容器主机运行一个名为 DADI-Agent 的 P2P 模块。此外,每个数据中心都有一个名为 DADI-Root 的 P2P 模块,扮演拓扑树的根角色。DADI-Root 负责从镜像仓库获取数据块到本地持久缓存,并管理其所在数据中心内的拓扑树。每个层 blob 都会创建并维护一个单独的树。

  • 数据请求与拓扑构建:每当一个 agent 首次需要从某个 blob 读取数据,或其父节点无响应时,它会向 root 发送一个请求 RPC。root 可以自己服务该请求,也可以选择重新安排拓扑并将请求的 agent 重定向到一个选定的父节点。请求的 agent 被视为加入该树,成为选定父节点的子节点。树中的每个节点(包括根)最多为少数几个直接子节点提供服务。如果请求的数据在父节点的缓存中不存在,请求会向上传递,直到某个父节点在其缓存中拥有该数据。从父节点接收的数据会被添加到子节点的缓存中,因为它很可能很快会被其他子节点或该节点自身需要。

  • 拓扑管理:DADI-Root 管理拓扑结构。它知道每个节点有多少个子节点。当需要将一个节点插入树中时,root 只是在内存中沿树向下遍历,总是选择一个子节点最少的子节点。遍历在第一个直接子节点数少于阈值的节点处停止。该节点成为请求 agent 的选定父节点。当一个节点发现其父节点失败时,它会回到 root,由 root 为其安排另一个父节点。由于 P2P 传输旨在支持容器启动,且此过程通常不会持续很长时间,DADI-Root 会相对较快地使拓扑信息过期,默认为 20 分钟后。

  • 高可用性与容错:DADI-Root 实际上是一个在多台服务器上运行的复制服务,以保证可用性,并为不同集群分开部署。一个 agent 在加入传输拓扑时会随机选择同一集群中的一个 root 服务器。当遇到故障时,它会切换到另一个 root 服务器。镜像仓库往往由多个集群共享,且可能跨越长距离,因此其性能可能不总是很高。为了确保在需要时数据块很可能存在于 root 上,我们在生产环境中每当构建或转换新层时,都会预热 root 服务器的缓存。

  • 数据完整性:为了防止潜在的数据损坏,我们在镜像构建或转换过程中为每个层 blob 创建一个单独的校验和文件。该校验和文件包含层中每个固定大小块的 CRC32 值。由于校验和文件很小,它们会在镜像拉取过程中完整分发到每个相关节点。数据块在到达每个节点时进行验证。


图 11: DADI 的树状 P2P 数据传输。

4 实现与部署

本节讨论 DADI 如何与应用程序和容器引擎接口,以及 DADI 如何在不同的用户场景中部署。

4.1 数据路径

  • 通用数据路径:DADI 通过挂载在虚拟块设备上的文件系统与应用程序连接。DADI 对文件系统的选择是不可知的,因此用户可以选择最适合其需求的文件系统。通过允许在镜像创建时明确捕获对文件系统的依赖,DADI 可以帮助应用程序利用 XFS【11. XFS】、Btrfs【2. Btrfs】、ZFS【12. ZFS: The Last Word in Filesystems】等文件系统的高级特性。

  • cgroups 运行时的数据路径:在 cgroups 运行时的情况下,我们使用一个名为 vrbd 的内部模块来提供虚拟块设备。vrbd 类似于 nbd,但包含了一些改进,使其性能更好并能处理用户空间守护进程的崩溃。如图 12 所示,I/O 请求从应用程序到常规文件系统(如 ext4),然后到虚拟块设备,再到名为 lsmd 的用户空间守护进程。属于已下载层的块的读取请求被定向到存储这些层的本地文件系统。其他读操作被定向到 DADI 的 P2P 代理,该代理维护着最近使用数据块的持久缓存。写和 trim 操作由 lsmd 处理,它将可写层的数据和索引文件写入本地文件系统。

  • 虚拟化运行时的数据路径:我们还为 QEMU 的块设备后端实现了一个驱动程序,以向虚拟化容器导出镜像。如图 13 所示,这种情况下的数据路径在概念上与 cgroups 运行时的相似,只是镜像文件系统和虚拟块设备在客户机上下文中运行,而块驱动程序取代了 lsmd。与其他虚拟机管理程序的集成应该是直接的。也可以将虚拟块设备从主机传递到客户机上下文中。这种方法几乎适用于所有虚拟机管理程序,但会产生稍高的开销。由于块设备接口比文件系统接口更窄、更简单,它向不受信任的客户机容器暴露的攻击面很小。


图 12: cgroups 运行时的 I/O 路径。


图 13: 虚拟化运行时(QEMU 等)的 I/O 路径。

4.2 容器引擎集成

  • 驱动实现:DADI 通过一个 graph driver 与 Docker 集成,这是一个用于从层组合根文件系统的 Docker 插件。DADI 也通过一个 snapshotter 与 containerd 集成,该 snapshotter 提供与 graph driver 类似的功能。我们实现了这些驱动程序来识别现有的和 DADI 的镜像格式。当它们遇到 .tgz 镜像时,会调用现有的驱动程序。当它们遇到 DADI 镜像时,会执行 DADI 特定的操作。

  • 平滑迁移:通过这种方式,容器引擎可以同时支持两种类型的镜像,因此在主机上部署 DADI 不需要驱逐该主机上现有的基于 .tgz 的容器或镜像。这使我们能够使用灰度发布的方法在复杂的生产环境中系统地推广 DADI。

  • “伪”拉取与社区贡献:DADI 目前通过一个由 DADI 特定元数据组成的小型 tarball 文件来伪造镜像拉取过程。这个 tarball 非常小,因此镜像拉取能很快完成。我们正在准备向容器社区提交一份提案,建议对镜像格式表示进行扩展,以实现懒加载镜像拉取,并使引擎能够感知远程镜像。

4.3 镜像构建

  • 构建速度优势:DADI 通过提供一个日志结构化的可写层来支持镜像构建。日志结构化设计将所有写操作转换为顺序写,因此使用 DADI 的构建过程通常比常规 .tgz 镜像要快(详见第 5 节)。由于 DADI 使用更快的压缩算法,使用 DADI 的提交操作比常规 .tgz 镜像更快。DADI 还避免了拉取整个基础镜像,这在专用的镜像构建服务器上节省了时间,因为在这些服务器上基础镜像通常不是本地存在的。

  • 构建过程优化:为了构建一个新层,DADI 首先通过启动一个虚拟块设备并在其上挂载文件系统来准备基础镜像文件系统。当层被提交时,DADI 卸载文件系统并关闭设备。对于新镜像中产生的每个层,这些操作都会重复,增加了大量时间。我们通过只启动和关闭设备一次来优化 DADI 镜像构建过程。中间的关闭和启动被一个名为 stack-and-commit 的定制操作所取代。stack-and-commit 首先在现有层之上堆叠一个新的可写层,然后在后台提交原始的可写层。这一优化显著提高了镜像构建速度,尤其是在资源充足的高端服务器上。

  • 格式转换与特殊层处理:为了将现有的 .tgz 镜像转换为 DADI 格式,DADI 从镜像的最低层处理到最高层。对于每个层,DADI 创建一个新的可写层,并将相应的 .tgz blob 解压到该层中,同时处理 whiteout(一种指示删除现有文件的特殊文件名模式)。如果用户想从 .tgz 基础镜像构建 DADI 镜像,必须首先使用此过程将基础镜像层转换为 DADI 格式。一些容器引擎会在容器层和其镜像层之间隐式创建一个名为 xxxxx-init 的特殊 init 层。在提交期间,DADI 将此 init 层与容器层合并,以保持镜像文件系统的完整性。

4.4 部署选项

  • P2P 传输(可选):DADI 的 P2P 数据传输能力是可选的,主要针对拥有大型应用的用户。

  • 共享存储方案:其他用户可能更倾向于将 DADI 与存储在高性能共享存储系统中的层 blob 一起使用,作为从镜像仓库获取层 blob 和在每台主机上存储层 blob 之间的一种折衷方案。类似解决方案已在社区中提出(例如 Teleport【9. Project Teleport, Microsoft Azure】、Wharf【41. Wharf: Sharing Docker Images in a Distributed File System, SoCC 2018】)。DADI 通过不需要解压层并支持如 HDFS 等替代存储系统,进一步增强了这些解决方案。

  • 按需拉取与本地缓存:对于不希望设置共享存储的用户,DADI 提供了从镜像仓库按需获取层 blob 并将数据块缓存在本地磁盘的选项。这种方法通过避免传输不需要的数据块,大大减少了冷启动延迟。如果在启动新容器实例时有可用的启动 I/O 轨迹,DADI 可以利用该轨迹预取启动容器所需的数据块,从而实现接近温启动的延迟。该轨迹可以简单地用 blktrace 收集,并用 fio 回放。

  • 本地下载方案:用户也可以选择通过将层 blob 下载到本地磁盘来使用 DADI。DADI 层不需要解压,节省了 .tgz 层所需耗时的顺序处理过程。因此,拉取 DADI 镜像要快得多。下载过程可以任选地卸载到 P2P 工具【20. Dragonfly: An Open-source P2P-based Image and File Distribution System, Alibaba Inc.; 22. Introducing Kraken, an Open Source Peerto-Peer Docker Registry, Uber Inc.; 24. FID: A Faster Image Distribution System for Docker Platform, FAS*W 2017; 30. Tupperware: Containerized Deployment at Facebook, 2014; 37. LargeScale Cluster Management at Google with Borg, EuroSys 2015】。我们在按需 P2P 传输遇到任何意外错误时,使用此方法作为备用路径。

A4 实验环境

  • 数据集/镜像

    • WordPress:来自 http://DockerHub.com 的流行内容管理系统镜像。包含 21 个 .tgz 格式的层,总大小 165MB;解压后为 501MB。DADI lz4 压缩格式下为 274MB。用于评估容器启动延迟。
    • Agility:一个基于 CentOS 7.6 的轻量级 Python 应用。包含 16 个层,ZFile 格式总大小 575MB,未压缩为 894MB。用于可伸缩性测试,因为它资源消耗少,便于大规模创建实例。
  • 对比系统

    • DADI
    • 标准 tarball 镜像 (.tgz)
    • Slacker (由于没有 Tintri VMstore,使用 LVM + NFS 作为近似替代,记为 pseudo-Slacker)
    • CRFS
    • LVM (dm or device mapper)
    • P2P 镜像下载
  • 硬件配置

    • 物理服务器:均配备双路多核 Xeon CPU 和 10GbE 或更高速率的网卡。
    • 存储:本地存储使用 NVMe SSD。通过限制 IOPS (2000) 和吞吐量 (100 MB/s) 来模拟低速的“云硬盘”。
    • 虚拟机:在公有云上托管,每台虚拟机配备 4 个 CPU 核心和 8GB 内存。虚拟网卡(vNIC)支持 5 Gbps 的突发带宽和 1.5 Gbps 的持续带宽。
  • 软件配置

    • 测试方法:测试前清除主机和客户机(如果适用)的内核页缓存以及 DADI 的持久缓存。DADI 默认使用 ZFile 压缩格式。
    • 测试工具:使用 fio 进行微基准测试,blktrace 收集 I/O 轨迹,dutar 测试容器内 I/O 性能。

A4 实验结果

5.2 启动延迟

  • 单实例冷启动 (图 14):当镜像层存储在镜像仓库或远程存储服务器上时,DADI 的冷启动时间显著低于 .tgz、CRFS 和 pseudo-Slacker,表现出明显的优势。

  • 单实例温启动 (图 15):当镜像层已缓存到本地磁盘后,DADI 的 I/O 路径效率更高。在 NVMe SSD 上,DADI 比 overlayfs 和 LVM 快 15%~25%;在模拟的“云硬盘”上,DADI 的性能是后者的 2 倍以上。

  • 基于追踪的预取 (图 16):通过预先记录启动时的 I/O 轨迹并在新实例启动时回放,可以消除冷启动和温启动之间 95% 的时间差异,实现接近温启动的效果。

  • 批量冷启动 (图 17):当批量启动 32 个实例时,DADI 的启动时间基本保持在 0.7 秒不变;而 pseudo-Slacker 的启动时间从 1.5 秒增加到 2.3 秒,显示 DADI 在批量场景下扩展性更好。

  • 生产环境启动 (图 18, 19):在生产环境中,拉取 DADI 元数据 tarball 通常不超过 1 秒,而拉取等效的 .tgz 镜像则需要超过 20 秒。更令人惊讶的是,使用 DADI 远程镜像和 P2P 传输的应用启动速度甚至比从本地 SSD 读取 .tgz 镜像更快。这归因于 OverlayBD 的性能优于 OverlayFS,以及 P2P 模式下主机能从其父节点的页缓存中读取数据,这比从本地磁盘读取更快。


图 14: 冷启动延迟。


图 15: 温启动延迟。


图 16: 使用基于追踪的预取时的启动延迟。


图 17: 批量冷启动延迟。


图 18: 生产环境中拉取镜像的时间。条形图表示10%和90%分位数。


图 19: 生产环境中启动应用的时间。

5.3 可伸缩性

  • 大规模启动 (图 20):在 1000 台虚拟机上冷启动 10000 个 Agility 容器,其延迟与温启动相比仅差 1 到 2 秒。这表明 DADI 在大规模部署下仍能保持高效。

  • 超大规模启动 (图 21):通过构建特殊的 P2P 拓扑进行推算,结果显示即使容器数量增加到 100,000 个,启动时间也基本保持平稳。实验还发现,当参与主机少于 20,000 台时,二叉树 P2P 拓扑最佳;超过此规模,3叉或4叉树效果更好。


图 20: 使用 DADI 的启动延迟(大规模启动)。


图 21: 使用 DADI 的预计启动延迟(超大规模启动)。

5.4 I/O 性能

  • 未缓存随机读 (图 22):在 I/O 队列深度为 1 时,DADI 的性能与 LVM 相当。随着队列深度增加,DADI 的性能增长较慢,但在队列深度为 128 时,未压缩的 DADI 最终超越 LVM,达到最高 IOPS。这表明 DADI 的索引效率更高,但其队列和批处理实现有优化空间。在队列深度小于 32 时,带压缩的 DADI 性能比不带压缩的好 10%~20%,因为压缩减少了数据传输量。

  • 文件扫描 (图 23, 24):使用 du(小随机读)和 tar(大顺序读)在容器内扫描整个镜像,DADI 在所有情况下均优于 overlayfs 和 LVM,尤其在“云硬盘”上优势更明显,这主要得益于压缩减少了数据传输量。


图 22: 未缓存随机读性能。


图 23: du 所有文件的时间。


图 24: tar 所有文件的时间。

5.5 镜像构建速度

  • 构建性能 (图 25):使用生产环境中的典型 dockerfile 进行测试,DADI 的镜像构建速度比 overlayfs 快 20%~40%(此测量不包括提交或压缩镜像的时间)。这主要得益于其基于日志结构的写性能优势和优化的构建流程。


图 25: 构建镜像的时间。

A7 补充细节

6 讨论与未来工作

  • 页缓存共享问题:使用 overlayfs 时,共享同一镜像层的容器可以共享主机页缓存。而 DADI 为每个层级镜像实现为独立的虚拟块设备,导致多个容器访问共享层中的同一文件时,在主机看来是访问不同的页面,因此主机页缓存无法共享,可能降低效率。

    • 解决方案:引入一个所有虚拟块设备共享的块池。利用 device mapper 将池中的段映射到虚拟块设备,使得不同容器对共享层中同一文件的访问看起来是访问池中的同一段。该池由页缓存支持,而上层的虚拟块设备和文件系统需要支持直接访问(DAX)以避免双重缓存。此方案可通过在池中执行块级去重进一步改进。
  • 融合的镜像服务:随着虚拟化运行时的出现,容器正在成为一种新型虚拟机,反之亦然。容器和虚拟机的运行时也可能开始融合。DADI 基于广泛支持的块设备,因此与容器和虚拟机都兼容,自然成为一种融合的镜像服务。这种融合基础设施将为当今云上的虚拟机用户带来层级镜像的便利和效率,并为用户提供更大的灵活性,使应用能够从基于云的(cloud-based)逐渐演进为云原生的(cloud-native)。

  • 未来工作:实现 DADI 潜力的关键是将其镜像格式标准化并促进其被采纳。我们正在努力将 DADI 的核心部分贡献给容器社区。

A5 结论

本文设计并实现了 DADI,一个用于容器的块级远程镜像服务。DADI 的核心思想是,增量镜像可以通过基于块的层来实现,其中每个层对应一组文件变更,但在物理上是给定文件系统下的块级变更集合。这种设计使得镜像服务与文件系统和平台无关,从而能够在不同环境中弹性部署应用。块级层的相对简单性进一步促进了一系列优化,以提高敏捷性,包括:细粒度的按需数据传输、使用高效编解码器的在线解压、基于追踪的预取、处理突发工作负载的 P2P 传输,以及与容器生态系统的轻松集成。DADI 在全球最大电子商务平台之一的生产环境中的应用经验表明,它在提高应用部署的敏捷性和弹性方面非常有效。