云计算兴起之初,市场上存在两种虚拟化解决方案。一是AWS提供的Elastic Compute Cloud (EC2)[1]服务,二是Google提供的App Engine[2]服务。在当时看来,两种解决方案针锋相对,分别代表了两种极端:EC2实例尽可能的还原物理环境,用户几乎可以掌控从OS Kernel 向上的整个软件栈;而App Engine 则为用户提供了丰富的云端基础服务,用户可以使用这些服务开发云应用。[3]
市场最终选择了AWS的“低级”虚拟化方案,因此谷歌、微软以及国内各大云提供商都提供了类似的接口。虚拟机方案能够成功,归功于在云计算的早期,前景尚不明朗的情况下,用户更多的是希望在云中构建与本地相同的环境,以实现应用迁移,而不是重新开发一套云端应用。

**然而这种选择的代价是,开发人员必须自己管理整个虚拟环境。**表1中列出了云中运行环境必须管理的问题。**一系列繁琐的环境管理工作激发了使用简单应用程序的客户对于简单云路径的需求。**例如,国庆期间朋友圈迎来了更换国旗头像的热潮。实现一个这样的图片处理程序,可能只需要几十行JavaScript的代码,与配置和维护服务器来运行这段代码的工作量相比,开发工作简直是微不足道的。
基于这种需求,AWS在2015年推出了名为AWS Lambda[8]的云函数服务。引发了业界对无服务计算的广泛,通过Lambda,用户只需要考虑应用程序的代码逻辑,无需考虑扩展、部署、容错、监控、日志记录服务器配置,因而又称之“无服务”计算。
如今,各大云提供商都提供了无服务计算服务。随着越来越多的大型应用,如大数据处理、机器学习模型训练等采用无服务计算的架构,无服务计算的性能问题也日益凸显。[9]
在现有FaaS平台中,每个Function实例都运行在隔离的容器或轻量级虚拟机的环境中,当请求到达时FaaS平台会启动并初始化一个新的实例来响应用户的请求,过程如图三[10]所示。

这种触发式启动的模式可以很好的根据负载的变化来自动的扩展或者缩小实例的数目。
图4所示,原来的Application直接通过一个线程响应请求,整个过程几乎没有任何初始化的开销。然而对于Serverless而言,需要首先启动一个Sandbox实例,

缓存实例即预启动服务实例,构建一个实例池,从而避免冷启动的过程,如Lambda的预留实例、OpenWhisk的warm-start机制等。此方案可以跳过初始化的过程,
但是需要在没有请求的情况下保留足够数量的实例,对于云提供商而言是额外的成本开销!此外,面对超高的突发负载,“池“策略的局限性将进一步凸显,对于超过“池”容量的负载请求,依然需要冷启动实例!

缓存的实例可以分为仅缓存Sandbox和缓存整个Function实例两种。
前者具有很好的通用性,缓存的sandbox可以用于处理不同Function的请求,但是无法消除应用的初始化开销[11];缓存整个实例如图5[12]所示,可以完全的消除冷启动的开销,但是通用性极差,放大了池化策略的缺点!
在面临突发请求时,启动足够的实例将面临巨大的性能挑战!考虑到单个Function执行所需的资源很小,可以考虑用一个实例并发处理多个请求,从而大大减少冷启动的数目。Knative[13] 融合了原来传统微服务的方式,将一个Function定义为一个更小的微服务,是一个拥有IP的可访问的执行单元,一个function实例可以并发的处理多个用户请求。Knative通过类似于HPA的方式实现了自动扩展,如图6所示。

每个Fucntion都运行在一个Pod中,Pod运行一个sidecar服务用于监控流量,并周期性向自动扩展组件Autoscaler汇报,Autoscaler 将根据负载情况控制实例的数目。
内部并发,可以减少启动实例的数目,从而减少冷启动开销。
但是如何确定实例的并发数目是一个复杂的问题,
然而Restore的过程,开销依然存在且无法忽略,如图7所示。

因此他们提出了按需恢复的策略,即不在启动的关键路径上完全恢复进程状态。**这是出于对进程初始化过程中启动的文件和加载的内存,只有很少一部分会在运行的过程中被使用这一实践情况的观察。**最终,该案可以将应用(JVM)的初始化开销,缩减至30ms左右。
这是一种以弱化隔离性为代价的解决冷启动开销的方案。
无服务计算的实例是无状态和完全隔离的,这有利于实现按需的自动扩展。然而伴随着无服务计算的应用范围越来越广泛,无状态的特性逐渐成为性能瓶颈问题。由于完全的隔离,实例之间无法直接进行通讯,只能通过第三方的存储服务来传递中间结果,称之为无服务计算的临时存储问题。[17]
通常临时存储的介质包括磁盘、SSD和内存存储。三者的价格依次上升而访问的响应时间依次减小。因此如何选择合适的存储介质,在确保服务质量的前提下,节省最大的成本,是临时存储所面临的主要挑战。Pocket[18] 通过对Function任务的分析,可以帮助用户动态的选择合适的存储方式。
然而即使选择内存存储,用户依然需要使用网络将数据拉取到本地,如果多个Function共享某个中间结果,那么就需要重复的拉取拷贝和数据序列化,如图9所示,这会导致额外的内存开销,降低函数的部署密度。
针对这个问题,FAASM[16]基于WebAssembly共享地址空间的特性,设计了共享内存的数据通讯方案。

此外Function无法直接调用启动另一个Function,需要首先将调用的请求上传到全局的Gateway中,再由Gateway进行裁决和任务的下发,这就带来了额外的调用开销,如图10所示,对于延迟敏感的任务,这种开销是同样是无法接受的。

SAND[10]、Nightcore[19]等实现了本地的消息队列,当本机的资源合适时,可以选择直接在本地启动实例。
数据分析型任务可以根据数据依赖性抽象成一个DAG图,分为多个阶段,每个阶段包含多个并行任务。数据分析型负载通常随着时间的推移资源需求也在变化,因此特别适配于无服务计算平台,可以充分的利用其按需资源分配和快速实例扩展的特性。[20]
除前述解决冷启动和通讯的系统方案之外,通过恰当的调度策略,可以在现有系统的基础上优化任务的执行路径,在保证任务服务质量的前提下,节省成本开销。
现有的解决方案主要通过分析任务流的结构,利用任务流内的调用关系以及数据局部性等,决策Function的启动时机和数据传递。
如下图所示,普通的启动模式分为:懒汉模式和饿汉模式。懒汉模式顺序初始化function,饿汉模式提前初始化function。显然懒汉的成本更低,饿汉的执行时间更短。Caerus[20]通过分析任务间的依赖关系,在何时位置启动下一步实例,重合执行可并发的部分,隐藏启动开销,同时缩短实例的等待时间。

如下图所示,可以从一个DAG图中分割出多个子图,子图内部的中间数据自下而上的传递。可以让子图中所有任务重用同一个Function实例,对于子图重合的部分,根据数据大小,动态选择数据传递方向,尽量避免不必要的数据传递。[21]

从无服务计算的概念被提出至今已有六年,特别是近三年来涌现了许多相关的研究。
https://zhuanlan.zhihu.com/p/425433134