type
status
date
slug
summary
tags
category
icon
password
摘要
无服务器平台面临着一个折衷:容器启动时间和预分配的并发(即缓存实例)之间的折衷,并且这个需求由于远程容器初始化的频率要求而被不断扩大。这一篇文章主要介绍了MITOSIS这一提供快速远程fork的OS原语。这一OS原语利用了OS内核和RDMA的协同设计。通过充分利用RDMA的快速远程读的能力与无服务器容器之间的部分状态转移,MITOSIS弥补了本地和远程容器初始化之间的性能差距。MITOSIS首次实现从一个实例中、在多个机器之间、在1s之内、fork出超过10000个新容器,同时还允许新容器可以实现fork出的新容器的预物化状态的高效转移。我们已经在linux中实现了MITOSIS,并将其与FN(一个流行的无服务器平台)集成。在现实世界无服务器工作负载的峰值负载下,MITOSIS借助数量级更低的内存使用,降低了89%的函数尾延时。对需要状态转移的无服务器工作流而言,MITOSIS将其执行时间(或者说执行效率)提高了86%。
1 简介
无服务器计算是一个新兴的云计算案例。主要云服务供应商都提供这一服务,包括AWS Lambda, Azure Functions, Google Serverless, Alibaba Serverless Application Engine与Huawei CloudFunctions。它的一个重要的优点是自动扩展——用户只需要提供无服务器的函数,然后无服务器平台自动分配算力(如容器)来执行这些函数。
从这里就能看出来MITOSIS原语的重要性了,因为对一个无服务器平台而言,是经常需要动态分配容器的,因此需要MITOSIS原语实现高效的容器fork
自动扩展使无服务器计算更加经济实惠:无服务器平台仅在函数被执行的时候收费(在空闲时间是不会收费的)
也就是用户真正占用算力的时候才会收费
然而,冷启动是快速自动扩展的一个瓶颈,因为对较短的无服务函数而言,容器启动时间(超过100ms)可能比函数执行时间的数量级更高。
这个也很好理解,因为自动扩展是在需要算力的时候分配容器,但是如果每一个容器都需要从头启动的话,延时自然就高了。
这里又能看出来MITOSIS原语的重要性,因为如果能借助Linux fork的机制,就可以实现fork出的容器与被fork的容器状态一致,这样就不需要进行冷启动了。
加速冷启动已经成为了学术界和工业界的热点话题。大部分的解决方式是通过预分配的并发实现使用“暖启动”。然而,“暖启动”需要使用大量的资源,例如,当将函数扩展到分布式设置时,每一个分布式机器上都需要部署很多缓存容器。
这是因为在分布式的应用场景下,可能每一个机器都需要执行某一个函数,而按道理来说某一个函数所需要的执行容器可能又是不一样的,这样就使得每一个物理设备上可能需要很多不同的缓存容器才能实现执行各种各样的函数。
不幸的是,将函数扩展到多个机器上执行是很常见的,这是因为一个单独的机器的执行函数的能力是有限的,这无法及时处理负载的峰值。考虑从Azure Functions的真实追踪中采样的函数。函数9a3e4e的请求频率会突然增加超过150k次调用/min(增加速率为1min 33000倍)。
从图中也可以看出来,在无服务器的平台上,当函数调用变多的时候,分配出的物理机器(容器)也会越多
为了避免新到来的函数调用被阻塞,无服务器的平台需要在多个机器之间立即启动足够的容器。
也就是上一个callout所说的现象。
由于无服务器工作负载不可预测的特性,这使得无服务器平台难以决定“暖启动”需要多少个缓存容器。因此,对这类资源也不是免费的:商业平台会要求用户保留这些资源并支付一定的费用,以获取更好的性能。
相当于就是要为预分配的缓存买单了,换句话说,用户还需要为空闲时间的缓存容器的浪费买单。
更糟糕的是,在分离容器中运行的相关函数不能直接转移状态。相反,他们必须通过消息传递或者用于状态转移的云存储,这就导致了数据的序列化/反序列化,内存拷贝以及存储栈的额外开销。近期的报告表明,这些操作会占用函数执行时间的95%。不幸的是,函数之间的状态转移在无服务器的工作流中也是很常见的——一种将函数组合成更复杂的应用程序的机制
这里应该是说serverless workflows就是这样一种机制。无服务工作流应该就是在一个容器上运行多个函数。当这些函数之间是相关的时候,在serverless workflows中就会将这些函数转换为一个整体,即应用程序。
尽管最近的研究都通过将局部函数放在同一个容器中,绕过了这些状态转移导致的额外性能开销,但未来需要如何处理这个问题仍然不清楚。
上面所说的开销是相关函数在分离容器执行时,函数进行状态转移时带来的额外性能开销,目前的研究的处理方式都是,既然这个开销是分离容器导致的,那么就把这些相关的函数都放在同一个容器中,这样转态转移的开销就不会出现了。文章中说到的unclear应该是指,这种模式并不灵活,因为这样可能不太好实现服务器的负载均衡(因为应用程序的粒度比较大),以及可能将相关的函数安排到同一个容器中也会导致一些开销。
我们认为远程fork是一个很有前景的原语,他可以使函数的部署更加高效,也可以使加快函数的状态共享。首先,fork机制已经被证明,在一个机器上,性能是可观的,启动一个容器所使用的资源也是较少的:在1ms之内,一个缓存容器就足以启动(或者说fork)大量的容器。通过将fork机制扩展到远程,一个活跃的容器就足以在所有的机器上启动大量的容器,这也就在分布式的场景下,实现了非预分配的并发。第二,远程fork为两个远程函数提供了透明的中间状态共享:通过fork创建出的容器中的code可以透明地获取被fork容器中的预物化状态,这也绕过了消息传递和云存储。
这里大概就是我对状态共享的理解,透明就是因为fork出来的容器跟被fork的容器一样,那么自然就可以透明访问预物化状态了。
然而,先进的系统只能通过检查点/回滚技术实现一个保守的远程fork。我们的分析表明,这些远程fork对无服务器计算而言并不高效(由于需要对父容器内存设置检查点文件、通过网络传输该文件并在分布式文件系统中访问这些文件将导致额外的开销,使得这种远程fork甚至比冷启动更慢)。尽管我们使用了先进的信号线以减少这些开销,但检查点的软件开销以及分布式文件访问仍然使C/R(检查点/回滚)模式的远程fork无法充分利用RDMA的低延迟、高吞吐量的特点。
我们提出了MITOSIS。MITOSIS是一个OS原语。它通过OS和RDMA的协同设计实现了一个快速的远程fork。设计的关键点在于,OS可以通过RDMA-capable NICs可以直接访问远程机器的物理内存。得益于RDMA可以绕过远程机器的OS和CPU,这种远程fork的速度极快。因此,我们可以模仿本地的fork来实现远程fork,具体的方式就是,通过将子容器的虚拟内存映射父容器的物理内存中,这样就不需要在内存中设置检查点了。
在现代OS中,fork通常采取一种机制:写时复制。也就是在fork之后,子进程的虚拟地址空间被映射到父进程的物理地址空间中(实际上也可以理解为页表的拷贝),然后在需要对该地址空间进行写操作时,才会进行拷贝操作。
这里的操作应该也是一样的,先将分布式系统中的机器的内存视为一个内存,那么远程fork的操作就是将子容器的虚拟地址空间映射到父容器的物理地址空间上,然后在写的时候再修改页表实现将远程数据拷贝到本地。
子容器借助写时复制机制,使用RNIC,可以直接读取父容器的内存。这样就绕过了由传统C/R引入的软件栈(如,分布式文件系统)
为了远程fork,充分利用RDMA和内核带来了一些新的挑战:
- 快速可扩展的,支持RDMA连接的建立过程
- 父容器物理内存的高效访问控制
- 高效的父容器生命周期管理
这些部分再第四章会再次提到
MITOSIS通过下面这些方法解决了这些问题:
- 改进先进RDMA的特征(即DCT)
从文章的后面可以看见,原有的RDMA是使用面向连接的传输,这样就会导致连接建立将会耗费更多的时间。因此在MITOSIS中采用了DCT以减少连接建立的时间
- 提出一个新的、基于连接的、为远程fork设计的内存访问控制方法
- 基于无服务器平台的协同设计容器生命周期管理
实际上跟上面的挑战是一一对应的关系
我们也引入了包括generalized lean container的技术,以减少远程fork时产生的容器化开销。总而言之,我们展示了,在无服务器的计算中,远程fork基于商业级的RNICs,是很高效、可行并且实际的。
我们在linux内核中实现了MITOSIS。我们使用Rust编写了该原语的主要功能,并将其作为了一个可装载的内核模块。该原语可以在五台机器上,在0.86s内远程fork 10000个容器。MITOSIS原语与主流的容器完全兼容,使该原语可以和现有的、基于容器的无服务器平台无缝衔接。为了证明该原语的效率和功效,我们将MITOSIS原语与Fn这一流行的开源无服务器平台结合在一起。在真实的无服务器工作负载的峰值下,MITOSIS借助更低数量级的内存使用,将峰值函数中第99百分位数的延时降低了89%。对真实场景中要求状态转移的无服务器工作流,MITOSIS减少了86%的执行时间。
贡献。我们强调的贡献如下:
- 问题:分析了现有容器启动技术的性能-资源配置权衡,以及函数之间状态转移的成本
也就是上面提到的启动开销和关联函数在不同容器中运行的状态转移开销。
- MITOSIS:一个RDMA和OS协同设计的远程fork,其可以在远程机器上快速启动容器,而不进行资源的预分配。他也可以使函数的状态转移更加高效
- 演示:一个在Linux中与Fn结合的实现,以及对微基准测试和实际无服务器应用程序的评估都证明了MITOSIS的有效性。MITOSIS的仓库位于:
2 背景与动机
2.1 无服务器计算与容器
无服务器计算是一个流行的编程范型。他从开发者的角度抽象了资源管理:开发者只需要通过流行的编程语言(如Python)编写类似函数的应用程序,然后将这些函数(作为容器镜像)上传到无服务器平台上,并且指明如何调用这些函数。无服务器平台可以根据函数请求、通过动态创建容器实现自动扩展来处理每一个函数调用。被创建出来的容器也会在函数返回后被自动回收,这使得无服务器平台非常经济:开发者只需要为正在使用的容器付费。
容器是一个流行的用于执行函数的主机。它只需要将应用的依赖打包进一个单独的镜像,进而简化函数的部署。此外,它还通过linux的cgroups和namespace提供了轻量的隔离机制,这些机制对应用在多用户的环境中运行十分有必要。不幸的是,由于容器的引导程序和函数地址空间的隔离,使能(或者说启动?)容器引入了额外的函数启动开销和状态转移开销。
2.2 启动和资源预分配的开销
冷启动的性能开销
从头开始启动一个容器,即“冷启动”,是十分缓慢的。这个启动过程包括了:拉取容器镜像、设置容器配置以及初始化函数语言的运行时环境。上述的所有步骤的性能开销都很大,甚至可能会花费超过上百ms。因此,冷启动可能会主导短暂的无服务器函数的端到端延时。例如,Lambda@Eedge报到:其67%的函数在20ms内就能运行结束。相比之下,通过runC(一个先进的容器运行时)启动一个Hello World的python容器,若镜像被存储在本地,将花费167ms;若镜像被存储在远端,将花费1783ms(在Table1中可以看到这些数据)。
由于预置并发导致的暖启动资源开销
大量的研究都聚焦于通过暖启动技术减冷启动的启动时间。然而,他们必须支付更多的资源配置成本(暖启动在Table1中应该对应的是Caching和Fork):
Caching
通过缓存已经使用结束的容器,而不是直接回收他们,这样的话,未来的函数就可以直接重用被缓存的容器(如通过Docker的unpause函数),而在这种情况下就几乎没有启动开销了(少于1ms)。然而缓存会大量消耗内存资源:预分配的资源——即缓存实例的数量(O(n)),应该与并发函数的数量匹配(n)
那我上面的理解应该是错的。上面我认为一个函数就要对应一个特定的容器。而通过这一句话可以看出来,一个容器应该能执行一类函数(这些函数的依赖可能是相互兼容的?),这样的话容器的数量自然就应该跟并发执行的函数的数量相同了。在下面也有解释为什么需要跟并发函数的数量匹配。
因为一个暂停的容器只能为一个函数取消暂停(应该意思是指容器无法同时执行多个容器镜像,也就是函数及其依赖)。
我上面可能就是对这句话的理解有问题,错误理解为了一个特定的函数,只能在一个特定的容器中运行
考虑到函数调用数量的不可预见性,
一个容器需要对应一个函数,这样所需要缓存的容器数量也不确定了
对开发者或者平台而言,很难决定需要缓存多少个容器实例。因此,容器缓存策略不可避免地面临着快速启动和低资源预分配之间的折衷,这就导致了大量的缓存丢失。
Fork
这里应该指的是本地fork
一个缓存容器(在fork的场景下被称为父容器)可以调用fork系统调用来启动一个新的容器(子容器)。
能使用fork是很正常的,因为fork通常是fork进程。而在一个物理机器上,一个容器一般就是以进程的形式被执行的
由于fork可以被调用很多次,因此每一个机器只需要一个缓存实例来fork新的容器。因此fork减少了Caching策略的资源预分配——需要缓存的容器数量从O(n)降低到了O(m)
这里的m是一定小于n的。当没有使用本地fork时,需要缓存的容器数量就可能大于m。比如在这种情况下,可能一台物理机器上,我需要3个Function并发执行,但是这个时候只能通过cache 3个容器来解决;而在使用了fork的情况下,同样需要在这一台机器上并发执行三个函数,就只需要在这台物理机器上cache一个容器,然后需要执行这三个函数的时候再fork出来两个就好了。因此空间复杂度从Function的数量降低到了机器的数量。
其中,m是函数启动执行需要的机器的数量
这里应该是指提供某一次服务的时候分配出来的机器数量,在这些机器上Function都可以并发执行。而上面的函数数量是指需要并发执行的函数数量
然而,这仍然是与机器数量成比例的(即空间复杂度为O(m)),因为fork无法推广到分布式场景下
因为fork只能本地fork。所以后面就有了remote fork
Checkpoint/Restore
C/R通过存储在文件中的容器检查点启动一个容器。这只需要O(1)的资源(也就是容器检查点文件)来进行暖启动。因为如果必要的话,文件可以通过网络进行传输。尽管在资源使用上已经进行了优化,但是C/R比Caching和Fork慢了几个数量级。我们将在第三章中详细分析。
这个结果也能在Table1中看出来一点点,就是在本地的情况下,C/R机制的耗时为Caching和Fork的5倍左右
2.3 (远程)状态转移开销
在无服务器的工作流中,在两个函数之间进行状态的转移是很常见的。工作流是描述函数之间的生产者-消费者模型的图形。
那就跟我最开始的理解是一样的,函数的相关性就是一些同步互斥的关系,但是由于这些函数在不同的容器中,所以不太好进行通信。
考虑到图2中所示的真实示例FINRA。这是一个经融应用,他会根据交易和市场数据来验证交易。上游函数(产生状态的函数,即fetchPortfolioData与fetchMarketData)会首先从外部源中读取数据。之后,他们会将结果转发到很多的下游函数(消费状态的函数,即runAuditRules)。这些下游函数为了获取更好的性能,将并发地处理这些状态。
感觉前面对状态这个词的理解有问题,看这里状态好像是生产出来的数据或者是函数的执行结果?也可能是像信号量一样的东西,就是上游函数发一个post释放一个“状态”,而下游函数通过调用pend消费一个“状态”
在不同的容器中运行的函数如果要进行状态转移,要么需要借助网络,通过信息传递进行状态拷贝,要么就需要在云存储服务上进行状态交换。
上面这两种方式说简单一点,就是通过网络进行消息传递,另一种就很像“共享内存”机制
图3展示了在AWS Lambda上运行FINRA的伪代码。对较小的状态转移(小于32KB,如Portfolio),Lambda将在消息中装载状态,并在协调者和函数容器之间进行消息交换。对较大的状态(如Market),函数必须通过S3——Lambda的云存储服务进行交换。
从这里也能看出来,文章中的状态应该就是指函数的产出数据,或者说函数的执行状态(返回值),所以后面还是不用状态转移这个翻译了,还是说状态转发吧
通过消息或云存储进行状态转发不可避免的面临着数据序列化、内存拷贝以及云存储栈所带来的额外性能开销,将降低1000倍的运行速度。为了应对这个问题,现有的工作提议需要优化无服务器平台上的消息传递原语或者存储系统,但是上面所提到的所有的额外性能开销都没有被完全消除。Faastlane通过threads将函数共同定位到同一个容器中,以便通过共享内存绕过这些额外的性能开销。然而线程不能推广到分布式的情境下。在上游函数和下游函数在不同的机器上的情况下,Faastlane减少了消息传递。SPRIGHT通过改造eBPF取得了相近的效果。然而,他们都不能在节点之间支持有效的数据共享。
说白了就是在状态转移方面的很多工作仍然存在一些额外的性能开销
3 用于无服务器计算的远程fork
我们将展示远程fork的下面的两个优点,已解决在上一个部分所提到的问题。
高效的(远程)函数启动。
或者说远程fork可以实现高效的容器启动
当将fork这个原语扩展到远程的场景下,单个父容器已经足够跨集群启动后续的子容器,这类似于C/R(详见Table1)
这里的详见table1应该是指Table1中指出的空间复杂度为O(1),表示在整个无服务器平台上,仅需要缓存一个容器(C/R机制是的O(1)是指容器内存检查点文件)
我们相信O(1)的资源预分配是对开发者和用户都是有利的,因为他们只需要明确指出他们是否需要资源以进行暖启动,而不是明确指出需要多少(如用于fork的机器数量,或者用于缓存的,缓存实例的数量)
快速透明的(远程)状态转移
fork原语从根本上连接了父容器和子容器的地址空间。
还记得吗fork原语采用的策略是:写时复制。在无服务器平台的应用场景下,就是将父容器的地址空间映射到子容器的地址空间,然后进行写的时候再进行拷贝。
在父容器的内存中,需要转发的状态已经被预物化了
换句话说就是父容器的地址空间中就已经有需要转发的状态了
因此子容器可以通过共享内存抽象无缝的访问这些状态,并且不需要进行数据的拷贝,也不需要进行拷贝(对只读的访问),也不会带来云存储的开销。与此同时,fork原语写时复制的语义避免了传统分布式共享内存系统开销极高的内存一致性协议。
图3b展示了一个具体的、FINRA中使用fork来转发market数据的例子。在这个例子中,所有的函数都被打包到同一个容器中。在这种情况下,有一个协调器负责将函数请求分派给用户实现的函数。
在图3中,较小的数据(portfolio)仍然通过消息进行传递,而较大的数据就通过一个容器进行fork(父容器是图中的第二个1号容器)。
到这里应该就能解决我上面的疑问了,这里是将所有函数所需要的依赖都装载到同一个容器中,这样直接fork该容器,其子容器就可以执行所有的函数了。
或许想到这里还会有一个问题,就是怎么确定函数的依赖集?需要注意的是,在使用无服务器平台的时候,一般要先提供所有需要执行的函数,并将这些函数打包成镜像交给平台执行。所以对某一个用户而言,他所需要的所有函数依赖是能确定的。
在这里也不应该将orchestrator称为协调器,看后面的文章内容orchestrator应该是指父容器。那么这里说的发派函数请求实际上就是fork出来几个子容器来执行用户自定义的函数。
我们进一步假设coordinator向orchestrator发布请求的时候是fork-aware的:基于在工作流图中的函数依赖(如图2中展示的),coordinator(协调器)在必要的情况下会请求父容器fork子容器。在父容器执行了fetchMarketData之后,父容器会fork出子容器以执行下游函数(runAuditRule),这样就可以直接访问由父容器预物化的global_market_data(可以结合图3来看)了
挑战:远程fork的效率
这里就是谈现有的remote fork机制
据我们所知,现有的容器只能基于C/R机制实现远程fork。为了fork出一个子容器,父容器首先需要为他的状态设置检查点(如寄存器的值和内存分页),并将这些信息拷贝到文件中。
也就是需要设置memory checkpoint。在这里的状态可能就不是上面所说的函数返回值啥的了?但是仔细想想,函数返回值本身就是存储在内存中的,所以对我自己理解的状态再进一步扩展:表示容器的状态。父容器中的函数和子容器中的函数的同步互斥关系,本质上就是两个容器之间状态的关系,如果子容器知道父容器是什么状态(如知道内存空间。缓存什么的),那么自然就能实现两个容器内部函数的同步互斥了。
此外这里说的拷贝文件就会带来上面所说的内存拷贝的开销。
然后需要将这些文件转发给子容器。这有两种实现方式:一是通过远程文件拷贝(详见图5中的CRIU-local),二是通过分布式文件系统(详见图5中的CRIU-remote)
在接收到这些文件之后,子容器需要通过从检查点文件中加载容器状态的方式恢复父容器的执行状态。
那么这种remote fork如果单纯从结果上来看,就是一个fork机制,就是将父容器直接拷贝到子容器中,然后再启动父容器。
需要注意的是,C/R可能会按需加载一些状态(例如,内存页)以获得更好的性能。
需要注意的是这里说的更好的性能应该是指在从检查点文件中恢复父容器的性能,而不是在进行检查点文件传输的时候的性能。
不幸的是,基于C/R的远程fork对无服务器计算并不足够有效。图4展示了在使用CRIU(一个linux中先进的C/R,用于实现CRIU-local和CRIU-remote,已经被精细地优化过,详见第7章)的情况下,无服务器函数在远程机器上的执行时间。
合成的函数随机的占用父容器的内存。
这里应该是指占用内存的大小是随机的
我们发现当子容器需要访问1GB的远程内存时,基于C/R的远程fork甚至会比冷启动慢2.7倍
这里说的访问远程内存应该就是指去访问通过检查点文件恢复出的父容器内存。虽然通过检查点文件可以恢复出父容器的内存布局,但是如果要访问父容器的内存空间的话就需要子容器去做remote access
我们把这个现象归因于下的一个或若干个因素:
- 检查点容器内存
在本地或分布式文件系统中,CRIU将花费9ms(518ms)与15.5ms(590ms)的时间来检查父容器1MB(1GB)的内存。造成这个额外开销的主要原因是将内存拷贝到文件中:在这种情况下与本地fork不同,子容器的OS位于另一台物理设备上,因此,使用C/R机制缺乏对父容器内存页的直接内存访问能力。
- 拷贝检查点文件
对CRIU-local而言,从父容器向子容器转发1MB-1GB镜像的整个检查点文件将花费11ms-734ms(相比之下,函数执行时间只需要花费0.61ms-570ms)。这整个文件的拷贝的过程通常情况下是没有必要的,因为无服务器函数通常只需要访问父容器的部分状态(详见图16b)
区分一下上面这两点的区别:第一点是说父容器的内存空间过大会导致对应的检查点文件过大,第二点是说检查点文件中会携带一些无用的信息。
- 额外的软件恢复开销
CRIU-remote支持按需文件传输:它在出现page fault时只会读取需要的远程文件页。然而,由于每一次的page fault都需要进行一次深度优先搜索来读取对应的页,因此CRIU-remote的执行时间会比CRIU-local慢1.3-3.1倍:DFS延时(100us)比本地文件访问的延迟高得多。更重要的是,由于软件开销,这个延时比一个RTT(3us)高得多
这里是在说进行remote fork的时候会出现的额外软件恢复开销。
4 MITOSIS操作系统原语
机遇:内核空间的RDMA
远程直接内存访问被广泛部署在数据中心中,它是一种快速的网络特性。尽管RDMA经常被使用在用户空间中,RDMA进一步给予了内核可以绕过远程机器的CPU直接读/写远程机器物理内存的能力(即单边RDMA READ)。而RDMA借助这一机制就能实现低延时(2us)以及高带宽(400Gbps)
这里说的经常被使用在内核空间中可能是指,通常情况下RDMA都是用作文件传输,而这些文件传输通常情况下是应用程序发起的,所以是“经常被使用在用户空间中”。
这里的后一句话说白了就是一台机器上的OS可以绕过远程机器的CPU和OS,直接访问远程机器的内存。
方法:借助RDMA模仿fork
MITOSIS借助RDMA,通过模仿本地fork,实现了一个有效的远程fork。图5c展示了一个概述。首先我们会拷贝父容器的元数据(如页表)到一个精炼的描述符中,以fork一个子容器。需要注意的是,这与C/R不同,我们不需要拷贝父容器的内存页到这个描述符中。
在传统的C/R机制中,在检查点文件中需要保存一些必要的寄存器内容以及内存页,而在MITOSIS中,仅需要保存元数据。这样就解决了C/R机制的第二个问题——检查点文件过大。
另外这里需要做一个纠正,C/R机制中的R应该不是回滚,而就是恢复(是指子容器需要根据检查点文件恢复父容器的执行)
这个描述符后续会通过RDMA拷贝到子容器中,以恢复父容器的元数据,这类似于本地fork的copy_process。
在本地fork中,进行fork的也只是将父进程的页表拷贝给子进程,而不是整个地址空间的拷贝。这样看的话C/R机制就像是传统的fork机制,是直接拷贝整个地址空间和页表的;而MITOSIS就是现代的fork机制,只需要拷贝页表,然后进行写时复制。
在执行过程中,我们将子容器的远程内存访问配置为触发页面错误,并且内核将会读取相应的远程页面。
这一句话有点不太明白。我理解的意思是如果需要进行远程内存访问的话,就会触发一个page fault,然后再处理这个page fault的时候就会去使用它RDMA去访问远程机器的内存
错误处理函数将会被按需触发,这就避免了转发整个容器的状态。
也就是说只有出现Fault的时候才会执行错误处理函数。
与此同时(这里应该是指出现page fault的时候),MITOSIS会直接使用单边RDMA READ,以读取远程机器的物理内存,通过这种方式就可以绕过所有的软件开销。
回忆一下上面所说的软件开销(对C/R而言):内存拷贝(这个拷贝量和父容器的内存大小有关,并且检查点文件中有很多无用的信息)、文件传输、文件读取(需要使用DFS处理page fault)。但是在使用remote fork的时候,由于仅需要拷贝元数据,所以解决了内存拷贝所带来的性能开销。后面的文件传输和文件读取都通过RDMA解决,由于RDMA可以绕过远程机器的CPU和OS,所以后面两个步骤所带来的软件开销也被极大地减小了。
架构
我们的目标是去中心化的架构——任意一个机器都可以从其他的容器中fork出来,反之亦然。需要注意的是,我们不需要使用专用资源(如被pin住的内存空间)以fork容器,
在通常情况下,为了得到较高的性能,会将部分资源pin在内存中,这样这些资源就不会被交换到磁盘上了。但是在MITOSIS的使用场景下,子容器可能会出现page fault,然后通过RDMA读取远程机器的内存。由于RDMA读取的内存可能不在内存中,那么会在远程机器上再次触发一个page fault,然后将数据交换到内存中,再使用RDMA。这样这个过程就不要求父进程中的数据是被pin在内存中的了(当然这也是我的猜测,后面应该会解释)
因此,非无服务器的应用程序可以与MITOSIS共同运行。
最直观的理解就是,MITOSIS并不会占用某些专用资源,因此传统的非无服务器的应用程序不会受到资源分配的限制。
这里举个反例可能更好理解一点,如果MITOSIS原语占用了某些专用资源,那么在非无服务器的应用程序运行的时候,就需要特殊考虑MITOSIS所占用的专用资源,进而就需要修改运行无服务器应用程序时的逻辑了。
而现在这种MITOSIS由于不占用专用资源,因此非无服务器应用程序的执行不会受到任何影响(或者说MITOSIS的存在对非无服务应用程序的运行不会造成任何影响)
我们在内核中增加了四个组件以实现MITOSIS(详见图6):
- fork协调器预演远程fork的执行(5.1&5.2)
不知道这里的协调器是不是上面那个运行FINRA的例子。但是需要注意的是,上面的例子是使用共享内存、消息传递、co-locate这些机制实现的,这里是MITOSIS。
并且需要注意的是,这里协调器是用于处理远程fork请求的,所以发起请求的是父容器。那么这样看的话我上面的理解又是错误的,上面我认为orchestrator就表示父容器,但是从这张图中就能比较清楚地看到,父容器是向fork协调器发送远程fork请求的。
- 用于管理一个可扩展的RDMA连接池的网络守护进程。这个RDMA连接池用于实现不同机器上的内核通信(5.3)
- 我们扩展了OS的虚拟内存子系统以借助RDMA使用远程机器的内存(5.4)
这里应该就是说将虚拟地址扩展了,一些虚拟地址映射到了远程机器的内存物理地址上,这个也是比较常见的操作
- 最后,备用守护进程提供了RPC(remote process call,远程进程调用)处理机制以便恢复少量的、无法使用RDMA的远程内存访问
这句话比较拗口,大致意思就是,RDMA没办法进行所有的远程内存访问。因此还有一个fallback守护进程存在,他负责处理无法使用RDMA的远程内存访问。
安全模型
我们保留了容器的安全模型,即OS和硬件(RNIC)都是可信的,但可能存在恶意的容器(函数)
4.1 挑战与方法
有效、可扩展的RDMA连接设置
尽管RDMA速度很快(甚至可以达到2us),但是过去RDMA只支持面向连接的传输(RC)。在面向连接的传输中,连接的建立非常缓慢(可能会达到4ms,还会限制700个连接/秒的吞吐量)。在其他机器上缓存连接可以缓解这个问题,但是当支持RDMA的集群扩展到超过10,000个节点时,这是不切实际的。
首先需要知道的是网络连接在无服务器平台上是十分常见的,因此如果所有的连接都通过缓存的话,那不是又有点像暖启动了,也就是所有的机器上都缓存了很多个连接,这样就变成预置并发了(但是这样感觉最起码会比直接使用暖启动好)。
此外4ms这个数量级与函数执行时间相比,占比还是有可能比较大的。。。
我们改写了DCT以便执行内核之间的通信。DCT是一个未被充分利用,但是被广泛支持的RDMA特性。它具有高速且可扩展的连接设置(在5.3章中介绍)。
这里有一点点疑问,就是在简介中说MITOSIS可以绕过远程机器的OS和CPU,但是这里又说使用MITOSIS需要两个内核之间进行通信,所以MITOSIS到底会不会绕过OS?
高效的远程物理内存控制
MITOSIS将父容器的物理内存暴露给子容器以便实现最快的远程fork。然而,这种方式在极端情况下会引入一致性问题。如果OS修改了父容器的虚拟地址和物理地址之间的映射关系(如进行内存交换),那么子容器就会读取不正确的内存页。
这里说的修改虚拟地址和物理地址之间的映射关系实际上就可以理解为修改页表。另外上面提到了MITOSIS只会将内存元数据传输给子容器,因此子容器应该是只有父容器的页表的。那么当父容器的页表被父容器的OS修改的时候,就会导致父容器和子容器二者的页表不同。这样在子容器访问父容器的内存空间的时候,首先需要父容器机器上的CPU或者OS去初始化一下DMA控制器,在初始化的时候会进行虚拟地址到物理地址的转换,这样就会导致子容器读取的和实际需要的数据不一致的情况了。
用户空间的RDMA可以使用内存注册(MR)机制来进行访问控制。然而MR有不小的注册开销。进一步来说,内核空间的RDMA已经限制了对MR的支持——在内核空间中,仅支持RCQP上的MR(以及FRMR)。
我们提出了一种无需注册的内存控制方法(5.4),这种方法可以将RNIC的内存检查转换为连接权限检查。我们进一步通过充分利用DCT可扩展的连接设置特性,来使这种检查更加高效。
父容器的生命周期管理
为了保证正确,我们必须保证被fork的容器(即父容器)是有效的,直到他的所有继承容器(包括从子容器中fork出来的子容器)都执行结束。一个很自然的想法是让每一个机器去跟踪这台机器上的父容器的所有继承容器的生命周期。然而,这会带来巨大的管理负担:一个父容器的继承者可能会在多个不同的机器上,最终将会形成一个分布式的fork tree。与此同时,每一个机器都可能会有多个树。
因为一台机器上可能会运行多个父容器,而每一个父容器又对应了一个fork tree。
因此,每一个机器都需要沿着fork tree的路径广泛地与其他机器进行通信,以保证父容器可以被安全的回收。
为了实现高效的父容器生命周期管理,我们在无服务器平台上装载了一个生命周期管理器(6.3章)。
有点怪,这个不能也使用RDMA进行管理吗。我的想法是还是由每一个主机去管理,但是主机管理的时候并不用一直沿着fork tree向下通信,检查继承者容器是否执行结束,而是只需要记录儿子容器的数量,然后当儿子容器执行结束的时候,就向父容器发送一个状态,以表示子容器执行结束,然后父容器中记录的儿子容器数量—,这样不就能通过递归回溯的方式实现父容器的回收了吗?
在上面的广泛通信可能是指当我想要回收父容器的时候去check一次,这样就可能每一条fork tree的路径上有多次通信,但是如果用我上面的方法,每一个fork tree都只会通信一次(当然我不知道文章所说的是不是就是我说的这个意思。。。)
后面看到6.3的时候应该就能明白了
观察发现,无服务器协调器(通过fork调用函数的节点,或者说就是父容器所在的机器?)自然地维护了被fork容器(也就是父容器)的运行时信息。因此,他们可以轻松地决定何时收回父容器。
到这里我还是对这个无服务器协调器没什么概念,可能需要看到后面具体实现的时候才能2明白这是什么含义吧。。。
5 设计与实现
为了简化,我们首先假设fork只会有一跳(也就是没有连续fork,或者说不会出现子容器fork子容器的情况),然后再扩展到多跳fork(5.5)
API
我们将fork区分成了两个阶段(见图7)
- 用户可以首先调用fork_prepare以生成父容器的元数据(也就是描述符),这个元数据与远程fork有关。描述符由本地唯一的handle_id和key(将fork_prepare将会返回这些信息)以及父机器的RDMA地址进行全局标识
这三个对描述符进行全局标识的东西在后面fork_resume中会使用到。
这里说的全局标识应该是指在整个分布式系统中做全局标识。handle_id和key可以在本地区分该描述符,而如果在分布式系统中,可能不同的机器上会存在两个元数据描述符,他们的handle_id和key是相同的,这样如果不提供额外的信息,就无法在分布式系统中区分这两个描述符了,所以需要通过handle_id、key以及父机器的RDMA地址在整个分布式系统中做全局标识
- 基于这个标识符(或者说描述符),用户可以通过fork_resume在分布式系统中的一个机器上启动一个子容器(这个机器可以与父容器所在的机器相同,这种情况下就是本地fork了)
相较于传统的单阶段的fork系统调用,一个两阶段的fork API(prepare与resume)——类似于Caching机制中的pause和unpause,对无服务器计算而言是更灵活的。例如,在协调器上准备并且记录了父容器的标识符之后,后续启动子容器的时候就不需要与父机器通信了。
好好好,我前面的理解还是错的,这个协调器是一个独立于所有机器之外的东西吧,然后每一个父容器的标识符可能都需要在这个协调器中注册一下。这样需要fork子容器的时候就只需要从协调器中获取父容器的描述符,而不需要向父机器索要父容器的描述符了。
父容器数据结构的可见性
在默认情况下,MITOSIS会在调用fork_prepare后向子容器暴露所有的父容器的数据结构——包括虚拟内存和文件描述符。MITOSIS可以引入APIs使应用程序限制暴露的范围,但是目前来看,我们发现这是没有必要的:父容器必须相信子容器,因为他们是来自同一个应用程序的。
在这里可以加深一下对无服务器平台的认识了。在前面我一直认为是用户只能提供函数,但是从这里看用户还可以提供一整个应用程序,只不过在这种情况下,这个应用程序中的函数就有状态转移的需求了。
5.1 Fork prepare阶段
从这里开始就是介绍内核中的fork orchestrator了
fork_prepare会捕获父容器的状态,然后生成一个本地的、在内存中的数据结构(容器描述符)。这个容器描述符包含了以下几个部分:
- 包含cgroup的设置以及命名空间标志——这些信息用于进行容器化
上面提到了容器会使用cgroup和namespace等机制,以实现资源隔离,所以这一部分信息应该是跟MITOSIS没什么关系的,这是容器初始化的时候所必须的信息。
- CPU寄存器的值——这些信息用于恢复执行状态
在C/R机制中也会保存CPU寄存器信息,这个也是必要的信息。但是需要注意的是C/R机制可能会保存某些内存页,这个是不需要的信息。在MITOSIS中可以通过page fault+RDMA实现读取远程机器的内存页。
- 页表和虚拟内存域(VMAs)——这些信息用于恢复虚拟内存
- 已经打开的文件的信息——这些信息用于恢复IO操作。
在OS中,已经打开的文件有一部分会被被缓存到内存空间中,然后通过虚拟地址进行访问;而还有一部分文件仍然在磁盘上,但是仍然会通过虚拟地址进行访问,只不过在访问的过程中会触发page fault
我们遵循本地fork(例如,Linux的copy_process())来获取(1)-(3)这些信息,并遵循CRIU以获取(4)信息。由于决定何时回收描述符是很困难的,因此我们总是让已经准备好的父容器(及其描述符)始终处于有效状态,除非无服务器平台能确定需要释放父容器及其描述符了(即通过fork_reclaim进行容器回收)
需要注意的是父容器的描述符和父容器本身是不一样的。当不需要对父容器进行fork的时候,实际上就可以回收父容器的描述符了,但是这个时候不能回收父容器;但是如果能回收父容器的话,那么就一定能回收父容器的描述符。
上面还提到了,生命周期管理是交给了无服务器平台来做的。
尽管MITOSIS的描述符与C/R机制中的检查点文件十分相似,但是我们需要强调一个很重要的区别:MITOSIS中的描述符仅存储页表,而不会存储内存页。因此,MITOSIS中描述符大小的数量级是比C/R的检查点文件小得多的,并且生成速度和转发速度也是快得多的。
这里可以回忆一下导致C/R缓慢的三个原因:一个是因为父容器内存越大,检查点文件一般就越大;二是检查点文件中保存了一些无用信息(比如内存页);三是额外的软件开销,即出现了page fault的时候需要进行DFS。
这里MITOSIS通过较小容器描述符,降低了C/R机制中第一点和第二点所带来的开销;而正是因为容器描述符较小,这样就可以实现更快的网络传输了。
5.2 Fork resume阶段
fork_resume将通过获取父容器的描述符并从中恢复相关信息以恢复父容器的执行状态。我们现在将会描述如何快速的执行上面提到的两个步骤。现在,我们假设子容器的OS已经与父容器建立了网络连接,并且能通过这个网络连接实现RPC和单边RDMA的发送。下一个部分将会描述这个连接建立的过程。
借助单边RDMA实现快速的容器描述符获取
一个直接的、实现获取容器描述符的方法是使用RPC(remote process call,远程进程调用)。然而RPC会带来巨大的内存拷贝开销(见图18),因为一个中等大小的容器的描述符,可能需要使用几KB。
这里估计也是说需要将内存中的某些信息拷贝到什么地方去,以便进行网络传输;或者在处理RPC请求的过程中,服务器为了方便处理请求,可能会涉及到一些内存拷贝。
理想的获取容器描述符的方式是使用单边RDMA READ,但是这需要几个条件:
- 将父容器的描述符存储到一个连续的内存空间
这应该是DMA的限制,就是需要提供一段内存空间的起始物理地址,后面进行DMA的时候只能进行连续空间的传输。
- 提前告知子容器的OS RDMA的内存起始地址和传输大小
在计组课上学到的,DMA控制器需要接收内存起始地址,还需要接收一个counter来记录需要传输的大小。
第一个条件可以通过将描述符序列化为格式化信息轻松解决。数据序列化只会带来极小的开销(亚毫秒级别),这是由于容器描述符简单的数据结构。
这里需要跟传统的进行状态转移的方式区分开。回忆一下传统的状态转移,通常情况下需要借助消息传递,可能还需要借助云存储,这些技术都需要数据的序列化,并且由于这些状态转移的信息比较冗杂,所以序列化的开销比较高;而在MITOSIS中,由于描述符比较简单,序列化带来的开销就小得多。
对第二个条件,一个很自然的想法是将这些内存信息编码到描述符标识符中(如handler_id),而描述符又会直接传递给fork_resume系统调用。然而,这个方法是不安全的,因为恶意用户可能会传递错误的ID,导致子容器会读取并使用错误的容器描述符。我们采取了一个简单的方式来解决这个问题:MITOSIS将会发送一个身份验证RPC,用描述符标识符查询描述符内存信息。
这里就加上了一个鉴权机制,使得获取RDMA地址、大小的过程更加安全。
如果通过鉴权,父容器就会返回描述符的存储地址以及负载,以便子容器可以直接通过单边RDMA直接读取这些信息。
这里说的描述符的存储地址和负载应该就是指描述符及父容器的相关信息。这里就需要区分一下描述符和标识符了,标识符是上面提到的那三个:handler_id、key、RDMA地址,而这三个标识符用于唯一标识一个容器描述符。
我们选择这样一个简单的设计,是因为一个额外的RPC(通常是几个字节)的带来的开销是极小的:读取描述符(几KB)在整个获取描述符的过程中将会占大部分。
通过generalized lean containers实现快速恢复
在上面提到,这个是一种高效、轻量级的数据容器或数据结构。在本篇文章的应用场景下,应该就是一种快速创建容器的方法。
通过获取容器描述符,子容器OS将通过以下两步,实现将子容器恢复到父容器的执行状态。
- 容器化:设置cgroup和命名空间以符合父容器的设置
- 交换:将调用者的CPU寄存器、页表和IO描述符替换为父容器的相关信息。
这里说的调用者应该是指fork_resume的调用者,也就是子容器。在上面提到过,容器描述符中携带的信息有:cgruop以及namespace、CPU寄存器、页表、已经打开的文件的相关信息。这里的两个步骤本质上就是在使用容器描述符中的状态了
交换过程是高效的(在几个亚毫秒之间就能完成):交换只需要模仿本地fork——例如,取消调用者当前的内存映射,然后通过将父容器的页表拷贝给子容器以实现将子容器的虚拟内存空间映射到父容器上。
感觉这里说的需要取消内存映射是因为可能原来这个容器中已经有一个页表了,在使用MITOSIS的时候需要取消这些内存映射以便进行内存的回收。
在另一方面,容器化会花费十几ms,这是由于设置cgroup和命名空间的开销导致的。
所以在resume的过程中,主要的开销为容器化,并且这个花费在某些情况下应该跟无服务器平台上的函数执行时间相当,甚至会更长一点,所以需要进行一点优化。
幸运的是,快速的容器化已经得到了充分的研究。例如,SOCK引入了lean container(倾斜容器)。这个倾斜容器是一个特殊容器,它拥有对无服务器计算必要的、最小的配置。它进一步使用池来隐藏容器引导的成本,将容器化所花费的时间从十几ms降低到了几ms。我们将SOCK的倾斜容器推广到分布式系统中,以加速远程fork的容器化。具体地说,在子机器上恢复远程父容器的执行状态之前,我们会使用SOCK创建一个空的倾斜容器,这个倾斜容器可以满足父容器的隔离性需求。然后,这个空容器将会调用MITOSIS以恢复父容器的执行状态。由于子容器已经荣国SOCK被正确地设置,因此我们可以跳过开销较高的容器化过程。
从这里可以看出来,上面所说的caller就是指子容器,这是因为子容器被容器化之后,就会调用fork_resume以恢复父容器的执行状态。
5.3 网络守护进程
从这里开始介绍第二个组件——网络守护进程。上面是说,这个守护进程主要用于管理RDMA连接
网络守护进程旨在减少创建RDMA连接的开销(通常情况下被称为RCQP,也就是上面提到的RDMA可靠连接队列对),该开销是在远程fork的关键路径上的。与此同时,网络守护进程也避免了缓存连接到所有服务器上的RCQP,减少了内存的开销。
解决方式:改进先进的RDMA传输(DCT)
在这个目标背后必要的要求是我们需要QP(queue pair,队列对)要是无连接的。RDMA的确提供了无连接的传输——不可靠的报文(UD),但是这只支持消息,所以我们只在进行RPC的时候使用它。
在后面传递DC Target和Key的时候会借助这种无连接的传输。
我们发现动态连接传输(DCT)——一个研究较少但得到广泛支持的RDMA特性,其非常适合远程fork。DCT保留了RC(Reliable Connection)的功能,而且进一步提供了一种无连接的错觉:单个DCQP可以与多个不同的节点进行通信。
“单个DCQP可以与多个不同的节点通信”,这就是无连接传输的一个很重要的特点。在计算机网络中,TCP会通过四元组标识一个连接,其中包含了发送方的IP以及端口信息,还包含了接收方的IP以及端口信息,这就使得这个连接只能在这二者之间进行传输;而对UDP而言,它使用二元组标识一个连接,其中仅包含目的IP和目的端口,这样发送方和接收方之间就没有对应的关系,就使得一个发送方可以发送多个UDP报文到不同的机器上了。这就是上面说的“单个DCQP可以与多个不同的节点通信”。
目标节点仅需要创建一个DC(动态链接)目标,这个DC目标由节点的RDMA地址以及一个12字节的DC key标识。子节点知道密钥后,无需连接即可向相应目标发送单侧RDMA请求——硬件将承载数据处理连接,这个过程速度极快(在1us以内),正如图8中所展示的。
这里最后一句话的意思应该就是:在硬件上已经支持了DCT连接,所以速度很快
左边是可靠数据传输的RDMA,而右边是使用DCT连接的RDMA。单纯从图上来看,使用DCT确实很像无连接传输。
另外需要注意的是,DC目标和key是建立在被连接的机器上的。
DCT连接的原理不是这篇文章的重点,这里关于DCT的部分我也就仅仅看到这里了。
基于DCT,网络守护进程管理了一个较小的、位于内核空间的DCQP池,以便处理来自子容器的RDMA请求。
我现在可能能理解单边RDMA READ是什么意思了,也就是在MITOSIS的场景下,只会存在子容器去读取父容器物理内存的情况,也就是单边(因为只会是子容器向父容器发送请求)RDMA READ
在MITOSIS的场景下,父容器应该是DC目标,所以子容器应该知道父容器的Key之后才能进行RDMA传输
更具体地说,每个CPU一个DCQP就足以利用RDMA。然而,单独使用DCT是不够的,这是因为子容器需要提前知道DCT key以便与父容器进行通信。因此,我们也实现了一个内核空间的FaSST RPC以便启动DCT。FaSST是一个基于UD的RPC,他支持无连接传输。我们在获取父容器的容器描述符的RPC请求中,将会同时传输与父容器相关的DCT key。
上面在“如何快速获取容器描述符”部分介绍了,在获取容器描述符的时候,会使用一个RCP来进行鉴权,如果鉴权通过,父容器就将向子容器返回容器描述符的地址以及负载。这里的意思就是,在这个鉴权RPC中,将会额外传输与父容器相关的DCT key。这样子容器如果获取到了父容器的描述符,那么同时就获取了父容器的DCT key。
为了节约CPU资源,我们仅部署了两个内核线程以处理RPC调用,这对我们的工作负载已经是足够的了(见图13b)。
感觉可以复习一下这篇文章中所遇到的所有的RPC调用:
- 在获取容器描述符的时候,子容器会向父容器发送一个鉴权RPC,如果鉴权通过的话,父容器将会返回容器描述符的存储地址、负载,以及父容器的DCT Key。
好吧仔细回忆了一遍好像只有这一个RPC
还有这里说道的两个内核线程,不知道是不是指上面四个组件中的网络守护进程和fallback守护进程。
讨论DCT的开销
众所周知,由于额外的重新连接消息,DCT有一定的性能问题。
DCT有一定RC的特性,但是就我的感受而言,他应该是可靠连接和不可靠连接二者的折衷,所以DCT相对于RC而言,肯定还是会出现一定的连接丢失的情况的。
相较于RC,在较小的(32B)单侧RDMA READ,它会导致高达55.3%的性能下降。然而,重新连接所带来的开销在RDMA传输数据量较大(超过1KB)时就微不足道了。这是因为传输数据将会占据大部分的时间。
这里说的是数据在网络中传输的时间,并不包括在子机器上容器化、交换的过程。
因此MITOSIS的工作负载模式主要是数据量较大的传输,例如以4KB的粒度读取远程页面。根据经验,我们没有发现这个问题的影响。
5.4 RMDA可感知的虚拟内存管理
为了恢复容器时的效率,在resume阶段,我们会直接将子容器的页表项中的映射页设置为父容器的物理地址。
也就是子容器的虚拟内存将会被映射到远程机器上的父容器的物理内存。
然而,源OS无法感知页表项中的远程物理地址。
根据下面的说法,这个源OS应该是指子容器的OS。这里说的没办法感知是因为,在原本的页表中,如果出现缺页默认会去磁盘上寻找对应的页。但是在MITOSIS的使用场景下,如果没有额外处理的话,发现内存中没有某个远程page,同样也会去磁盘中寻找(也就是文章中所说的,源OS无法感知页表项中的远程物理地址,换句话说,就是子容器的OS无法区分一个物理地址是远程物理地址还是本地物理地址),这显然是不合理的。
因此,为了区分,我们在PTE中专门设置了一个remote bit。特别是,OS将会将remote bit设置为1,并在恢复阶段的切换过程中清除PTE的当前位。
从上下文里面看,这个当前位只能是remote bit了。
在这之后,在交换之后,子容器的远程页面访问将会陷入到内核中。因此MITOSIS可以在能感知RDMA的page fault处理程序中,处理子容器的远程页面访问。
由于上面的那个当前位我不太明白,所以这一句话就有点难懂了。我想的是,在子容器上,如果页面没有在子容器中的话,remote bit就会被设置为1,反之会被设置为0。这样在进入缺页中断的时候,如果remote bit为1,就需要使用RDMA进行处理,反之则需要读取磁盘信息。
还有就是这里所说的RDMA-aware,我的理解是:这个page fault处理函数能感知到这个缺页错误是由于远程物理页缺失导致的。
需要注意的是,我们并没有修改页表项的数据结构:我们将被忽视的页表项位(即58-52位中的一位)作为了remote bit
使用未使用的页表位作为remote bit,这样就能很大程度上保证这个能感知RDMA的虚拟内存管理器的兼容性
能感知RDMA的缺页错误处理程序
表2总结了我们如何处理与远程fork相关的、不同的错误。如果发生错误的页面没有被映射到父容器中,例如在栈增长时触发了缺页错误,我们就将像处理一个普通的缺页错误一样在本地处理这个缺页错误。否则我们就需要检查出错的虚拟地址是否被映射到了远程物理地址上。如果是这样的话,我们就将使用单边RDMA以将远程页面读取到本地页面。大部分的子页面都可以通过RDMA恢复,这是因为无服务器函数通常会涉及到上一次运行的子集。
这里说的子页面应该是指在子容器上的页面,在函数执行阶段我们需要使用RDMA将子页面更新为父容器的页面。
这里说的某次函数的执行只会涉及到上一次运行的子集,这个道理还是比较好懂的,因为函数执行的都是同一个函数,那么需要使用到的数据在大部分情况下,所占据的内存页面都是相同的。
而正是因为同一个函数执行的时候使用的页面都是相对固定的,因此对一个函数而言,RDMA只需要传递这些函数执行需要使用到的页面即可,而在传输过程中不需要对RDMA传输的数据起始地址、大小做出修改。
在丢失映射的情况下,我们就将回退到RPC。
或者说在没办法使用本地
问题
为什么缓存实例被称为预分配的并发?我印象中的缓存是根据程序运行动态变化的,而不是被分配的(我是指分层存储中的cache)。还是说这里的缓存是问磁盘读取的时候在内存中的缓存?(这个缓存确实与并发有关)
fork出containers是什么意思?
“无服务器容器之间的部分状态转移”(partial state transfer across serverless containers)是什么意思
“容器的预物化状态的高效转移”(efficiently transfer the pre-materialized states of the forked one)是什么
可能需要了解一下分布式原理
不是应该一个容器镜像(函数及其执行环境)对应一个特定的容器吗?那这样缓存了容器之后,特定的容器不是只能用于执行特定的容器吗?为什么我看下来感觉是一个容器是一个通用的执行环境,这样一个容器虽然不能同时执行多个函数,但是可以分时执行多个函数。
按照我的理解而言,通过fork创建出来的容器应该是完全一致的,那么这样fork出来的容器怎么能用来执行环境完全不匹配的函数呢)
什么叫“dispatching function requests to user-implemented functions”?(在500页右侧)
什么是“present bit”?(在503页右下侧)
orchestrator与coordinator有什么区别?翻译看都是协调器
感觉需要了解一下RPC原理
RPC实际上就相当于是一个机器去调用了另一台机器上的函数。所以RPC不应该被称为远程进程调用,应该被称为远程过程调用。
- 作者:Noah
- 链接:https://imnoah.top/article/PaperRead/Paper2
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。