本文翻译自SOCK: Rapid Task Provisioning with Serverless-Optimized Containers.

一. 摘要

Serverless计算平台能够为用户降低生产成本以及提供良好的弹性,但container以及application的启动延迟是硬伤。针对此缺陷,这篇文章提出了一套runc平台下的(译者注:另一平台为runv或两者的折中,本文不涉及)优化解决方案SOCK,旨在提高任务的启动性能。这篇文章首先对Linux提供的各种操作原语进行性能剖析,在此基础上提出SOCK的实现,并给出性能评估。

二. 容器性能剖析

本小节回答如下几个问题:物理机如何提升容器文件系统(即挂载docker image)的密度?使用namespace进行资源隔离的开销有多大?哪些资源是必须隔离的?如何避免cgroup的创建开销?作者的实验环境:8-core m510机型,4.13.0-37内核。

容器存储

container存储提供一个独立的根文件系统,隔离I/O资源。功能需求简单明了,而实现上则是百花齐放。已有的runc容器存储解决方案丰富(如overlay,AUFS,devicemapper,btrfs等),感兴趣的可以参考docker官方文档storage dirver。本文以AUFS方案作为基线,与bind mount进行对比。bind挂载允许当前文件系统的一个文件或者目录挂载在另一处目录下。显然,bind挂载是不支持COW语义的,一般而言可以用来实现不同容器间data volume共享。二者对比如Fig.1所示,bind挂载比AUFS快了两倍。

image.png

当根文件系统挂载完成以后,接下来则是根文件系统的切换。这里主要对比了chroot 与 unshare两种方式。chroot使用指定目录作为新的根目录。unshare则是创建了新的mount namepsace,使用形态与COW类似。用户可以在新namespace里进行mount/unmount操作,修改只对当前namespace可见。灵活度很高,但同时也带来了性能问题。本文测量系统已有mount namespace的数量对新创建以及删除namesapce的影响。如Fig2.所示,随系统已有namespace数量增加,ops趋近于0,可以看到mount namespace的扩展性差。

image.png

逻辑隔离:namespace

上一小节已提到了mount namespace,那么这一小节则会涉及NET,UTS,IPC,PID namesapce。unshare允许用户创建以及切换新的namesapce,namespace类型由用户通过参数控制。当使用namespace的最后一个进程退出时,则销毁该namespace。不同数量的进程执行unshare以及退出,用ftrace来测量不同namespace类型的创建销毁开销。Fig3. 选取了开销top4的的namespace操作。 可以看到,mount 以及 IPC namespace的延迟大约在几十ms量级,通过调查发现,延迟主要是因为等待一个RCU grace period完成。由于等待上下文并没有持锁,所以并不会对吞吐有影响。事实上,由前一小节也可得知,mount namespace的最高创建ops为~1500。

image.png

NET namespace的创建以及销毁开销大,主要是因为不同net ns间共享了一把全局锁。创建时,Linux需要在持有这把全局锁,然后遍历已有的net ns。所以,系统已有的net ns越多,开销越大。销毁时,net ns还需要等待一个RCU grace period,理论上会更慢。幸运的是,此处使用了batch操作,所以平摊后的开销较小。

image.png

作者继续测量了net ns对容器创建销毁的影响。如Fig.4所示,无任何优化时的吞吐是200c/s (containers/second)。通过disable IPv6以及移除比较耗时的广播逻辑,吞吐可达400c/s;如果完全不使用net ns,吞吐可达900c/s。

性能隔离:cgroup

Linux的cgroup可以用来实现不同资源(如CPU,memory,blk I/O,net等)的隔离。主要的使用模式有两类,第一类分为四个步骤:1. 创建cgroup;2. 创建进程并attach到cgroup里;3. 进程退出;4. 删除cgroup。第二类则是通过cgroup复用,大部分场景只有步骤 2 && 3。

image.png

Fig.5对比了这两类使用模式的区别。可以看到cgroup复用相比于cgroup每次重新创建的方式,效率至少提升一倍。线程数=16时,吞吐达到峰值,这是因为系统的HT=16。

对serveless的影响

根据上述的几组观察以及实验,思考serverless的实现方案。在sererless场景,handlers可能仅依赖于一个或少量的基础镜像,所以union文件系统的弹性叠加特性并不是必须的,倾向于使用开销更小的bind挂载方式。同样的,可以使用开销小的chroot来替代mnt ns。serverless平台跑的并非service后台服务,端口静态绑定并非是必选项,所以net ns也可不使用。最后,cgroup复用对降低延迟或者提升吞吐都很有意义。

三. Python初始化开销分析

上一节分析了容器的创建删除开销,本小节则以python为例,分析runtime初始化开销。主要回答如下几个问题:Python里主流的package是哪些?这些package的初始化开销有多大?在本地的lambda worker中,缓存主流的packages是否具有可行性?

python Apps

作者对github上876K个python项目进行包依赖分析,选取了最主流的top20 packages,如Fig.6所示。其中36%的import集中在这20个包(对应PyPi源所有packages的0.02%)。这20个包可分为五类:网络框架,数据分析,通信,存储,开发。其中,网络框架类可能会被基于serverless的网络框架替换,开发类则还没有应用场景。

image.png

第一次使用某个package,需要三个步骤:download,install,import。继续使用该package,则可以跳过其中某些步骤。top20 包的初始化开销如Fig.7所示,平均初始化时间为1~13s,细分成三步:download开销1.6s,install开销2.3s,import开销107ms。

image.png

PyPi仓库

接下来分析将PyPi仓库存储在本地的可行性。PyPi仓库包含101K个packges,Fig 8. 显示了整个仓库的packages分布,不包含索引文件,总大小为~1.5TB,压缩后~0.5TB。

image.png

接下来需要回答,有多少PyPI packages可以共存安装在本地。这里不描述细节,直接给出结论,约97%的包是可以共存的,感兴趣的读者可以阅读原文。

对serverless的影响

package download和install耗时秒级,import耗时百ms级。幸运的是,大部分package仓库存储在本地是可行的。不同package具有不同的使用频率,可考虑通过pre-import部分主流package来进一步降低延时。

四. SOCK实现

本节主要涉及SOCK的设计与实现。将SOCK集成到OpenLambda,替代Docker,并使用单独的SOCK容器为python包提供缓存。SOCK的两大设计准则:一,python import 库需要低延时;二,沙箱初始化要高效率。SOCK提出了三个优化点:1. 创建瘦容器;2. 使用了更泛化的Zygote机制;3. 设计了一套三层缓存的系统,提升python包的安装速度以及import开销。

瘦容器

创建一个容器需要三步:构建root文件系统,创建通信管道,以及设置安全边界。

存储:SOCK使用bind mount 将 host的四个目录合并成为容器的根目录,见Fig.10带’F’标志的目录。所有容器的基础镜像一样,都是ubuntu系统,将它放置在RAM中。packages目录用于加速,所有容器共享。lambda code目录(只读)与 scratch 目录(可写)是私有权限。目录合成后,使用chroot进行根目录切换,并创建 init 与 helper 两个进程。

通信: scratch目录包含了一个Unix domain socket,用于OpenLambada manager 与 容器内的进程通信。该通道还被用于控制平面,如一些特权控制。

隔离:Linux 进程使用cgroup与namespace隔离。由于cgroup创建的开销较大,SOCK使用了cgroup pool 进行优化,容器创建时直接从pool中申请,容器销毁后则将cgroup释放给pool。init进程使用unshare创建一系列新的namespace,其中mount与net namespace由于扩展性差,不会新创建。

image.png

泛化的Zygotes机制

Zygotes机制最早用于安卓系统的Java应用。首先,启动一个Zygote进程,它已经预导入了大部分包;应用进程通过fork的方式从Zygote进程启动,避免了包的初始化开销。SOCK在这个理念上又向前走了一步,主要的不同点如下:1. 预导入的包类型是runtime时决定的;2. 创建多个Zygote进程,每个进程导入不同的包;3. 容器纬度;4. 不导入恶意包,保证安全。SOCK的helper进程是一个python程序,它监听了SOCK的通信端口,有两个作用:1. 预导入python modules;2. 加载lambda程序。此处细节不再赘述,感兴趣的读者可以参考原文。

Serverless Caching

SOCK使用了三层缓存:handler & install & import缓存,如Fig.12 所示。handler 缓存包含了未使用的handler容器,这些容器均处于暂停状态。它们不消耗CPU,但消耗内存,使用LRU淘汰算法来限制内存开销。install 缓存指预先在磁盘上安装的packages(占全量的97%),已安装的包通过只读方式挂载至每一个容器。import缓存主要是用来管理Zygotes。Zygotes会消耗内存,且package的使用频率随时间而变化,所以Zygotes的管理相对复杂一些。此处细节不再赘述,感兴趣的读者可以参考原文。

image.png

五. 性能测评

本节对SOCK-based 与 Docker-based的OpenLambda以及其他的一些平台做了性能对比。

容器优化测评

首先disable SOCK的各类缓存以及Zygotes,仅评测SOCK的瘦容器特性。Fig.14从延迟与吞吐两个维度进行了测评,SOCK比Docker均占优。当enable Zygotes后,SOCK吞吐又提升了3倍,如Fig.15所示。Fig.16 评测了SOCK使用pause/unpause之后的延迟性能,相比于仅使用Zygotes,延迟从32ms下降到3ms,提升了一个数量级。

image.png

image.png

image.png

python包优化测评

关于package,SOCK实现了两个优化:Zygotes预导入(import缓存),以及预安装(install缓存)。优化前后对比如Fig.17所示,优化提升了45倍,延迟从秒级降低至20ms。Fig. 18评估了三层缓存的延迟CDF,三者叠加效果最好。

image.png

image.png

扩展性

该测试中构造了100K个packages,测试不同的handler数量(100/500/10k)下的延迟CDF,三种场景(代表了小中大工作集)分别测试到了handler/import/install缓存的加速能力。

image.png

真实workload测评:图像缩放

测试case:首先从AWS S3读取镜像,然后使用Pillow包进行缩放,最后再写回AWS S3。如Fig. 21所示,SOCK 与AWS Lambda 以及 Open Whisk进行对比,其中SOCK占优。其中SOCK cold+与SOCK cold的区别在于前者对Pillow进行了预热。

image.png

六. 译者总结

SOCK是由若干优化手段叠加而成的一套容器优化方案,其中缓存加速本质是通过空间(本地磁盘或者内存)换时间(低延迟高吞吐),优化理念也并非完全新鲜(安卓的Zygote,AWS的container reuse,kata的vmtemplate等)。SOCK的价值在于对这些优化理念进行了加工,整合形成一套适用于runc平台下,为Serverless服务的容器解决方案。SOCK对实际生产环境的指导价值则由读者来评判,感兴趣的同学请移步原文