模型服务模式 本章涵盖 使用模型服务在训练好的机器学习模型上生成预测或对新数据进行推理 处理模型服务请求并通过副本模型服务实现水平扩展 使用分片服务模式处理大型模型服务请求 评估模型服务系统和事件驱动设计
在上一章中,我们探讨了分布式训练组件中面临的一些挑战,并介绍了一些可以整合到这一组件中的实用模式。 分布式训练是分布式机器学习系统中最关键的部分。 例如,我们在训练用于标记新 YouTube 视频主题的大模型时遇到了挑战,它无法在单台机器上运行。 我们研究了如何克服使用参数服务器模式带来的挑战。 还学习了如何使用集合通信模式来加速小模型的分布式训练,来避免参数服务器和工作节点之间不必要的通信开销。 最后,我们讨论了分布式机器学习系统中由于数据集损坏、网络不稳定和节点抢占导致的稳定性问题,以及对应的解决方案。
模型服务是模型训练成功后的下一个步骤。 是分布式机器学习系统的基本步骤之一。 模型服务组件需要具有可扩展性和可靠性,以处理不断增长的用户请求数和单个请求的数据量。 在构建分布式模型服务系统时,不同设计决策带来的权衡也至关重要。
在本章中,我们将探讨分布式模型服务系统中涉及的一些挑战,并将介绍一些在工业界广泛采用的成熟模式。 例如,我们将看到在处理不断增加的模型服务请求时遇到的挑战,以及我们如何在副本服务的帮助下来实现水平扩展以克服这些挑战。 我们还将讨论分片服务模式如何帮助系统处理大型模型服务请求。 此外,我们将学习如何评估模型服务系统并确定事件驱动设计对实际应用场景是否有帮助。
4.1 模型服务的基本概念 模型服务是一个通过加载已经训练好的模型以生成预测或对新输入数据进行推理的过程。是模型训练成功后的下一个步骤。 图 4-1 展示了模型服务在机器学习流水线中的位置。
图 4-1 模型服务在机器学习流水线中的位置
Ingest the data and train a machine learning model with the ingested data. 摄取数据并使用这些数据训练一个机器学习模型。
Model serving is the next step after we have successfully trained a machine learning model. We use the trained model to generate predictions or make inferences on new input data. 模型服务是我们成功训练出机器学习模型后的下一步骤。我们使用这个训练好的模型对新输入的数据生成预测或进行推理。
Data ingestion 数据摄取 Model training 模型训练 Model serving 模型服务
模型服务是一个通用概念,它出现在分布式和传统的机器学习应用中。 在传统的机器学习应用程序中,模型服务通常是在本地机器上运行单个程序,并对新数据集生成预测,并且这些数据集之前没有被用于模型训练。 对于传统的模型服务来说,数据集和使用的机器学习模型都应该足够小,以便它们能够被存储在单台机器的本地磁盘上。
相比之下,分布式模型服务通常发生在一组机器中。 用于模型服务的数据集和训练好的机器学习模型可能非常大,并且必须存储在远程分布式数据库中或分区存储在多台机器的磁盘上。 表 4-1 总结了传统模型服务和分布式模型服务系统之间的差异。
表 4-1 传统模型服务与分布式模型服务系统的对比
Traditional model serving Distributed model serving Computational resources Personal laptop or single remote server Cluster of machines Dataset location Local disk on a single laptop or machine Remote distributed database or partitioned on disks of multiple machines Size of model and dataset Small enough to fit on a single machine Medium to large 传统模型服务 分布式模型服务 计算资源 个人笔记本电脑或单个远程服务器 一组集群 数据集位置 单个笔记本电脑或机器上的本地磁盘 远程分布式数据库或分区存储在多台机器的磁盘上 模型和数据集的大小 足够小,可以容纳在一台机器上 中型到大型
构建和管理一个针对不同使用场景的可扩展、可靠且高效的分布式模型服务系统并不简单。 接下来我们将研究一些使用场景以及能够应对不同挑战的模式。
4.2 副本服务模式:处理不断增长的服务请求 您可能还记得,在上一章中,我们构建了一个机器学习模型,使用 YouTube-8M 数据集 (http://research.google.com/youtube8m) 来标记新视频的主题,这个数据集包含了数百万个 YouTube 视频 ID,具有来自 3,800 多个不同实体类别(例如食物、汽车、音乐等)的高质量机器生成注释。 YouTube-8M 数据集中视频的截图如图 4-2 所示。
现在我们想构建一个模型服务系统,允许用户上传新视频。 然后系统将加载训练好的机器学习模型来标记上传视频中出现的实体类别/主题。 请注意,模型服务系统是无状态的,因此用户的请求不会影响模型服务的结果。
该系统获取用户上传的视频并向模型服务器发送请求。 然后模型服务器从模型存储中检索之前训练好的实体标记机器学习模型来处理视频并最终生成视频中可能出现的实体类别。 该系统的概览如图 4-3 所示。
图 4-2 YouTube-8M 数据集中视频的截图。(来源:Sudheendra Vijayanarasimhan 等人,遵循 Nonexclusive License 1.0 许可协议)
图 4-3 单节点模型服务系统的架构图
Users uploads videos and then submit requests to model serving system to tag the entities within the videos. 用户上传视频,然后向模型服务系统提交请求以标记视频中的实体。 Model server (single-node) 模型服务器(单节点) Model storage 模型存储 Trained ML model 经过训练的机器学习模型 Send request 发送请求 Retrieve model 检索模型 Send data 发送数据 Obtain model 获取模型 Process data 处理数据 Generate entities 生成实体
模型服务器的初始版本仅在单台机器上运行,并按照先到先得的原则响应用户的模型服务请求,如图 4-4 所示。 如果只有很少的用户在使用该系统,这种方法可能会很有效。 然而,随着用户或模型服务请求数量的增加,用户在等待系统完成处理之前的请求时将遇到巨大的延迟。 现实中,这种糟糕的用户体验会立即使用户失去对这个系统的兴趣。
图 4-4 模型服务器仅运行在单机上,并按照先到先服务的原则响应用户的模型服务请求。
User requests are being processed on first-come, first-served basis. 用户请求按照先到先得的原则进行处理。 Model server (single-node) 模型服务器(单节点) Model storage 模型存储 Trained ML model 经过训练的机器学习模型 Send request 发送请求 Retrieve model 检索模型 Send data 发送数据 Obtain model 获取模型 Process data 处理数据 Generate entities 生成实体
4.2.1 问题 系统接收用户上传的视频,然后将请求发送到模型服务器。 这些模型服务请求在排队等待模型服务器处理。
受限于单节点模型服务器的性能,它只能按照先到先得的原则服务于有限次数的模型服务请求。 随着实际请求数量的增长,当用户必须等待很长时间才能收到模型服务的结果时,用户体验变得更糟。 所有请求都在等待模型服务系统的处理,计算资源受限制于单个节点。 有没有比顺序处理模型服务请求更好的方法呢?
4.2.2 解决方案 我们忽略了一个事实:现有的模型服务器是无状态的,这意味着每个请求的模型服务结果不会受到其他请求的影响,并且机器学习模型一次只能处理一个请求。 也就是说,模型服务器不需要保存状态就能正常运行。
由于模型服务器是无状态的,我们可以添加更多的服务器实例来协助处理额外的用户请求,并且这些请求之间不会相互干扰,如图 4-5 所示。 这些额外的模型服务器实例是原始模型服务器的副本,它们具有不同的服务器地址,每个实例负责处理不同的模型服务请求。 换句话说,它们是模型服务的副本服务,简称为模型服务器副本。
图 4-5 额外的模型服务器实例协助处理更多的用户请求,且这些请求之间不会相互干扰
Horizontal scaling of the stateless server 无状态服务器的水平扩展 Model server replicas 模型服务器副本 Scaling up 扩容 Scaling down 缩容
通过增加更多的机器将额外资源加入我们的系统,这种扩展方式称为水平扩展。 水平扩展系统通过添加更多副本来处理越来越多的用户或流量。与水平扩展相对的是垂直扩展,通常通过向现有机器直接添加计算资源来实现。
类比:水平扩展与垂直扩展 您可以将垂直扩展想象为:当您需要更多马力时,卖掉跑车并购买一辆赛车。 虽然赛车速度很快且看起来也很棒,但它也很昂贵并且不太实用。最终它耗尽所有的汽油能带您走的距离也是有限的。 此外,赛车只有一个座位,而且必须在平坦的路面上行驶,因此它只适合赛车运动。 而水平扩展则是通过增加另一辆车来获得额外的马力。 事实上,您可以将水平扩展想象成可以同时容纳大量乘客的几辆车。 也许这些车中没有一辆是赛车,它们也不需要是赛车,因为在整个车队中,您已经拥有了所需要的所有马力。
让我们回到最初的模型服务系统,它接收用户上传的视频并向模型服务器发送请求。 与我们之前设计的模型服务系统不同,该系统拥有多个模型服务器副本来异步处理模型服务请求。 每个模型服务器副本处理单个请求,从模型存储中检索之前训练好的实体标记机器学习模型,然后处理请求中的视频,以标记视频中可能出现的实体。
因此,我们通过向现有的模型服务系统中添加模型服务器副本,成功地扩展了模型服务器。 新的架构如图 4-6 所示。模型服务器副本能够同时处理多个请求,因为每个副本可以独立处理各个模型服务请求。
图 4-6 通过向系统中添加模型服务器副本来扩展模型服务器的系统架构
Users upload videos and then submit requests to model serving system to tag the entities within the videos. 用户上传视频,然后向模型服务系统提交请求,以标记视频中的实体。 Model server replicas 模型服务器副本 Model storage 模型存储 Trained ML model 经过训练的机器学习模型 Send request 发送请求 Retrieve model 检索模型 Send data 发送数据 Obtain model 获取模型 Process data 处理数据 Generate entities 生成实体
在新架构中,用户的多个模型服务请求同时被发送到模型服务器副本。 然而,我们还没有讨论它们是如何分发和处理的。 例如,哪个请求由哪个模型服务器副本处理?换句话说,我们还没有定义请求与模型服务器副本之间的映射关系。
为此,我们可以添加新的层——负载均衡器,它负责在副本之间分配模型服务请求。 例如,负载均衡器接收用户的多个模型服务请求,然后将请求均匀地分发到每个模型服务器副本,每个副本负责处理各个请求,包括模型检索和对请求中的新数据进行推理。 图 4-7 展示了这个过程。
负载均衡器使用不同的算法来决定哪个请求发送到哪个模型服务器副本进行处理。 负载平衡的示例算法包括轮询、最少连接、哈希等。
副本服务模式为我们的模型服务系统提供了一种很好的水平扩展方式。 它也可以推广应用于任何需要服务大量流量的系统。 当单个实例无法处理流量时,引入此模式可确保所有流量都能得到同等且高效的处理。 我们将在第 9.3.2 节中应用此模式。
图 4-7 负载均衡器如何用来将请求均匀地分配到各个模型服务器副本的示意图
Multiple model serving requests from users
The load balancer distributes the requests evenly to each of the model server replicas.
用户的多个模型服务请求
负载均衡器将请求均匀分配到每个模型服务器副本。 Load balancer 负载均衡器 Model server replicas 模型服务器副本
轮询负载均衡 轮询是一种简单的方法,负载均衡器根据一个列表循环地将每个请求转发到不同的服务器副本。
尽管使用轮询算法很容易实现一个负载均衡服务器,但是对负载均衡器服务器而言,如果它收到了大量需要被处理的请求,可能会超过它所能承受的最大负载,也就出现了过载的情况。
4.2.3 讨论 现在我们具备了负载均衡的模型服务器副本,能够支持不断增长的用户请求,并且实现了模型服务系统的水平扩展。 我们不仅能够以可扩展的方式处理模型服务请求,整个模型服务系统的可用性也变得非常高(https://mng.bz/EQBd )。 高可用性是指指系统无中断地执行其功能的能力,代表系统的可用性程度。 它通常用一年中的正常运行时间百分比来衡量。
例如,某些组织可能要求服务达到高度可用的服务级别协议,这意味着服务的运行时间达到99.9%(称为 3 个 9 的可用性)。 也就是说,该服务每天只允许有 1.4 分钟的停机时间(24小时×60分钟×0.1%)。 借助副本模型服务,如果任何模型服务器副本宕机或实例被抢占,其余的模型服务器副本仍然可用并准备处理用户的模型服务请求,这为用户提供了良好的体验并使系统更加可靠。
此外,由于我们的模型服务器副本需要从远程模型存储中检索之前训练的机器学习模型,因此它们不仅需要保持在线,还需要随时准备就绪。 构建并部署就绪探针,以通知负载均衡器副本建立与远程模型存储的连接,并准备好服务来自用户的模型服务请求,这一点至关重要。 就绪探针可帮助系统确定指定的副本是否已准备好提供服务。 有了就绪探针,当系统因内部问题未准备好时,用户不会由于请求到未就绪副本而遇到未知异常。
副本服务模式解决了我们的水平可扩展性问题,该问题限制了模型服务系统支持大量模型服务请求。 然而,在实际的模型服务场景中,不仅服务请求的数量增加,而且每个请求的数据量大小也会增加。 在这种情况下,副本服务可能无法处理大数据量的请求。 我们将在下一节中讨论这种情况,并介绍一种可以缓解该问题的模式。
4.2.4 练习 1 副本模型服务器是无状态的还是有状态的? 2 如果模型服务系统中没有负载均衡器,会发生什么情况? 3 如果我们只有一个模型服务器实例,能实现 3 个 9 的服务级别协议(SLA, service-level agreement)吗?
4.3 分片服务模式 副本服务模式有效地解决了水平扩展问题,使我们的模型服务系统能够支持不断增长的用户请求。借助模型服务器副本和负载均衡器,我们还实现了系统的高可用。
注意:每个模型服务器副本的计算资源都是有限且预先分配的。 更重要的是,为了让负载均衡器能够正确且均匀地分发请求,每个副本的计算资源必须是相等的。
接下来,我们假设用户想要上传一个高分辨率的 YouTube 视频,需要使用模型服务器应用程序为该视频进行实体标记。 即使高分辨率视频文件很大,如果模型服务器副本有足够的磁盘存储空间,文件也能够成功上传。 然而,我们无法在任意一个模型服务器副本中独立地处理该请求,因为处理这个单一的大请求需要在模型服务器副本中分配更多的内存。 这种需要消耗大内存的场景通常是由模型的复杂性带来的,正如我们在上一章中看到的,它可能包含许多繁重的矩阵计算或数学运算。
例如,用户请求将高分辨率视频上传到模型服务系统。 其中一个模型服务器副本接收到了这个请求,并成功检索到之前训练好的机器学习模型。 不幸的是,由于负责处理这个请求的模型服务器副本内存不足,模型无法处理请求中的数据。 最终,我们可能会在用户等待很长时间后通知用户请求失败,从而导致糟糕的用户体验。 这种情况的示意图如图 4-8 所示。
图 4-8 该图显示模型无法处理请求中的大数据,因为负责处理该请求的模型服务器副本没有足够的内存 示意图显示模型未能处理请求中的大数据,因为负责处理此请求的模型服务器副本内存不足。
User uploads a high-resolution video to the model serving system. This fails as the model server replica that’s processing this large request does not have enough computational resources. 用户将高分辨率视频上传到模型服务系统。 由于处理这个大请求的模型服务器副本计算资源不足,上传失败。 Model server replicas 模型服务器副本 Model storage 模型存储 Trained ML model 经过训练的机器学习模型 Send request 发送请求 Retrieve model 检索模型 Send data 发送数据 Obtain model 获取模型 Process data 处理数据 Generate entities 生成实体
4.3.1 问题:处理包含高分辨率视频的大型模型服务请求 系统正在处理的请求数据量之所以大,是因为用户上传的视频分辨率高。 在之前训练的机器学习模型可能包含繁重数学运算的情况下,这些大型视频请求无法被内存有限的单个模型服务器副本成功处理。 我们该如何设计模型服务系统来成功处理这种具有高分辨率视频的大型请求?
4.3.2 解决方案 鉴于我们对每个模型服务器副本的计算资源需求,我们是否可以通过增加每个副本的计算资源来垂直扩展,以便它们可以处理类似高分辨率视频的大型请求?由于我们为所有副本都垂直扩展相同的资源量,因此不会对负载均衡器产生影响。
但我们不能简单地垂直扩展模型服务器副本,因为我们无法预估请求的量。 想象一下,只有少数用户有需要处理的高分辨率视频(例如,拥有能够拍摄高分辨率视频的高端相机的专业摄影师),而其余绝大多数用户仅从智能手机上传较小分辨率的视频。 结果,模型服务器副本上添加的大部分计算资源都处于空闲状态,这会导致资源利用率非常低。 我们将在下一节中从资源利用率的角度出发进一步探究,但就目前而言,这种方法并不实用。
还记得我们在第 3 章中介绍了参数服务器模式吗?它支持对一个非常大的模型进行分区。 图 4-9 是我们在第 3 章中讨论的图,展示了使用多个参数服务器进行分布式模型训练;大模型已经进行分区,每个分区位于不同的参数服务器上。 每个工作节点获取数据集的部分子集,执行每个神经网络层所需的计算,然后发送计算好的梯度以更新存储在某个参数服务器上的一个模型分区。
图 4-9 使用多个参数服务器进行分布式模型训练,对大模型进行分区,每个分区位于不同的参数服务器上
Parameter server 参数服务器 Push updates 推送更新 Pull updates 拉取更新 Worker node 工作节点
为了解决大型模型服务请求的问题,我们可以借鉴相同的思路并将其应用于特定的场景中。
我们首先将原始的高分辨率视频分割成多个独立的视频片段,然后每个视频片段由多个模型服务器分片独立处理。模型服务器分片是模型服务器实例的副本,每个分片负责处理大型请求的一个子集。
图 4-10 中的示意图是分片服务模式的一个示例架构。 在图中,一个包含狗和孩子的高分辨率视频被分割成两个独立的视频,其中每个视频代表原始大型请求的一个子集。 其中一个视频包含狗出现的部分,另一个视频包含孩子出现的部分。 这两个分割的视频成为两个独立的请求,由不同模型服务器分片单独处理。
图 4-10 分片服务模式的示例架构,其中一个高分辨率视频被分割为两个独立的视频。 每个视频代表原始大请求的一个子集,并由不同模型服务器分片独立处理。
The high-resolution video is divided into two separate videos and sent to each of the model server shard. 高分辨率视频被分割为两个单独的视频并发送到各个模型服务器分片。 Model server replicas 模型服务器副本 Model storage 模型存储 Trained ML model 经过训练的机器学习模型 Send request 发送请求 Retrieve model 检索模型 Send data 发送数据 Obtain model 获取模型 Process data 处理数据 Generate entities 生成实体
在模型服务器分片收到子请求(其中每个子请求都包含原始大型模型服务请求的一部分)后,每个模型服务器分片接着从模型存储中检索之前训练好的机器学习模型,然后处理请求中的视频以标记视频中可能出现的实体类别,这与我们之前设计的模型服务系统类似。 一旦所有子请求都被各个模型服务器分片处理完毕,我们合并两个子请求(即狗和孩子这两个实体)的模型推理结果,以获得原始大型模型服务请求(包含高分辨率视频)的结果。
我们如何将两个子请求分发到不同的模型服务器分片? 与我们用来实现负载均衡器的算法类似,我们可以使用与哈希函数非常相似的分片函数来确定模型服务器分片列表中的哪个分片应负责处理哪一个子请求。
通常,分片函数是使用哈希函数(hash)和模运算符(%)定义的。 例如,即使哈希函数的输出远大于分片服务中的分片数量,hash(request) % 10 也会返回 10 个以内的分片数。
分片哈希函数的特点 定义分片函数的哈希函数将任意对象转换为表示特定分片索引的整数。 它有两个重要特性: 1 对于相同的输入,哈希函数的输出始终相同。 2 分片函数输出值的分布始终是均匀的。 这些特性非常重要,可以确保特定请求始终由同一分片服务器处理,并且请求在分片之间均匀分布。
分片服务模式解决了我们在构建大规模模型服务系统时遇到的问题,并提供了一种处理大型模型服务请求的方法。 它类似于我们在第 2 章中介绍的数据分片模式:我们不是将分片应用于数据集,而是应用于模型服务请求。 当一个分布式系统的单台机器的计算资源有限时,我们可以应用这种模式将计算负担分摊到多台机器上。
4.3.3 讨论 分片服务模式有助于处理大型请求,并有效地将处理大型模型服务请求的负载分摊给多个模型服务器分片。 当请求超出单台机器所能容纳的数据量时,这种模式通常非常有用。
然而,与我们在上一节中讨论的副本服务模式(在构建无状态服务时非常有用)不同,分片服务模式通常用于构建有状态服务。 在我们的例子中,我们需要使用分片服务维护原始大型请求中的子请求的状态或结果,然后将结果合并到最终的响应中,使得它能包含原始高分辨率视频中的所有实体类别。
在某些情况下,这种方法可能不太适用,因为这取决于我们如何将原始大请求分割为更小的请求。 例如,如果原始视频被分割为两个以上的子请求,某些子请求可能没有意义,因为它们可能不包含模型能够识别的完整实体。 对于这种情况,我们需要对合并结果进行额外的处理,以删除对我们无用且无意义的实体。
在构建大规模模型服务系统以处理大量大型模型服务请求时,副本服务模式和分片服务模式都起到了至关重要的作用。 然而,要将它们整合到模型服务系统中,我们还需要了解服务所需的计算资源,如果模型服务的流量是动态变化的,我们的资源可能无法被正常使用。 在下一节中,我将介绍另一种模式,重点关注可以处理动态流量的模型服务系统。
4.3.4 练习 1 垂直扩展有助于处理大型模型请求吗? 2 模型服务器分片是有状态的还是无状态的?
4.4 事件驱动处理模式 我们在 4.2 节中研究的副本服务模式有助于处理大量模型服务请求,而 4.3 节中的分片服务模式可用于处理不适合单个模型服务器实例的大数据量请求。 虽然这些模式解决了构建大规模模型服务系统的挑战,但它们更适合这样一个场景:在开始接收用户请求之前,系统已经知道需要分配多少计算资源、模型服务器副本或模型服务器分片。 但当系统知道在系统开始接受用户请求之前要分配多少计算资源、模型服务器副本或模型服务器分片时,它们更合适。 然而,在我们不知道系统将接收到多少模型服务流量的情况下,很难有效地分配和使用计算资源。
现在想象一下,我们为一家提供节日和活动规划服务的公司工作。 我们希望提供一项新服务,该服务将使用训练好的机器学习模型,来根据日期和客户想要度假的位置,预测度假区酒店每晚的价格。
为了提供这项服务,我们可以设计一个机器学习模型服务系统。 该模型服务系统提供一个用户界面,用户可以在其中输入他们感兴趣的假期日期范围和位置。 一旦请求被发送到模型服务器上,之前训练好的机器学习模型将从分布式数据库中被检索出来,并用于处理请求中的数据(日期和位置)。 最终,模型服务器将返回给定日期范围内每个位置的酒店价格预测信息。 完整流程如图 4-11 所示。
在我们对选定的客户测试该模型服务系统一年后,我们将能够收集到足够的数据来绘制随时间变化的模型服务流量。 事实证明,人们更倾向于在假期前的最后一刻预订假期,因此假期前不久流量会突然增加,然后在假期结束后减少。 这种流量特性导致了非常低的资源利用率。
在我们当前的模型服务系统架构中,分配给模型的底层计算资源始终保持不变。 这种策略似乎并非最佳:在低流量时,我们的大部分资源都处于空闲状态,从而被浪费。而在高流量时,我们的系统难以及时响应,并且需要比平常更多的资源来运行。 也就是说,系统必须使用相同数量的计算资源(例如:10 个 CPU 和 100 GB 内存)来处理高流量和低流量的场景,如图 4-12 所示。
图 4-11 预测酒店价格的模型服务系统示意图
Users enter date range and location and then submit requests to the serving system. 用户输入日期范围和位置,然后提交请求到服务系统。 Model server replicas 模型服务器副本 Model storage 模型存储 Trained ML model 经过训练的机器学习模型 Send request 发送请求 Retrieve model 检索模型 Send data 发送数据 Obtain model 获取模型 Process data 处理数据 Generate entities 生成实体 City: San Francisco 城市: 旧金山 Hotel name 酒店名 Hotel 1 酒店 1 Hotel 2 酒店 2 Price/night 每晚价格
图 4-12 在始终分配等量计算资源的情况下,模型服务系统的流量随时间变化
The peak traffic arrives at about one week away from Christmas (10 CPUs and 100 GBs of memory). 距离圣诞节一周左右出现流量高峰(10 个 CPU 和 100 GB 内存)。
The traffic decreases dramatically during Christmas (10 CPUs and 100 GBs of memory). 圣诞节期间流量急剧下降(10 个 CPU 和 100 GB 内存)。
There is very little traffic at two weeks after Christmas(10 CPUs and 100 GBs of memory). 圣诞节后两周流量非常少(10 个 CPU 和 100 GB 内存)。
Number of total requests per day 每天的总请求数 Thanksgiving 感恩节 Christmas 圣诞节 time 时间
既然我们或多或少知道这些假期是什么时候开始的,我们能否提前计算流量大小呢? 不幸的是,某些突发事件会让流量突增的预测变得困难。 例如,在某个度假村附近举行了一场大型国际会议,如图 4-13 所示。 这一意外事件发生在圣诞节前,突然增加了该时间窗口的流量(实线)。 如果我们不知道会议的情况,就会错过分配计算资源时应该考虑的时间窗口。 在我们的具体场景中,虽然针对我们的用例进行了优化(两个 CPU 和 20 GB 内存),但在这个时间窗口内的资源已经不足以用户处理请求了。 这样用户体验将会变得非常糟糕:想象一下所有参会者都坐在笔记本电脑前,长时间等待着预订酒店房间。
图 4-13 模型服务系统的流量随时间变化,并为不同时间窗口分配了最佳数量的计算资源。 此外,圣诞节前发生了一个意外事件,导致该特定时间段内的流量突然增加(实线)。
2 CPUs and 20 GBs of memory Resource utilization rate: high 2 个 CPU 和 20 GB 内存,资源利用率:高
10 CPUs and 100 GBs of memory Resource utilization rate: high 10 个 CPU、100 GB 内存,资源利用率:高
Huge international conference 大型国际会议
2 CPUs and 20 GBs of memory Resource utilization rate: high and unable to handle all requests within this time window 2 个 CPU、20 GB 内存,资源利用率:高,但无法处理该时间窗口内的所有请求
20 CPUs and 200 GBs of memory Resource utilization rate: high 20 个CPU、200 GB 内存,资源利用率:高
1 CPU and 10 GBs of memory Resource utilization rate: high 1 个 CPU 和 10 GB 内存,资源利用率:高
Number of total requests per day 每天的总请求数 Thanksgiving 感恩节 Christmas 圣诞节 time 时间
这种简单的解决方案仍然不是非常实用和有效,因为划分不同的时间窗口以及计算每个时间窗口需要多少计算资源并不是一件容易的事。 我们能想到更好的方法吗?
在我们的场景中,我们处理的是随时间变化的动态模型服务请求量,并且请求量与节假日的时间高度相关。 如果我们能保证有足够的资源,并且暂时忽略提高资源利用率的目标呢? 如果计算资源始终保证充足,我们就可以确保模型服务系统能够处理节假日期间的大流量。
4.4.1 问题:基于事件响应模型服务请求 我们可以让系统在可能经历大流量的任何可能的时间窗口之前相应地预估和分配计算资源,但这是不可行的。 因为确定大流量的确切时间窗口以及每个时间窗口所需的确切计算资源量并不容易。
简单地将计算资源增加并始终保持足够的量也是不切实际的,因为我们之前关心的资源利用率仍然很低。 例如,如果在特定时间段内几乎没有用户请求,那么我们分配的计算资源大部分处于空闲状态,造成资源的浪费。 是否有另一种方法可以更灵活地分配和使用计算资源呢?
4.4.2 解决方案 我们的解决方案是维护一个共享的计算资源池(例如,CPU、内存、磁盘等),这些资源不仅分配给这个特定的模型服务系统,还分配给其他模型服务或分布式机器学习流水线的其他组件。
图 4-14 是一个示例架构图,其中共享资源池被不同的系统同时使用,例如:数据摄取、模型训练、模型选择、模型部署和模型服务。 这个共享资源池通过预先分配历史峰值流量期间所需的资源,并在达到瓶颈时自动扩展,从而为我们提供了足够的资源来处理模型服务系统的峰值流量。 因此,系统按需使用资源,并且只使用每个模型服务请求所需的特定资源量。
在这里我们只关注图中的模型服务系统,暂时忽略其他系统的细节。 此外,我们假设模型训练组件只使用一般的资源类型,例如 CPU。 如果模型训练组件需要 GPU 或 CPU/GPU 混合,则根据具体用例,使用单独的资源池可能会更合适。
当使用我们的酒店价格预测应用的用户在页面中输入他们感兴趣的假期日期范围和地点时,模型服务请求将被发送到模型服务系统中。 在接收到请求后,系统会通知共享资源池系统需要使用的确切资源量。
例如,图 4-15 展示了模型服务系统的流量随着时间的推移出现了意外的流量高峰。 这一意外的流量增长是由于圣诞节前举行的一次大型国际会议。 这个事件突然增加了流量,但模型服务系统从共享资源池借用了所需的资源,成功应对了突增的流量。在共享资源池的帮助下,在突发事件期间,系统能保持高资源利用率。 共享资源池监控当前可用资源量并在需要时自动扩展。
图 4-14 一个架构图,其中共享资源池被不同组件(例如:数据摄取、模型训练、模型选择和模型部署)以及两个不同的模型服务系统同时使用。 实线箭头表示资源,虚线箭头表示请求。
Model serving system 1 模型服务系统 1 Shared resource pool 共享资源池 Data ingestion 数据摄取 Model training 模型训练 Model selection 模型选择 Model deployment 模型部署 Model serving requests 模型服务请求 Pull resources 拉取资源
图 4-15 模型服务系统的流量随时间的变化。圣诞节前发生了一次意外的流量高峰,导致流量突然增加。 模型服务系统通过从共享资源池中借用必要的资源量成功地处理了突增的请求。在突发事件期间,系统仍然能保持高资源利用率。
2 CPUs and 20 GBs of memory Resource utilization rate: high 2 个 CPU、20 GB 内存,资源利用率:高
10 CPUs and 100 GBs of memory Resource utilization rate: high 10 个 CPU、100 GB 内存,资源利用率:高
Huge international conference 大型国际会议
“Borrow” necessary resource from resource pool Resource utilization rate: high 从资源池借用需要的资源,资源利用率:高
20 CPUs and 200 GBs of memory Resource utilization rate: high 20 个 CPU、200 GB 内存,资源利用率:高
1 CPU and 10 GBs of memory Resource utilization rate: high 1 个 CPU、10 GB 内存,资源利用率:高 Number of total requests per day 每天的总请求数 Thanksgiving 感恩节 Christmas 圣诞节 time 时间
这种系统监控用户请求,并在用户发出请求时作出响应并使用资源的方法,称为事件驱动处理。
事件驱动处理与长期运行的服务的对比 事件驱动处理与我们在前面几节中讨论过的模型服务系统不同(例如,使用副本服务[第 4.2 节]和分片服务模式[第 4.3 节]的系统),这些服务器始终处于运行状态。 这些长时间运行的服务系统适用于许多负载较高、在内存中保存大量数据或需要后台处理的应用程序。 然而,对于在非高峰时段处理少量请求的应用程序,例如:酒店价格预测系统,使用事件驱动处理模式更合适。 近年来,随着云厂商函数即服务(function-as-a-service)产品的出现,这种事件驱动处理的模式蓬勃发展了起来。
在我们的场景中,酒店价格预测系统的每个模型服务请求都代表一个事件。服务系统监听此类事件,使用共享资源池中的所需资源,从分布式数据库中检索并加载训练好的机器学习模型,估算指定时间或地点的酒店价格。 这个事件驱动模型服务系统的示意图如图 4-16 所示。
使用这种事件驱动处理模式,我们可以确保系统按需使用资源,而不必担心资源利用率和资源空闲的问题。 这样,系统就有足够的资源来处理高峰流量并返回预测价格,并且用户在使用系统时不会感到明显的延迟或滞后。
尽管我们现在有了一个充足计算资源的共享资源池,可以从共享资源池借用计算资源来按需处理用户请求,但我们还应该在模型服务系统中建立一种机制来防御拒绝服务(Denial-of-service)攻击。 拒绝服务攻击会中断授权用户对计算机网络的访问,通常是来自攻击者的恶意攻击,并且在模型服务系统中十分常见。 这些攻击可能会导致共享资源池中计算资源的被恶意占用,最终可能导致依赖资源池的其他服务资源短缺。
拒绝服务攻击可能在各种情况下发生。例如,它们可能来自在很短的时间内发送大量模型服务请求的用户。 开发者可能错误配置了调用模型服务 API 的客户端,导致它不断发送请求,或者在生产环境中意外启动了负载或压力测试。
图 4-16 预测酒店价格的事件驱动模型服务示意图
Users enter date range and location and then submit requests to the serving system. 用户输入日期范围和地点,然后向服务系统提交请求 Shared resource pool 共享资源池 Model server instances 模型服务器实例 Pull resources 拉取资源 Model storage 模型存储 Trained ML model 经过训练的机器学习模型 Send request 发送请求 Retrieve model 检索模型 Send data 发送数据 Obtain model 获取模型 Process data 处理数据 Generate entities 生成实体 City: San Francisco 城市: 旧金山 Hotel name 酒店名 Hotel 1 酒店 1 Hotel 2 酒店 2 Price/night 每晚价格
为了应对这些在现实中经常发生的情况,引入防御拒绝服务攻击的机制是非常重要的。 一种避免这些攻击的方法是限流:将模型服务请求添加到队列中,并限制系统处理队列中请求的速率。
图 4-17 是 4 个模型服务请求的流程图。 其中有两个请求被限流,因为该系统最多允许两个并发的模型服务请求。 在这种情况下,模型服务请求的限流队列首先检查收到的请求是否低于当前的速率限制。 一旦系统处理完这两个请求,它将继续处理队列中剩余的两个请求。
如果我们部署并向用户公开模型服务的 API,通常的做法是对匿名用户设置相对较低的请求速率限制(例如,一小时内只允许请求一次),然后要求用户登录以获得更高的速率限制。 这样的系统能够更好地控制和监控用户的行为和流量,以便我们采取必要的措施来解决任何潜在的问题(例如:拒绝服务攻击)。 例如,要求用户登录可以方便对用户行为进行审计,从而找出哪些用户的行为导致了意外的模型服务请求。
图 4-17 发送到模型服务系统的 4 个模型服务请求的流程图。 其中有两个请求被限流,因为该系统最多允许两个并发的模型服务请求。 一旦系统处理完这两个请求,它将继续处理队列中剩余的两个请求。
Model serving requests 模型服务请求 Requests rate limiting queue 请求速率限制队列 Model serving system 模型服务系统 Ok to add to the queue? 是否添加到队列中?
Model serving requests that are under rate limit: 2 maximum concurrent requests 模型服务请求速率限制:最大并发请求数为 2
图 4-18 演示了前面描述的策略。 在图中,左侧的流程图与图 4-17 相同,共有 4 个来自未认证用户的模型服务请求发送到了模型服务系统。 然而,由于当前的速率限制,系统只能同时处理两个请求,允许未认证的用户最多发送两个并发的模型服务请求。 相反,右侧流程图中的模型服务请求都来自认证用户。此时模型服务系统可以处理 3 个请求,因为经过身份认证的用户的最大并发请求数的限制是 3。
图 4-18 对经过身份认证和未经身份认证的用户使用不同限流策略的对比
Model serving requests from unauthenticated users 来自未经身份验证用户的模型服务请求 Model serving requests from authenticated users 来自经过身份验证用户的模型服务请求 Requests rate limiting queue 请求速率限制队列 Model serving system 模型服务系统
Ok to add to the queue? 是否添加到队列中?
Model serving requests that are under rate limit: 2 maximum concurrent requests for unauthenticated users 模型服务请求速率限制:未经身份认证的用户的最大并发请求数为 2
Ok to add to the queue? 是否添加到队列中?
Model serving requests that are under rate limit: 3 maximum concurrent requests for authenticated users 模型服务请求速率限制:经过身份认证的用户的最大并发请求数为 3
速率限制取决于用户是否经过身份认证。因此,限流有效地控制了模型服务系统的流量,防止了可能导致共享资源池计算资源被恶意占用,最终导致依赖资源池的其他服务资源短缺。
4.4.3 讨论 尽管我们已经看到事件驱动处理模式如何使我们的特定服务系统受益,但我们不应该试图将此模式作为通用解决方案。 还可以使用许多其他的工具和模式来帮助您开发分布式系统,以满足独特的需求。
对于流量稳定的机器学习应用程序(例如:根据计划定期计算的模型预测服务),事件驱动的处理方法是不必要的,因为系统已经知道何时处理请求,并且尝试去监控这种规律性流量会有太多的开销。 此外,那些可以容忍一定预测精度损失的应用不需要事件驱动也可以很好地运行;它们还可以重新计算并提供特定时间粒度(例如:每天或每周)的预测。
事件驱动处理更适合流量多变的应用,对于这种应用,系统难以预先准备好所需的计算资源。 通过事件驱动处理,模型服务系统按需申请必要的计算资源。 服务可以提供更准确和实时的预测,因为它们在用户发送请求后进行实时预测,而不是依赖于定期预先计算的预测结果。
从开发者的角度来看,事件驱动处理模式的好处之一是它非常直观。 例如,它极大地简化了将代码部署的过程,因为除了代码本身之外,没有制品需要构建或推送。 事件驱动处理模式使我们可以轻松地从笔记本电脑或浏览器部署代码到云中运行。
在我们的场景中,我们只需要部署训练好的机器学习模型,该模型可以通过用户请求触发推理。 部署后,该模型服务将被自动管理和扩展,无需开发人员手动分配资源。 换句话说,随着流量的增加,更多的模型服务实例将被创建,它们使用共享资源池来处理新增的流量。 如果模型服务功能因机器故障而失败,它将自动在共享资源池中的其他机器上重新启动运行。
鉴于事件驱动处理模式的特性,用于处理模型服务请求的每个函数都需要是无状态的,并且独立于其他模型服务请求。 每个函数实例不能有本地缓存,需要将所有状态统一存储在存储服务中。 例如,如果我们的机器学习模型强依赖于之前预测的结果(例如:时间序列模型),在这种情况下,事件驱动处理模式可能就不适用了。
4.4.4 练习 1 假设我们在模型服务系统的整个生命周期内为酒店价格预测服务分配了相同的计算资源量。随着时间的推移,资源利用率会怎么变化? 2 副本服务或分片服务是长期运行的系统吗? 3 事件驱动处理是无状态的还是有状态的?
4.5 习题答案 第 4.2 节 1 无状态。 2 模型服务器副本不知道要处理用户的哪些请求,并且当多个模型服务器副本尝试处理相同的请求时,可能会出现潜在的冲突或重复。 3 是的,只有当单个服务器每天的停机时间不超过 1.4 分钟时。
第 4.3 节 1 有帮助,但会降低总体资源利用率。 2 有状态。
第 4.4 节 1 它会随着时间的推移而变化。 2 是的。服务器需要保持运行以接受用户请求,并且需要始终分配和占用计算资源。 3 无状态。
总结 模型服务是指加载之前训练好的机器学习模型,对新输入数据生成预测或进行推理的过程。 副本服务有助于处理不断增长的模型服务请求,借助它可以实现水平扩展。 分片服务模式可支持系统处理大型请求,并将处理大型模型服务请求的负载分摊给多个模型服务器分片。 通过事件驱动处理模式,我们可以确保系统根据每个请求按需使用资源,而不必担心资源利用率。