论文阅读Slacker: Fast Distribution with Lazy Docker Containers

摘要

作者做了一个新的benchmark工具叫hello bench对各个容器进行分析,发现需要用76%的时间来pull镜像,并且只读取了6.4%的数据,于是作者设计了slacker,一个对快速启动容器的最优化存储引擎。用了一个集中的存储区域可以分享给所有的docker workers和registries。worker提供容器存储用来通过延迟获取容器数据达到后台clone并且最小化启动的目的。Slacker将容器开发周期(?开发周期不清楚是什么意思)中值提高了20倍,部署周期提高了5倍。

1. 介绍

隔离在云计算和多租户平台很重要,没有隔离的话,用户(在系统中付费的用户)会忍受不可预测的性能、碰撞和侵犯隐私。
虚拟机管理器已经习惯给程序提供隔离,每个程序部署在自己的虚拟设备上去,用设备自己的环境和资源。但是虚拟机管理器需要干预并且需要提供一些特权操作。并且用roundabout技术(?)去推断资源使用情况。结果就是虚拟机管理器很庞大,并且启动时间速度慢运行时开销很庞大。
Docker最近作为一个可供选择的轻量级基于管理程序的虚拟化技术很受欢迎。在容器里,一切进程相关的资源都通过操作系统来虚拟化,包括网络端口和文件挂载系统。容器本质上是享受所有资源的虚拟化的进程,不仅仅只有CPU和内存。所以没有本质的原因说明容器启动比正常进程启动要慢。
不幸的是实际上实验显示启动容器的结果要慢很多,因为文件系统的瓶颈。然而启动网络,计算和内存资源相对来说非常迅速并且简单。一个容器程序运行需要初始化文件系统,程序二进制文件,完整的linux系统调度,和依赖的库。在Docker或谷歌Borg集群中部署容器通常需要大量的复制和安装开销。最近对bb0 Borg的研究表明:“[任务启动延迟]是高度可变的,中位数通常在25秒左右。安装包安装大约占总数的80%:一个已知的瓶颈是本地磁盘的争用,在本地磁盘上编写安装包。”
如果可以提高启动时间,就会有很多机会:应用程序可以立即扩展以处理flash-crowd事件(?),集群调度程序可以快速地以低成本重新平衡节点,开发人员可以交互式地构建和测试分布式应用程序。
我们使用两个方法解决容器启动的问题,首先,我们开发了一个新的开源的Dockerbenchmark,HelloBench,可以更加清楚的观察容器启动。HelloBench基于57种不同的容器工作负载,测量从部署开始到容器准备开始执行有用工作的时间。我们使用HelloBench和静态分析来描述Docker图像和I/O模式。根据我们的分析显示,复制包数据占容器启动时间的76%,而容器开始有用的工作实际上只需要复制6.4%的数据,对于单个镜像简单的block-deduplication压缩比gzip压缩获得更好的压缩率。
然后我们根据我们的发现建立了Slacker,一种新的Docker存储驱动程序,通过使用专门的存储系统支持多个镜像层来实现快速的容器分配。具体来说,Slacker使用我们的后端存储服务器(Tintri VMstore[6])的快照和克隆功能来显著降低常见Docker操作的成本。Slacker没有预先传输整个容器镜像,而是根据需要延迟地提取镜像数据,从而大大减少了网络I/O。Slacker还利用我们对Linux内核所做的修改来改进缓存共享。
使用这些技术的结果是对常见Docker操作的性能有了巨大的改进;镜像推送变成153×更快,而拉出变成72×更快。涉及这些操作的对于常见Docker用例非常有用。例如,Slacker实现了容器部署周期的5倍中值加速和开发周期(?)的20倍加速。
我们还创建了MultiMake,一个新的基于容器的构建工具,展示了Slacker的快速启动的好处。MultiMake使用不同的GCC版本从一个源码中产生16个不同的二进制文件,使用slacker ,MultiMake过程速度提高了10倍。
剩下的内容由以下几部分组成。首先,我们介绍了现存的Docker框架。然后我们介绍HelloBench,一个我们用来分析Docker加载特征,我们从这个工具里的发现来指导我们设计Slack。最后,我们模拟了Slacker,呈现了MultiMake,讨论相关工作和总结。

2. Docker背景

现在我们介绍docker框架,存储接口,和默认的存储驱动。

2.1 容器版本控制

虽然Linux一直使用虚拟化来隔离内存,但是cgroups [37] (Linux的容器实现)通过为文件系统挂载点、IPC队列、网络、主机名、进程id和用户id[19]提供六个新的名称空间来虚拟化更广泛的资源。Linux cgroups是在2007年发布的,但是容器的广泛使用是最近才出现的现象,与Docker(2013年发布)等新的管理工具同时出现。使用Docker,像“Docker run -it ubuntu bash”这样的单个命令将从Internet上拉出ubuntu包,用一个新的ubuntu安装初始化一个文件系统,执行必要的cgroup设置,并在环境中返回一个交互式bash会话。这个示例命令有几个部分。首先,“ubuntu”是一个镜像的名字。镜像是系统数据的只读副本,通常包含应用程序所需的应用程序二进制文件、Linux发行版和其他包。将应用程序捆绑到Docker镜像中非常方便,因为分发服务器可以选择一组特定的包(及其版本),这些包将在运行应用程序的任何地方使用。其次,“run”是对图像执行的操作;run操作根据要用于新容器的映像创建初始化的根文件系统。其他操作包括“push”(用于发布新图像)和“pull”(用于从中心位置获取已发布的图像);如果用户试图运行非本地镜像,则会自动提取镜像。第三,“bash”是要在容器内启动的程序;用户可以在给定的映像中指定任何可执行文件。Docker管理镜像数据的方式与传统版本控制系统管理代码的方式非常相似。这个模型适用于两个原因。首先,同一个镜像可能有不同的分支(例如,“ubuntu:latest”或“ubuntu:12.04”)。第二,镜像自然地建立在彼此之上。例如,Rails上的ruby映像构建在Rails映像之上,而Rails映像又构建在Debian映像之上。这些图像代表在旧镜像之上的一个新的提交。可能有其他未标记为可运行镜像的提交。当运行一个镜像,可能从一个已经提交的镜像开始,但是文件系统被修改了。在版本控制术语里。这些修改被称为未提交的修改。Docker的“commit”命令将容器和对该容器的修改转变为一个新的只读的镜像。在Docker中,一个层要么引用提交的数据,要么引用容器的未分层更改(unstaged changes?)。
Docker工作的机器运行一个当地的守护进程。通过向某个特定的worker的本地守护进程发送命令,可以在该worker上创建新的容器和映像。映像共享是通过集中式的仓库完成的,这些仓库通常运行在与Docker worker位于同一集群中的机器上。镜像可以通过从守护进程向仓库推送发布,而镜像可以通过在集群中执行多个守护进程来部署。仅传输接收端没有的层。层表示为网络上和仓库机器上的gzip压缩tar文件。守护程序计算机上的表示由可插拔存储驱动程序决定。

2.2 存储驱动程序接口

Table1:Docker Driver API
图1
Docker容器以两种方式访问存储,首先,用户可以在容器内的主机上挂载目录。例如,运行容器化编译器的用户可以将其宿主目录装入容器中,这样编译器就可以读取代码并在主机目录中生成二进制文件。其次,容器需要访问用于表示应用程序二进制文件和库的Docker层。Docker通过容器用作根文件系统的挂载点显示此应用程序数据的视图。容器存储和安装由Docker存储驱动程序管理;不同的驱动程序可以选择以不同的方式表示层数据。驱动程序必须实现的方法如表1所示(没有显示一些无趣的函数和参数)。所有函数都使用一个字符串“id”参数来标识被操作的层。Get函数请求驱动程序挂载该层并返回到挂载点的路径。返回的挂载点不仅应该包含“id”层的视图,还应该包含它的所有祖先的视图(例如,在挂载点的目录遍历期间应该看到“id”层的父层中的文件)。Create通过复制父层来创建一个新的镜像层,如果父层是null,那么新的层应该是空的。Docker使用Create命令来为新的容器提供新的镜像层,并且分配层来存储拉取的数据。
Diff和ApplyDiff分别用于Docker push和pull操作,如图1所示。当Docker pushing一个镜像层,Diff将这个镜像层从一个当地的表现形式转变为一个包含这个镜像层的压缩的tar文件。ApplyDiff恰恰相反,给定一个tar文件和一个本地层,它在现有层上解压tar文件。
图2显示了第一次运行四层映像(例如ubuntu)时的驱动程序调用。在镜像pull的的过程中创建了4层镜像。镜像本身多创建了两层镜像,A-D层呈现了镜像。A的create在一个NULL的父层操作,所有A初始化的时候是空的。但是,随后调用ApplyDiff操作告诉驱动程序从提取的tar文件添加到A中。B-D层各自都由两步构成:从父文件拷贝副本(通过Create)和从tar文件添加(通过ApplyDiff)。在第8步之后,拉操作完成,Docker准备创建一个容器。它首先创建一个只读层E-init,并向其添加一些小型初始化文件,然后创建容器将用作其根的文件系统E。

2.3 AUFS驱动程序实现

图2
对于Docker发行版,AUFS存储驱动程序是一个常见的默认设置。这个驱动程序基于AUFS文件系统(另一个联合文件系统)。联合文件系统不直接在磁盘上存储数据,而是使用另一个文件系统(例如ext4)作为底层存储。
联合挂载点提供基础文件系统中多个目录(multiple directories?)的视图。使用底层文件系统中的目录路径列表挂载AUFS。在路径解析期间,AUFS遍历目录列表;选择包含要解析的路径的第一个目录,并使用该目录中的inode。AUFS支持特殊的whiteout文件,使某些较低层的文件被删除;这种技术类似于其他分层系统(例如LSM数据库[29])中的删除标记。AUFS还在文件粒度上支持COW(即写时复制);写时,在允许继续写之前,将底层的文件复制到顶层。
AUFS驱动程序利用了AUFS文件系统的分层和写时复制功能,同时还可以直接访问底层AUFS文件系统。AUFS驱动对自己存储的每个镜像层在底层AUFS文件系统上创建了一个新的文件夹。一个ApplyDiff操作的简单的将归档的文件解压到镜像层的文件夹。当一个Get调用,AUFS驱动程序创建一个统一的镜像视图和它的历史。当Create操作调用时AUFS驱动使用它的写时复制的功能很高效的复制镜像层的数据。不幸的是,我们发现写时复制的功能在一个文件的力度有一些性能上的问题。

3 HelloBench

我们实现了HelloBench,一个新的benchmark被设计于测试容器的启动。HelloBench直接执行Docker命令,所以pushes,pull和run等操作可以被独立测量。这个benchmark有两部分组成:(1)一个容器镜像的收集器和(2)用于在上述容器中执行简单任务的测试工具。这些镜像是截止至2015年6月1日Docker hub 库中获取的。HelloBench由当时72个镜像中的57个组成。我们选择的镜像是使用最小配置运行并且不依赖于其他容器。比如说,不包括WokrdPress因为WordPress容器依赖于分开的MySQL容器。
Table2
Table2列出了HelloBench使用的镜像。我们将镜像分成6个面板上的类别,有些分类有些主观;比如说,Django镜像包括一个web服务器,但是大多数认为他是一个Web框架。HelloBench利用在容器中运行最简单的任务或等待容器报告就绪来度量启动时间。作为语言类容器,任务就是的编译或者解释一个简单的“hello world”语言程序。Linux发行版的镜像就是运行一个非常简单的shell命令,通常是“echo hello”。对于长时间运行的服务器(尤其是数据库和web服务器),HelloBench测量到容器输出“up and ready”的信息的时间。对于一些特别的服务器,将轮询公开端口,直到有响应为止。
每个HelloBench的镜像由好几个镜像层组成,一些还在彼此的容器中共享。图3显示了这个镜像层之间的关系。通过57个镜像,由550个节点和19个根节点。在一些例子中,一个镜像版本作为一个其他的镜像版本的基础(比如说“ruby”是“rails”的基础)。只有一个镜像由单个镜像层组成:”alpine“, 一个特别轻量级的Linux发行版镜像(比如Debian的老版本),这就是为什么多个镜像镜像共享一个公共的基础而不是一个solid black circle(?)。
为了评估HelloBench的代表性,对于常用的镜像,我们在2015年1月15日统计了每个Docker Hub library镜像被拉取的次数(在原始HelloBench镜像被拉取的7个月后)。在此期间,镜像库的镜像从72个增加到94个。图4显示了94张镜像,按HelloBench分类。HelloBench是代表了流行的镜像,占所有拉取镜像的86%。大多数受欢迎的是Linux发行库(例如BusyBox和Ubuntu)。数据库(如Redis和MySQL)和web服务器(如nginx)也很受欢迎。
图3
图4

4 工作负载分析(Workload Analysis)

在本节中,我们将分析HelloBench工作负载的行为和性能,提出四个问题:容器映像有多大,执行需要多少数据(§4.1)?推、拉和运行图像需要多长时间(§4.2)?图像数据是如何跨层分布的,其性能影响是什么(§4.3)?以及不同运行之间的访问模式有多相似(§4.4)。
所有性能测量都是从运行在具有2ghz Xeon cpu (E5-2620)的PowerEdge R720主机上的虚拟机中获得的。VM提供8 GB RAM、4个CPU内核和一个由Tintri T620[1]支持的虚拟磁盘。服务器和VMstore没有其他负载。

4.1 容器数据

我们从研究Docker Hub中提取的HelloBench镜像开始分析。对于每个图像,我们采取三个度量:压缩大小、未压缩大小和执行HelloBench时从图像中读取的字节数。我们通过在blktrace (?)跟踪的块设备上运行工作负载来测量读取。图5显示了这三个数字的CDF。我们观察到读取数据的中值为20MB,但镜像压缩后的大小的中值为117mb,未压缩后为329mb。
图5
在这里插入图片描述
我们将读取的数据和大小按类别分类,在图6所示。相对浪费最大的是发行版工作负载(分别为30×和85×用于压缩和未压缩),但是绝对浪费也最小。对于语言和web框架类别来说,绝对的浪费是最大的。在所有图像中,平均只有27mb被读取;平均未压缩镜像为15×倍,说明容器启动只需要6.4%的镜像数据。
虽然Docker镜像压缩为gzip归档文件时要小得多,但是这种格式不适合运行的容器修改数据。因此,容器通常存储未压缩的数据,这意味着压缩减少了网络I/O,而不是磁盘I/O。重复数据删除是适用于更新的数据压缩的简单替代方法。为了计算重复数据删除的有效性,我们扫描HelloBench图像来寻找文件块之间的冗余。图7比较了文件和块(4 KB)粒度上的gzip压缩率和重复数据删除率。条形图表示单个镜像的速率。gzip的速率在2.3到2.7之间,而重复数据删除在每幅图像的基础上做得很差。然而,跨所有镜像的重复数据删除率分别为2.6(文件粒度)和2.8(块粒度)。
图7
结论:执行过程中读取的数据量远远小于总图像大小,不管是压缩或未压缩。镜像数据通过网络被压缩发送,然后到本地解压存储。因此,网络和磁盘的开销都很高。减少开销的一种方法是用较少的安装包构建更精简的映像。另一种选择是,当容器需要时,可以延迟下载镜像数据。我们还发现,与gzip压缩相比,基于全局块的重复数据删除是一种有效的镜像数据表示方法。

4.2 操作性能

图8
图9
一旦构建好,容器化的应用程序通常按以下方式部署:开发人员将应用程序映像一次推入中央仓库,许多用户拉出映像并且运行应用程序。我们使用HelloBench测量这些操作的延迟,在图8中报告CDFs。推、拉和运行的平均时间分别为61秒、16秒和0.97秒。 图9按工作负载类别划分了操作时间。这一模式大体上是成立的:运行快,推拉镜像慢。运行速度最快的是发行版和语言类别(分别为0.36和1.9秒)。推、拉和运行的平均时间分别为72秒、20秒和6.1秒。因此,在远程仓库上启动一个新映像时,76%的启动时间将花在pull上。
因为推和拉是最慢的,所以我们想知道这些操作是否只是高延迟,或者它们是否在某种程度上也很需要时间,从而限制了吞吐量,即使多个操作同时运行。为了研究可伸缩性,我们同时推拉不同大小的不同数量的人造的镜像。每个镜像包含一个随机生成的文件。我们使用人造的镜像而不是HelloBench镜像来创建不同大小的相同大小的图像。图10显示了总时间大致与镜像数量和镜像大小成线性关系。因此,推和拉不仅是高延迟,而且消耗网络和磁盘资源,限制了可伸缩性。
图10
**结论:**容器启动时间以pull为主,新部署中76%的时间将花在pull上。对于迭代开发应用程序的程序员来说,使用push发布图像将非常缓慢,尽管这种情况可能没有已发布图像的多部署常见。大部分推操作由存储驱动程序的Diff函数完成,而大部分拉操作由ApplyDiff函数完成(§2.2)。优化这些驱动程序功能将提高分发性能。

4.3 镜像层

图11
镜像数据通常跨多个层进行分割。AUFS驱动程序在运行时合成图像的各个层,以提供文件系统的完整视图。在本节中,我们将研究分层的性能和数据跨层分布。我们首先看看分层文件系统容易出现的两个性能问题(图11):查找深层文件和对非顶层文件进行小的写操作。首先,我们创建(并使用AUFS组合)16个层,每个层包含1K个空文件。然后,使用冷缓存(cold cache?),我们从每层随机打开10个文件,测量打开延迟。图11a显示了结果(平均运行超过100次):层深度和延迟之间有很强的相关性。其次,我们创建两个层,其底部包含大小不同的大型文件。我们测量将一个字节附加到存储在底层的文件的延迟。如图11b所示,小写入的延迟对应于文件大小(而不是写大小),正如AUFS在文件粒度上所做的COW操作。在一个文件被修改之前,它被复制到最上层,所以写一个字节可能需要20秒。幸运的是,对较低层的小写操作会导致每个容器的一次性开销;后续的写操作将更快,因为大文件将被复制到顶层。
图12
图13
在考虑了层深度与性能之间的关系之后,我们现在要问的是,HelloBench图像通常存储的数据有多深?图12显示了每个深度级别的总数据百分比(以文件数量、目录数量和字节为单位的大小表示)。这三个指标大致对应。一些数据深达28层,但质量更集中在左边。超过一半的字节深度至少为9层。现在我们考虑数据如何跨层分布的差异,为每个图像测量存储在最上层、最下层和最大层中的部分(以字节为单位)。图13显示了分布情况:对于79%的图像,最上层包含0%的图像数据。相反,在中位数情况下,27%的数据位于最底层。大多数数据通常驻留在一个单层中。
**含义:**对于分层文件系统,存储在更深层的数据访问速度较慢。不幸的是,Docker镜像往往比较深,至少一半的数据深度在第九层或更大。扁平层是避免这些性能问题的一种技术;然而,扁平化可能需要额外的复制,从而使分层文件系统提供的其他COW优势失效。

4.4 缓存

图14
我们现在考虑的情况是,同一个worker运行相同的镜像不止一次。特别是,我们想知道是否可以使用第一次执行的I/O预填充缓存,以避免在后续运行时执行I/O。为此,我们连续两次运行每个HelloBench工作负载,每次收集块跟踪。我们计算第二次运行期间读取的部分,这些部分可能受益于第一次运行期间由读取填充的缓存状态。
图14显示了第二次运行的读和写。读取分为命中和未命中,对于给定的块,只计算第一次读取(我们希望研究工作负载本身,而不是收集跟踪的特定缓存的特征)。在所有工作负载中,读/写比率是88/12。对于发行版、数据库和语言工作负载,工作负载几乎完全由读取组成。在读取操作中,99%的操作可能由以前运行时缓存的数据来完成。
**结论:**相同的数据经常在同一映像的不同运行期间读取,这表明当同一映像在同一台机器上多次执行时,缓存共享将非常有用。在具有许多容器化应用程序的大型集群中,除非容器放置的机器受到很强的限制,否则不太可能重复执行。此外,其他目标(例如负载平衡和故障隔离)可能会使托管变得不常见。然而,对于容器化的实用程序(如python或gc1c)和在小集群中运行的应用程序,重复执行可能很常见。我们的结果表明,后一种场景将受益于缓存共享。

5 Slacker

图15
在本节中,我们将介绍一种新的Docker存储驱动程序Slacker。我们的设计基于对容器工作负载的分析和五个目标:(1)非常快速地进行推拉操作;(2)对长时间运行的容器不造成任何损害;(3)尽可能重用现有的存储系统;(4)利用现代存储服务器提供的强大原语,并且(5)除了在存储驱动程序插件(§2.2)中,不对Docker注册表或守护进程进行任何更改。
图15说明了运行Slacker的Docker集群的体系结构。该设计基于集中式NFS存储,在所有Docker守护进程和注册中心之间共享。容器中的大多数数据都不需要执行容器,因此Docker worker只根据需要从共享存储延迟地获取数据。对于NFS存储,我们使用Tintri VMstore服务器[6]。Docker镜像由VMstore的只读快照表示。镜像仓库不再用作层数据的主机,而是仅用作将图像元数据与相应快照关联起来的名称服务器。推拉不再涉及大型网络传输;相反,这些操作只是共享快照id。Slacker使用VMstore快照将容器转换为可共享的映像,并根据从注册中心提取的快照ID克隆到提供容器存储。在内部,VMstore使用块级COW来高效地实现快照和克隆。
Slacker的设计基于我们对容器工作负载的分析;特别是,以下四个设计小节(§5.1至§5.4)对应于前面四个分析小节(§4.1至§4.4)。最后,我们讨论了Docker框架本身可能的修改,以便更好地支持非传统的存储驱动程序,如Slacker(§5.5)。

5.1 存储层

我们的分析显示,在容器开始有用的工作之前,通过拉传输的数据中实际上只需要6.4%(§4.1)。为了避免在未使用的数据上浪费I/O, Slacker将所有容器数据存储在NFS服务器(Tintri VMstore)上,由所有worker共享。图16a说明了这种设计:每个容器的存储都表示为一个NFS文件。Linux loopbacks(§5.4)用于将每个NFS文件视为一个虚拟块设备,可以将其作为运行容器的根文件系统进行挂载和卸载。Slacker将每个NFS文件格式化为ext4文件系统。
图16b比较了Slacker堆栈和AUFS堆栈。尽管两者都使用ext4(或其他一些本地文件系统)作为关键层,但有三个重要的不同之处。首先,ext4由Slacker中的一个网络磁盘支持,但是由一个带有AUFS的本地磁盘支持。因此,Slacker可以通过网络延迟加载数据,而AUFS必须在容器启动之前将所有数据复制到本地磁盘。
其次,AUFS在文件级别上执行的COW高于ext4,因此很容易受到分层文件系统所面临的性能问题的影响(§4.3)。相反,Slacker层在文件级得到了有效的扩展。然而,Slacker仍然受益于COW,它利用了VMstore中实现的块级COW(§5.2)。此外,VMstore还可以在不同的Docker worker上运行包含程序,从而进一步节省空间。
第三,AUFS使用单个ext4实例的不同目录作为容器的存储,而Slacker使用不同的ext4实例来备份每个容器。这种差异带来了一个有趣的权衡,因为每个ext4实例都有自己的日志。使用AUFS,所有容器将共享相同的日志,从而提供更高的效率。然而,众所周知,日志共享会导致版本内的优先级降低,从而破坏QoS保证[48],这是多租户平台(如Docker)的一个重要特性。当NFS存储被划分为许多小的、非完整的ext4实例时,内部碎片[10,Ch. 17]是另一个潜在的问题。幸运的是,VMstore文件是稀疏的,所以Slacker不会遇到这个问题。

5.2 VMstore集成

早些时候,我们发现与运行相比,Docker的推拉速度相当慢(§4.2)。运行速度很快,因为新容器的存储是使用AUFS提供的COW功能从图像初始化的。相比之下,推和拉在传统驱动程序中速度较慢,因为它们需要在不同的机器之间复制大的层,所以AUFS的COW功能是不可用的。与其他Docker驱动程序不同,Slacker构建在共享存储之上,因此在概念上可以在守护进程和镜像仓库之间进行COW共享。
图17
幸运的是,VMstore使用一个辅助的基于rest的API扩展了它的基本NFS接口,其中包括两个相关的COW函数,snapshot和clone。snapshot函数调用会创建NFS文件的只读快照,clone函数从快照创建NFS文件。快照不会出现在NFS名称空间中,但是有惟一的id。文件级快照和克隆是功能强大的原语,用于构建更高效的日志记录、重复数据删除和其他常见存储操作[46]。在Slacker中,我们分别使用snapshot和clone来实现Diff和ApplyDiff。这些驱动程序函数由Docker push和pull操作(§2.2)分别调用。
图17a显示了一个运行Slacker的守护进程如何在推送时与VMstore和Docker仓库进行交互。Slacker要求VMstore创建表示该层的NFS文件的快照。VMstore获取快照,并返回快照ID(大约50个字节),在本例中为“212”。Slacker将ID打包到压缩的tar文件中。Slacker将ID打包进tar文件中,以实现向后兼容性:当就收到一个tar文件后不需要修改镜像仓库。如图17b所示,A pull实际上是相反的操作。Slacker从注册表接收快照ID,它可以从该ID克隆NFS文件用于容器存储。Slacker的实现速度很快,因为(a)layer数据从不压缩或未压缩,(b)layer数据从不离开VM存储,所以只有元数据通过网络发送。
考虑到Slacker的实现,“Diff”和“ApplyDiff”这两个操作有些不恰当。特别是,Diff(A, B)应该返回一个delta,另一个守护进程(已经有A)可以从这个delta重构B。因此,Diff(A, B)没有返回delta,而是返回一个引用,另一个工作者可以从这个引用获得B的克隆,不管有没有A。
Slacker与运行非Slacker驱动程序的其他守护进程部分兼容。Slacker提取tar时,它会在处理前查看流tar的第一个字节。如果tar包含层文件(而不是嵌入快照),Slacker将返回到简单的解压缩而不是克隆。因此,Slacker可以抓取其他驱动推送的镜像,尽管速度很慢。但是,其他驱动程序将无法获取Slacker图像,因为它们不知道如何处理打包到到tar文件中的快照ID。

5.3 优化Snapshot和Clone操作

图像通常由许多层组成,HelloBench中超过一半的数据深度至少为9(§4.3)。对于这类数据,块级COW相对于文件级COW具有固有的性能优势,例如遍历块映射索引(可能是衰减的)比遍历底层文件系统的目录更简单。
然而,深度分层的图像仍然对Slacker构成了挑战。正如前面所讨论的(§5.2),Slacker层是填充的,所以安装任何一层都可以提供一个容器可以使用的文件系统的完整视图。不幸的是,Docker框架没有增加层的概念。当Docker获取一个图像时,它获取所有的层,并通过ApplyDiff将每个层传递给驱动程序。对于Slacker来说,最上层就足够了。对于28层的图像(如jetty),额外的克隆代价很大。
我们的目标之一是在现有的Docker框架内工作,因此我们没有修改框架以消除不必要的驱动程序调用,而是使用延迟克隆(lazy cloning)对它们进行优化。我们发现,一次pull的主要成本不是快照tar文件的网络传输,而是VMstore克隆。因此,Slacker(如果可能的话)没有将每个层表示为NFS文件,而是用一个记录快照ID的本地元数据表示它们。ApplyDiff只是设置这个元数据,而不是立即克隆。如果Docker调用到达该层,Slacker将在挂载之前执行真正的克隆。我们还使用快照id元数据进行快照缓存。特别地,Slacker实现了Create,它创建了一个层的逻辑副本(§2.2),其中快照紧随其后的是一个克隆(§5.2)。如果许多容器是由相同的映像创建的,Create将在同一层上被多次调用。Slacker不为每个创建创建快照,而是只在第一次创建快照,然后重用快照ID。如果挂载某个层,则该层的快照缓存将失效(挂载后,该层可能发生更改,使快照过期)。
快照缓存和延迟克隆的结合可以使创建非常高效。特别是,从A层复制到B层可能只涉及从A的快照缓存条目复制到B的快照缓存条目,而不需要对VMstore进行特殊调用。在背景部分(§2.2)的图2中,我们展示了10个Create和ApplyDiff调用,它们用于拉出和运行一个简单的四层镜像。如果没有延迟缓存和快照缓存,Slacker将需要执行6个快照(每个创建一个)和10个克隆(每个创建或ApplyDiff一个)。使用我们的优化,Slacker只需要做一个快照和两个克隆。在步骤9中,Create执行一个延迟克隆,但是Docker在E-init层调用Get操作,因此必须执行一个真正的克隆。对于步骤10,Create必须同时执行快照和克隆,以生成和挂载层E作为新容器的根。

5.4 Linux内核修改

我们的分析表明,从同一镜像开始的多个容器倾向于读取相同的数据,这表明缓存共享可能是有用的(§4.4)。AUFS驱动程序的一个优点是COW是在底层文件系统之上完成的。这意味着不同的容器可以在底层系统中加载并利用相同的缓存状态。lacker在VMstore中做COW,低于本地系统的级别。这意味着两个NFS文件可能是相同快照的克隆(经过一些修改),但是不会共享缓存状态,因为NFS协议不是围绕COW共享概念构建的。缓存重复数据删除可以帮助节省缓存空间,但这不会阻止初始I/O。在从VMstore通过网络传输两个块之前,重复数据删除不可能实现两个块是相同的。在本节中,我们将描述在NFS文件级别的Linux页面缓存中实现共享的技术。
为了实现NFS文件之间的客户端缓存共享,我们修改了NFS客户端(比如loopback模块),以添加对VMstore快照和克隆的操作。特别是,我们使用位图来跟踪类似NFS文件之间的差异。所有对NFS文件的写入都是通过环回模块完成的,因此环回模块可以自动更新位图以记录新的更改。快照和克隆是由Slacker驱动程序发起的,因此我们扩展了回循环API,以便Slacker可以通知模块文件之间的COW关系。特别是,我们使用位图来跟踪类似的NFS文件之间的差异。所有对NFS文件的写入都是通过loopback模块完成的,因此loopback模块可以自动更新位图以记录新的更改。快照和克隆是由Slacker驱动程序发起的,因此我们扩展了loopback API,以便Slacker可以通知模块文件之间的COW关系。
图18用一个简单的例子说明了这种技术:两个容器,B和C,从同一个图像开始,A.当启动容器时,Docker首先从基础镜像(A)创建两个init层(B-init和C-init)。注意,在init层中,“m”被修改为“x”和“y”,而第0位被修改为“1”来标记更改。Docker从B-init和C-init创建最顶层的容器层B和C。Slacker使用新的loopback API将B-init和C-init位图分别复制到B和C。如图所示,随着容器的运行和写入数据,B和C位图积累了更多的变化。作为API的一部分,Docker并没有显式地将init层与其他层区分开来,但是Slacker可以推断出层类型,因为Docker恰好为init层的名称使用了“-init”后缀。
现在假设容器B读取block 3.loopback模块在3的位置看到一个未修改的“0”位,表示文件B和文件A中的block 3是相同的。因此,loopback模块将read发送给A而不是B,从而填充A的缓存状态。现在假设C读取block 3。C的第3块也是未修改的,因此读取再次被重定向到A。当然,对于B和C与A不同的块,重要的是读取没有重定向。假设B读取block 1,然后C读取block 1。在这种情况下,B的读取不会填充缓存,因为B的数据与A不同。在这种情况下,C的读取不会使用缓存,因为C的数据与A不同。

5.5 Docker框架讨论

我们的目标之一是不更改Docker镜像仓库或守护进程,除非在可插入存储驱动程序中。尽管存储驱动程序接口非常简单,但它已经足够满足我们的需求。但是,对Docker框架进行了一些更改,可以启用更优雅的Slacker实现。首先,如果镜像仓库可以表示不同的层格式(§5.2),这将有助于驱动程序之间的兼容性。目前,如果一个非Slacker层拉一个Slacker推的层,它将以一种不友好的方式失败。格式跟踪可以提供友好的错误消息,或者,理想情况下,为自动格式转换启用挂钩(hook)。其次,添加扁平层的概念将非常有用。特别是,如果驱动程序可以通知框架某个层是平的。Docker就不需要在拉动时获取祖先层。这将消除我们对延迟克隆和快照缓存的需求(§5.3)。第三,如果框架显式地标识了init层,那么Slacker就不需要依赖层名作为提示(§5.4),这将非常方便。

6 评价

我们使用与分析相同的硬件进行评估(§4)。为了进行公平的比较,我们还对Slacker存储使用了与运行AUFS实验的VM的虚拟磁盘相同的VMstore。

6.1 HelloBench 镜像加载

图19
图20
早些时候,我们看到在HelloBench中,推拉时间占主导地位,而运行时间非常短(图9)。我们用Slacker重复这个实验,在图19中显示了新的结果和AUFS结果。平均而言,推阶段是153×更快,拉阶段是72×更快,但是运行阶段慢17% (AUFS拉阶段为运行阶段预热缓存)。
在不同的场景中使用了不同的Docker操作。一个用例是开发周期:在每次更改代码之后,开发人员将应用程序推入镜像仓库,将其拉到多个工作节点,然后在节点上运行它。另一个是部署周期:一个不经常修改的应用程序由镜像仓库托管,但是偶尔的负载爆发或负载平衡需要在woker上拉出并运行。图20显示了这两种情况下Slacker相对于AUFS的加速速度。对于中值镜像加载,Slacker在部署和开发周期中分别将启动提高5.3×和20×。速度提升是高度可变的:几乎所有工作负载都至少有适度的改进,但是10%的工作负载至少在部署和开发方面分别提高了16×和64×。

6.2 长时间运行表现

图21
在图19中,我们看到使用Slacker推和拉的速度要快得多,而运行的速度要慢得多。这是预期的,因为运行在任何数据传输之前就开始了,二进制数据只在需要时才延迟传输。我们现在运行几个长时间运行的容器实验;我们的目标是证明,一旦AUFS完成了对所有图像数据的提取,而Slacker完成了对热图像数据的延迟加载,那么AUFS和Slacker具有相同的性能。
为了进行评估,我们选择了两个数据库和两个web服务器。对于所有的实验,我们执行5分钟,测量每秒的操作。每一个实验都从一个pull开始。我们使用“loosely based on TPC-B”的pgbench来评估PostgreSQL数据库。我们使用具有相同频率的获取、设置和更新键的自定义基准来评估内存数据库Redis。我们评估Apache web服务器,使用wrk[4]基准反复获取静态页面。最后,我们评估io。类似node的基于javascript的web服务器。使用wrk基准反复获取动态页面。
图21A显示了结果。AUFS和Slacker通常提供大致相同的性能,不过对于Apache来说,Slacker要快一些。虽然驱动程序在长期性能方面类似,但是图21B显示Slacker容器开始处理任务的速度比AUFS快3-19倍。

6.3 缓存

图22
我们已经证明Slacker提供了相对于AUFS(当需要pull时)更快的启动时间和等效的长期性能。Slacker处于劣势的一种情况是,同一台机器上多次运行相同的短集群工作负载。对于AUFS,第一次运行将比较慢(因为需要拉操作),但是后续的运行将比较快,因为图像数据将存储在本地。此外,COW是在本地完成的,因此从同一个启动映像运行的多个容器将受益于共享RAM缓存。
而Slacker则依赖Tintri VM-store在服务器端做COW。这种设计支持快速分发,但缺点是,如果不更改内核,NFS客户端不会自然地意识到文件之间的冗余。我们将修改后的 loopback 驱动程序(§5.4)与AUFS作为共享缓存状态的一种方法进行比较。为此,我们将每个HelloBench工作负载运行两次,测量第二次运行的延迟(在第一次运行预热了缓存之后)。我们将AUFS与Slacker进行比较,其中包含和不包含内核修改。
图22显示了所有工作的运行时CDF—三个系统的负载(注意:这些数字是使用运行在ProLiant DL360p Gen8上的VM收集的)。虽然AUFS仍然是最快的(平均运行时间为0.67秒),但是内核修改显著加快了Slacker的速度。Slacker单独运行的平均时间为1.71秒;对loopback模块进行内核修改后,时间是0.97秒。虽然Slacker避免了不必要的网络I/O,但是AUFS驱动程序可以直接缓存ext4文件数据,而Slacker缓存ext4下的块,这可能会带来一些开销。

6.4 可伸缩性

图23
早些时候(§4.2),我们看到,就同时处理的图像的大小和数量而言,推拉时AUFS的伸缩性很差。我们使用Slacker重复前面的实验(图10),再次创建合成图像,并同时推拉不同数量的图像。
图23显示了结果:图像大小不再像对AUFS那样重要。总时间仍然与同时处理的图像数量相关,但绝对时间要好得多;即使有32张图片,推和拉的时间最多也只有两秒钟。同样值得注意的是,对于slaker来说,推的时间与拉的时间相似,而对于AUFS来说,推的时间则要长得多。这是因为AUFS对其大型数据传输使用压缩,而压缩通常比解压缩更昂贵。

7 案例研究:MultiMake

图24
在创建Dropbox时,Drew Houston(联合创始人兼首席执行官)发现,构建一个广泛部署的客户端需要进行大量“粗糙的操作系统工作”,以使代码兼容各种平台[18]的特性。例如,一些bug只会出现在瑞典版本的Windows XP Service Pack 3中,而其他非常类似的部署(包括挪威版本)则不会受到影响。避免这些错误的一种方法是在许多不同的环境中广泛地测试软件。有几家公司提供了容器化集成测试服务[33,39],包括针对几十个版本的Chrome、Firefox、Internet Explorer和其他浏览器[36]快速测试web应用程序。当然,这种测试的范围受到不同测试环境供应速度的限制。
我们演示了快速容器准备在使用新工具(数百万)进行测试时的有用性。在源目录上运行multiake使用最后16个GCC重新租约构建目标二进制文件的16个不同版本。每个编译器都由一个镜像仓库托管的Docker映像表示。比较二进制文件有很多用途。例如,某些安全检查已知会被某些编译器重新租约[44]优化掉。数百万使开发人员能够评估跨GCC版本的此类检查的健壮性。
multimake的另一个用途是针对不同的GCC版本评估代码片段的性能,这些版本使用不同的优化。例如,我们在一个简单的C程序上使用了multiplake,做了20M的向量算术运算,如下:

for (int i=0; i<256; i++) { 
	a[i] = b[i] + c[i] * 3;
}

图24a显示了结果:最新的GCC发行版很好地优化了向量操作,但是4.6和4.7系列编译器生成的代码执行起来要花费大约50%的时间。GCC 4.8.0生成了快速的代码,尽管它是在一些较慢的4.6和4.7版本之前发布的,所以一些优化显然没有进行反向移植。图24b显示,Slacker(68秒)收集该数据的速度比AUFS驱动程序(646秒)快9.5倍,因为大部分时间都是用AUFS拉动的。尽管所有GCC映像都有一个公共的Debian基础(只能提取一次),但是GCC安装代表了大部分数据,AUFS每次都提取这些数据。清理是AUFS比Slacker更昂贵的另一个操作。在AUFS中删除一个层需要删除数千个小的ext4文件,而在Slacker中删除一个层则需要删除一个大型NFS文件。
快速运行不同版本代码的能力可能会使除数百万之外的其他工具受益。例如,git bisect通过在提交[23]的范围内进行二叉搜索来发现引入错误的提交。除了基于容器的自动构建系统[35],一个集成了Slacker的平分工具可以非常快速地搜索大量提交。

8 相关工作

优化磁盘映像的多部署与我们的工作类似,Slacker使用的ext4格式的NFS文件类似于虚拟磁盘映像。Hibler等人构建了Frisbee系统,该系统使用基于文件系统感知的技术(例如,Frisbee不考虑未被系统使用的块)来优化差分图像更新。Wartel等人比较了从中央存储库(很像Docker镜像仓库)延迟分发虚拟机映像的多种方法。Nicolae等人研究了映像部署,发现“预传播是一个昂贵的步骤,特别是因为初始VM只有一小部分是实际访问的。他们进一步建立了一个分布式的系统来托管支持VM数据延迟传播的虚拟机映像。Zhe等人构建了一个基于云的web应用程序平台Twinkle,旨在处理“快速拥挤的事件处理”。“不幸的是,虚拟机往往是重量级的,正如他们所指出的:“虚拟设备的创建可能需要几秒钟。”
各种集群管理工具提供了容器调度,包括Kubernetes[2]、谷歌的Borg[41]、Facebook的Tupperware[26]、Twitter的Aurora[21]和Apache Mesos[17]。Slacker是这些系统的补充;快速部署为集群管理器提供了更多的可用性,支持廉价的迁移和经过调优的负载平衡。
许多技术与我们共享缓存状态和减少冗余I/ O.VMware ESX server[43]和Linux KSM9扫描和重复删除内存的策略相似。虽然这种技术节省了缓存空间,但它没有预先释放初始I/O。Xingbo等人[47]也发现了这样一个问题,即读取多个几乎相同的文件会导致可避免的I/O。他们将btrfs修改为根据磁盘位置索引缓存页面,从而使用页面缓存服务于btrfs发出的一些块读取。Sapuntzakis等人对VM映像使用脏位图来标识迁移过程中必须传输的虚拟磁盘映像块的子集。Lagar-Cavilla等人构建了一个“VM fork”函数,该函数可以快速创建正在运行的VM的许多克隆。一个克隆需要的数据被多播给所有克隆,作为预取的一种方式。Slacker可能会从类似的预取中获益。

9 结论

Fast startup拥有用于可伸缩web服务、集成测试和分布式应用程序交互开发的应用程序。Slacker填补了两者之间的空白。容器本身很轻,但是目前的管理系统,如Docker和Borg,在分发图像方面非常慢。相比之下,虚拟机本身就是重量级的,但是对虚拟机映像的多部署已经进行了深入的研究和优化。Slacker为容器提供了高效的部署,借鉴了VM映像管理的思想,比如延迟传播,还引入了新的特定于docker的优化,比如延迟克隆。使用这些技术,Slacker将典型的部署周期提高了5倍,开发周期提高了20倍。HelloBench和我们在本文实验中使用的图片的快照[15]可以在网上找到:https://github.com/Tintri/hello-bench。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值