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 的块级镜像服务,通过按需拉取和基于块的分层模型,实现了大规模集群中应用的敏捷与弹性部署。

要解决什么问题 传统的容器启动采用“下载、解包、启动”的瀑布模型,其中拉取镜像层占用了约 80% 的启动时间。在创建或更新大型容器集群时,全量下载会迅速耗尽网络和文件系统资源。解包过程同时是 CPU、内存和 I/O 密集型的,容易导致主机上其他容器停滞。实际上,容器启动阶段仅使用了镜像约 6.4% 的数据。为了绕开全量下载,现有方案多采用基于文件系统的远程镜像(如按需拉取文件),但这带来了新的卡点:文件系统语义极其复杂,跨虚拟化边界传递会严重限制性能;暴露的攻击面较大,降低了公有云的安全性;且难以兼容非 POSIX 工作负载(如在 Linux 主机上运行 Windows 容器或极简的 Unikernel)以及高级文件系统特性。另一方面,基于块存储快照的方案又往往与特定存储底座深度绑定,失去了容器镜像跨平台共享和增量分发的标准优势。

怎么做的 核心思路是放弃文件级别的文件系统变更集,改用基于块的层级模型。DADI 将容器镜像建模为一个虚拟块设备,镜像层在物理上表示为特定文件系统下块级别的变更集合。这种设计将镜像服务与具体的文件系统解耦,只负责物理块的分发,从而绕开了复杂文件系统语义带来的性能和兼容性卡点。关键设计由以下四个部件构成:第一,OverlayBD 模块。它为多个只读层提供合并视图。每个层包含原始数据块和基于可变长度段的索引,段记录了变更在逻辑块地址空间中的起止位置及存储偏移量。为了解决多层查找时时间复杂度高达 $O(n \cdot \log m)$($n$ 为层数,$m$ 为每层段数)的问题,DADI 在加载时预计算一个合并索引,将随机读的查找复杂度降低到: $$O(\log M)$$ 其中 $M$ 是合并索引中的段总数。第二,ZFile 压缩格式。标准压缩包不支持随机读,DADI 将文件按固定大小分块压缩,并附加由 32 位整数数组构成的压缩索引。读取时通过索引定位并仅在线解压所需的数据块,利用 lz4 等高效算法,使得读取较小压缩数据节省的时间超过了解压耗时。第三,日志结构化的可写层。为了摆脱对联合文件系统的依赖并支持不支持随机写的存储后端,可写层采用追加写入模式,包含数据文件和内存中的红黑树索引,支持跨层块引用,并在后台进行垃圾回收。第四,树状 P2P 传输网络。针对大规模并发拉取,DADI 在主机本地缓存数据块,并通过 DADI-Root 节点管理应用层多播树。节点按需请求数据块,若父节点无缓存则向上传递,实现细粒度的按需传输并平衡网络流量。

效果如何 实验在配备多核 Xeon CPU 和 NVMe SSD(部分限制 IOPS 和吞吐量以模拟低速云硬盘)的物理机及公有云虚拟机上进行,使用 WordPress 和轻量级 Python 应用 Agility 镜像。对比基线包括:标准 tarball 镜像(传统全量下载路线)、CRFS(基于文件系统的远程镜像路线)、pseudo-Slacker(基于 LVM 加 NFS 模拟的块快照路线)以及 LVM 和常规 P2P 下载。量化结果显示,在单实例冷启动场景下,DADI 的延迟显著低于所有基线。在 32 个实例的批量冷启动中,DADI 的启动时间稳定在 0.7 秒左右,而 pseudo-Slacker 则退化至 2.3 秒。在大规模可伸缩性测试中,DADI 能够在 4 秒内于 1000 台虚拟机上冷启动 10000 个容器,推算显示即使容器数量增加到 100000 个,启动时间也基本保持平稳。结合基于追踪的预取机制,可消除冷启动与温启动之间 95% 的时间差异。在生产环境中,拉取 DADI 元数据通常不超过 1 秒,而等效的传统镜像需 20 秒以上。该方法也有其代价和局限:为了支持在线解压和块级对齐,ZFile 的压缩比略逊于传统格式,导致镜像文件体积略大。此外作者承认,由于 DADI 为每个容器分配独立的虚拟块设备,不同容器读取共享层中的同一文件时,在主机看来是不同的物理页面,因此无法像 overlayfs 那样直接共享主机页缓存,在密集共享场景下可能会降低内存利用效率。

A1 主要贡献

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


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

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

2.1 容器镜像

2.2 远程镜像

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

2.4 基于块快照的远程镜像

2.5 其他相关工作

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 镜像


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

3.2 层的合并视图

输入: 要查找的范围 (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 压缩与在线解压


图 8: ZFile 格式。


图 9: 相对层 Blob 大小。

3.4 DADI 容器层


图 10: DADI 的可写层。

3.5 P2P 数据传输


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

4 实现与部署

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

4.1 数据路径


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


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

4.2 容器引擎集成

4.3 镜像构建

4.4 部署选项

A4 实验环境

A4 实验结果

5.2 启动延迟


图 14: 冷启动延迟。


图 15: 温启动延迟。


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


图 17: 批量冷启动延迟。


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


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

5.3 可伸缩性


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


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

5.4 I/O 性能


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


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


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

5.5 镜像构建速度


图 25: 构建镜像的时间。

A7 补充细节

6 讨论与未来工作

A5 结论

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