3.1.4 有状态函数应用场景
Serverless的应用场景如图3-9所示,有状态函数可以将Serverless拓展到更通用的应用场景。
1. 微服务的应用场景
微服务架构的主要特点是,各微服务可独立开发、独立部署和运行,其应用场景主要是交付周期短、需要频繁变更的互联网应用,如Web应用和移动应用的后端服务。传统微服务框架开发的场景可以使用有状态函数开发,函数开发可以使开发者更加聚焦业务的实现而不用关心业务运行的系统环境。
图3-9 Serverless的应用场景
2. 现有Serverless平台的应用场景
现有Serverless平台的FaaS实现以无状态函数为主,其应用场景适合事件驱动型应用,包括物联网场景、连接应用与云服务的“胶水型”任务等,在应用中起到连接各组件和进行数据搬运的作用。
3. 新一代Serverless的应用场景
通过云平台原生提供状态管理能力,包括并发控制、一致性、伸缩能力等,有状态函数更适用于以下低时延、高性能、高可靠的状态使用场景。
• 大规模分布式机器学习中的模型训练和服务,如迭代计算场景。
• 大数据与流处理,如对实时性要求较高的大数据预测和推荐、风控服务。
• 用户实时交互型应用,如多人在线游戏、电商(秒杀、抢购)、在线票务系统。
• 多人共享的协作场景,如多人共享文档协作、多人在线聊天室。
3.1.4.1 机器学习中的迭代计算场景
参数服务器是在分布式机器学习中新兴的一种计算模式,在该模式下需要通过迭代计算循环更新参数,参数数量可达到十亿至万亿个。如果状态加载时延较长,会影响整体计算性能。因此,可使用有状态函数显著降低状态加载的时延。
图3-10为参数服务器计算集群的工作原理。集群中的节点分为计算节点(worker)和参数服务节点(server)。其中,worker负责对分配到自己本地的训练数据(块)进行计算,并更新对应的参数;server采用分布式存储的方式,作为服务方接收计算节点的参数查询和更新请求。一次迭代计算过程的具体步骤如下。
(1)计算(compute):训练数据(块)分配到某个worker上,worker调用特定的机器学习算法进行训练,大部分的算法是在构建合适的损失函数后利用“链式法则”进行求导以计算出梯度,每个worker只负责对参数全集w中的一部分参数wi (i=1,…,m)进行计算。
(2)推送参数(push):各个worker将计算出的梯度gi (i=1,…,m)推送给server。
(3)更新参数(update):server将各个worker推送的梯度gi (i=1,…,m)进行汇总,更新参数全集w。
(4)加载参数(pull):各个worker从参数全集w中加载自己需要的参数wi (i=1,…,m),重复进行上述步骤,完成下一次迭代。
图3-10 参数服务器计算集群的工作原理
在上述场景中,如果使用微服务或无状态函数来实现,由于worker和server本身不保留状态数据,则需要将参数和梯度数据保存到外置存储(如S3)上,使得worker和server需要多次从或向外部存储上加载参数或推送梯度数据。在大规模机器学习训练中,训练数据的大小可以达到1TB~1PB,复杂模型的训练参数甚至多达109~1012个,迭代次数可以达到数千至数万次。在这样的过程中,参数本身的加载和梯度的推送占用了大量的时间,影响机器学习性能的提升。
如果使用有状态函数实现参数服务功能,重构后的参数服务器架构如图3-11所示。
图3-11 使用有状态函数重构后的参数服务器架构
其中,原有的server使用有状态函数实现,各个worker函数使用无状态函数实现。各个参数wi (i=1, …, m)和各个梯度gi (i=1, …, m)作为状态保留在server中,整个架构不再需要外置存储。
使用有状态函数重新实现后,相比之前的无状态函数计算过程具有以下优势。
• 性能提升:省去了对数据进行序列化和反序列化的开销和网络通信开销,可显著减少端到端时延。
• 节省空间:原有的计算过程需要将参数wi (i=1, …, m)和梯度gi (i=1, …, m)在worker、server和S3中各存储一份,使用有状态函数重构后无须在外置存储中进行存储,节省了1/3的存储空间。
3.1.4.2 大数据计算场景
在大数据计算场景下,经常会产生大量数据I/O,这些数据量通常达到GB或TB级别。如果使用有状态函数,则可以显著降低I/O开销。
例如,网络规划领域的某服务化工具,其使用流程如下。
• 服务编排设计:设计工程师事先将原子服务(类似函数)按照所需要的业务流程进行编排,组成一套任务流程,以达到服务复用的目的。
• 任务计算:系统从编排好的任务流程生成任务实例,服务工程师在使用时设定参数、上传文件,系统按照指定的任务流程进行自动计算,最后给出分析结果。
两函数串行编排流程如图3-12所示,函数A接收输入,将计算得到的中间结果交给函数B继续计算,函数B输出最终结果。这一场景分别使用无状态函数和有状态函数的实现方式比较如下。
(1)使用无状态函数+外部存储实现。如图3-13所示,如果用无状态函数+外部存储来存储中间结果,那么函数A和函数B就不可避免地要通过网络I/O来读写中间结果。然而,大数据场景下的中间结果往往较大,I/O耗时成为其主要瓶颈。用户关心的是最终结果而不是中间结果,最后还需要删除中间结果以避免长期占用额外空间。
(2)使用无状态函数+内部存储实现。使用无状态函数+外部存储实现方式的主要痛点在于使用了外部存储,造成网络I/O开销大。如果使用无状态函数+内部存储实现会怎样呢?
如图3-14所示,如果使用无状态函数+内部存储实现,就需要将函数A和函数B“亲和性”部署到同一个虚拟机或物理机中,从而借助同一份内部存储(通常是内存或磁盘)来传递中间结果。由于使用的是内部存储,可以显著降低I/O耗时,也避免了将中间结果持久化到外部存储及删除所带来的开销。
图3-12 两函数串行编排流程
图3-13 无状态函数+外部存储
图3-14 无状态函数+内部存储
对用户来说,需要自己开发一个函数调度平台来实现对函数节点的亲和性部署、调度和维护。这个平台类似于一个小型的PaaS,要实现较高的可用性和资源利用率,对普通用户来说是有一定门槛的。这种方式的主要痛点在于函数调度平台的开发和维护成本较高。
(3)使用有状态函数实现。使用无状态函数+内部存储实现方式中的函数实例与状态紧密绑定,这实际上就是有状态函数的雏形。如果其中的函数调度工作由Serverless平台完成,并且对用户透明,实际上就是有状态函数的应用。
如图3-15所示,如果使用有状态函数实现,可以将函数A和函数B亲和性部署到同一个Serverless运行时中,它们可以借助同一个状态数据来传递中间结果。这种方式的优势在于,一是状态数据在内部存储中,避免了网络I/O开销;二是用户无须关心如何调度函数实例,降低了开发和维护成本。
图3-15 使用有状态函数实现
3.1.4.3 实时交互型场景
大型多人在线游戏(Massively Multiplayer Online Game,简称MMOG或MMO)通常需要与百万级用户产生每秒高达数百万次的实时交互,它对于时延的要求非常高,通常端到端的时延大于100ms就会给用户带来不可接受的影响,其中网络时延和玩家自身网络情况有关,无法完全避免,这就需要服务端的计算和I/O时延尽可能低。
对战类的游戏是有状态的,这是因为多人加入同一场战斗时,会同时在一台服务器上进行,不能分布在多处。如果分布在多台服务器上进行处理,就需要通过外部数据库存储状态,导致在游戏过程中频繁连接数据库而增加时延。如果整场战斗在同一台服务器上进行,在内存中保留状态并进行计算,则可以显著降低时延。
那么,如何对游戏的状态进行管理呢?通常会采用游戏对象(Game Object)的设计模式来设计MMO游戏。游戏对象将游戏状态封装到一个逻辑抽象中,其中不仅包含游戏中所有有意义的实体数据,还包括玩家角色、NPC条目、互动世界对象等。这个抽象对象有一个ID标识元素,每个游戏对象实例拥有在整个游戏系统中唯一的ID,如图3-16所示。
图3-16 游戏对象示意
这是一种stateful的设计模式,游戏将状态保留在内存中以便重复使用直到不再需要,结合有状态函数的使用,其主要特点如下。
• 位置唯一性:每个游戏对象实例在某个时间只存在一个有状态函数实例中,游戏对象不允许在函数实例之间复制移动,从而降低对象创建和同步的开销。位置唯一性使系统更容易定位到某个特定的游戏实例,并且有助于增强数据状态的一致性。
• 资源独享:不需要共享资源,也避免了多线程对资源的竞争。
因此,在该类场景下,有状态函数主要利用数据本地化的优势带来更低的时延。