在 Facebook 数十亿用户眼里,Facebook 的服务看起来就像是一个统一的移动 App 或网站。但从公司内部看,却有着不同的视角。Facebook 提供了数千种服务,功能包罗万象,从均衡互联网流量到转码图像,再到提供可靠的存储。Facebook 的整体效率是各项服务效率的总和,而其中的每项服务通常都有自己的优化方式,这些方式难以进行泛化或适应款速变化的节奏。
为了更有效地优化这些服务,并灵活地适应不断变化的内部服务网络,我们开发了 Spiral。Spiral 采用了实时机器学习技术,致力于在 Facebook 范围内实现自调节的高性能基础设施服务。通过使用 Spiral 取代手工调整模式,我们可以在几分钟内对更新的服务进行优化,而不是几周。
应对伸缩性挑战
在 Facebook,变化发生得非常快。Facebook 代码库每隔几个小时就会被推到生产环境。在这个高度动态化的世界中,试图通过手动微调服务的方式来保持高效是不切实际的。手动重写缓存策略或做出其他手动调整实在太困难了。我们必须从根本上改变维护软件的思维方式。
为了有效地解决这一挑战,系统需要具备自我调整能力,而不是依赖手动硬编码或参数设置。这种转变促使 Facebook 工程师以一种新的方式开展工作:工程师现在不再通过查看系统生成的图表和日志来验证正确和高效的操作,而是直接在代码中声明。今天,Facebook 的工程师不再指定如何对请求做出正确的响应,而是通过编码的方式告诉自调整系统应该如何提供反馈。
传统的缓存策略可能看起来像是一棵带有分支的树,它根据对象的大小、类型和其他元数据来决定是否缓存它们。自调整缓存可以通过不同的方式来实现。这种系统记录了缓存项的访问历史:如果某个缓存项从未被访问过,那么缓存它可能不是个好主意。在机器学习中,使用元数据(特征)和相关反馈(标签)来区分缓存项的系统被称为“分类器”。这种分类器被用来决定哪些缓存项应该被缓存,并被不断地重复训练改进。这种持续的重复训练让系统即使是在环境变化后也能保持最新的状态。
从概念上讲,这种方法类似于声明式编程。SQL 就是这种编程范式的一个很好的例子:工程师只需要指定要计算的内容,然后引擎会找出最佳查询并执行它,而不需要指定如何进行复杂的计算。
在系统中使用声明式编程的挑战在于,要确保能够正确且完整地指定目标。与之前提到的自调整缓存策略一样,如果针对应不应该缓存某项内容的反馈不够准确或不够完整,系统就会很快学会提供不正确的缓存决策,这反而会降低性能。在使用 Spiral 时,精确定义自我调整所需的结果是最困难的部分。不过,我们也发现,经过几次迭代后,工程师往往能够聚焦在清晰而正确的定义上。
Spiral:易于集成和实时预测
为了让 Facebook 的系统工程师能够跟上不断变化的步伐,Facebook 波斯顿办公室的工程师们开发了 Spiral,一个只有很少依赖的小型嵌入式 C++ 库。Spiral 使用机器学习技术为资源受限的实时服务创建基于数据驱动的反应式方案。与手工编码相比,这个系统可以更快地开发和维护这些服务。
要集成 Spiral,需要在代码中添加两个调用:一个用于预测,一个用于反馈。预测调用是用于做出决策的智能输出,例如“该项是否应该被放进高速缓存?”预测调用为快速的本地计算,并且会在每个决策上执行。
反馈调用用于在某些情况下提供反馈,例如“此项在缓存中已过期但从未被命中,因此我们可能不应该缓存这样的项。”
这个库可以在嵌入模式下运行,它会向 Spiral 后端服务发送反馈和统计信息。后端服务可以可视化用于调试的有用信息,将数据记录到长期存储中以供后续分析,并训练和选择模型,在嵌入模式下没有足够的资源来训练这些模型,所以需要在后端进行。
发送到服务器的数据采用反偏见采样,以避免样本出现偏见渗透。例如,如果在一段时间内收到的负例样本比正例样本多 1,000 倍,我们只需要在 1,000 个负例样本中选择 1 个记录到服务器中,同时赋给它 1,000 的权重。服务器对数据全局分布的可见性通常会得到比单个节点的本地模型更好的模型。要实现这些不需要进行任何设置,只需要引入这个库并使用上面提到的两个函数。
在 Spiral 中,一旦有反馈进入,就开始学习。随着更多的反馈进入,预测质量会逐渐提高。大多数服务会在几秒到几分钟内生成反馈,所以开发周期非常短。领域专家可以添加新功能,并在几分钟内查看它是否有助于提高预测质量。
与硬编码方式不同,Spiral 可以适应不断变化的环境。以缓存准入策略为例,如果某些项被请求的频率不高,则可以在没有人工干预的情况下,通过反馈重新训练分类器,以减少将这些项放入缓存的可能性。
案例研究:自动化反应式缓存
Spiral 的第一个生产用例与 Phil Karlton 的著名格言完美契合,“计算机科学中只有两个难题:缓存失效和命名。”(我们已经为项目命名,所以剩下的事情就是使用 Spiral 解决缓存失效问题)。
在 Facebook,我们推出了一个反应式缓存,Spiral 的“用户”(也就是其他内部系统)可以订阅查询结果。从用户的角度来看,这个系统提供了查询结果和对结果的订阅。只要有外部事件影响了查询,它就会自动将更新的结果发送给客户端。这减轻了客户端轮询的负担,并减少了用于计算查询结果的 Web 前端服务的负载。
当用户提交查询时,反应式缓存首先将查询发送到 Web 前端,然后创建一个订阅,并返回结果。除了初始查询结果,缓存还会接收到在计算结果时修改的一系列对象和关联关系。然后,它开始监控对象或关联的数据库更新流。每当它看到可能会影响其中一个订阅的更新时,反应式缓存会重新执行查询并将结果与缓存进行比较。如果结果发生了变化,它会将新结果发送给客户端并更新自己的缓存。
这个系统面临的一个问题是,大量的数据库更新中只有很小一部分会影响查询的输出。对于“我的哪位朋友喜欢这个帖子?”这样的查询,就没有必要对帖子何时被阅读保持持续的更新。
这个问题类似于垃圾信息过滤:给定一条消息,系统应该将其分类为垃圾信息(不影响查询结果)还是 ham(会影响查询结果)?第一个解决方案是手动创建静态黑名单。这样做有可能是可以的,因为反应式缓存工程团队意识到超过 99%的负载来自一小组查询。对于低容量查询,他们只是假设所有更新都是 ham,并且对查询所引用对象的每次更新都重新执行查询。对于一小组高容量查询,他们通过创建黑名单来决定对象的哪些字段影响了查询的输出。对于每个黑名单,这个过程通常需要占用工程师几周的时间。更复杂的是,一些高容量查询不断发生变化,因此黑名单很快就会过时。每当使用缓存的服务修改了查询,系统就必须改变垃圾信息的过滤策略,这需要更多的工作量。
更好的解决方案:Spiral 垃圾信息过滤
重新执行查询后,只需将新的查询结果与旧的查询结果进行比较,就可以知道更新是垃圾信息还是 ham。这个机制被用于向 Spiral 提供反馈,以便创建分类器。
为了确保采样是无偏见的,反应式缓存只提供来自一小部分订阅的反馈。缓存不会对这些订阅的更新进行过滤,只要修改了相关的对象或关联,就会重新执行查询。它将新的查询输出与旧结果进行比较,然后向 Spiral 提供反馈——例如,告诉它更新“上次查看时间”不会影响“点赞数”。
Spiral 从所有反应缓存服务器收集这些反馈,并用它们为每种不同的查询类型训练分类器。这些分类器会被定期推送到缓存服务器。为新的查询创建过滤器或为响应 Web 层的变更行为更新过滤器不再需要工程团队的任何手动干预。随着对新查询的反馈的流入,Spiral 会自动为这些过滤器创建新的分类器。
更快的部署和 Spiral 的机会
使用基于 Spiral 的缓存失效机制,在反应式缓存中支持新查询所需的时间从几周降至几分钟。在 Spiral 出现之前,反应式缓存工程师必须通过运行实验和手动收集数据来检查每个新查询的副作用。有了 Spiral 之后,大多数用例都可以在几分钟内自动学习本地模型,因此可以立即获得本地推理。在大多数情况下,服务器能够使用来自多个服务器的数据在 10 到 20 分钟内训练好模型。在发布到每个服务器上后,这个更高质量的模型就可用于改进高保真推理。查询发生变更后,服务器能够自适应变更,并在收到更新的查询后重新学习新的模式。
查看英文原文: https://code.fb.com/data-infrastructure/spiral-self-tuning-services-via-real-time-machine-learning/
感谢郭蕾对本文的审校。
评论