跳到正文

驯服机器学习数据洪流:扩展特征一致性

了解 Uber 如何设计出高度可扩展的特征日志管道,以确保训练/服务的数据一致性,并提升大规模模型的性能。 #MachineLearning #MLOps #UberEngineering #DataEngineering #Scaling

本文目录文章开头

简介

User features flow through microservices and ML model to generate personalized food recommendations, with online and offline data sources.

图1:机器学习特征生命周期。

现代机器学习系统,尤其是用于 Uber Eats 餐饮推荐的大型推荐模型,依赖于持续、高质量、一致且新鲜的特征流。但在实践中,模型在生产环境中看到的值可能与训练时的不同:来源不同、计算逻辑不同,这会导致训练和推理的不一致。这些不一致和不匹配会降低模型有效性,增加调试时间,在严重情况下甚至会影响服务可靠性。

我们的目标是让推理时使用的特征与训练下一个模型迭代时使用的特征保持一致。特征的单一事实来源能带来更强的模型性能以及  更快的问题检测和修复。本博客介绍了我们如何通过特征日志框架在 Uber 扩展特征一致性。

问题

图2展示了在 Uber 的机器学习环境中,在线和离线训练管道在计算和使用特征方面的不一致如何导致机器学习模型性能的退化。 

Diagram comparing online prediction and offline training data pipelines, highlighting a regression due to language code mismatch.

图2:在线推理和离线训练管道如何计算和使用特征。

在线路径

在在线推理环境中,以下 特征 被传入模型以提供预测服务: 

  • 用户会话信息,如 user_id 和语言
  • 门店信息,如餐厅元数据
  • 预计算的行为特征,如过去的门店点击、订单等

训练期间,模型从固定的词汇表中学习,例如,语言编码为 en-US 或 fr-FR。然而在服务时,这些相同的特征可能以不同的格式到达,例如 en 而不是 en-US。这种不匹配意味着服务层传递的是模型从未训练过的值。因此,强信号会悄然退化:特征分布发生偏移,预测稳定性受损,而没有明显的故障。 

离线路径

相比之下,离线训练从多个日志来源聚合历史数据:

  • 应用日志
  • 机器学习特征 Apache Hive™ 表
  • 点击日志

这些数据被转换为离线特征表,成为模型训练的输入。最终数据集包括:

  • 用户会话信息
  • 门店信息
  • 机器学习特征
  • 用户操作(标签),如 did_click, did_order

离线特征计算管道开始生成具有不同格式约定的值,例如使用 _ 而不是 -。这导致训练数据包含 jp_JA 这样的值,而在线数据包含 jp-JA 这样的值。这种细微的格式不匹配导致模型从服务期间从未出现过的类别中学习,从而导致了另一个漂移来源。

其他问题包括: 

  • 脆弱的 ETL 血缘。 训练数据由许多相互依赖的 ETL 作业组装而成,这些作业可能存在缺失的分区或上游变更,从而悄然降低训练数据集的质量。
  • 新鲜度差距。一些关键特征从生产变更到训练可见性存在多天延迟,限制了模型的适应速度。
  • 开发者生产力损失。开发者将不得不花费数周时间来排查 ETL 和新鲜度问题。

我们的解决方案:特征日志记录

Diagram comparing online and offline data pipelines for ML, showing feature logging and unified log creation for training.

图 3:在线推理如何记录特征,以及离线训练管道如何消费这些特征。

我们设计并实现了一个特征日志记录框架,该框架在推理时记录特征,并将其作为规范的训练数据源。在推理时记录有几个主要好处:它通过记录模型在预测期间消费的确切特征值集合来保证特征一致性,并且只需要最小的调用方改动。

规模挑战

在实现这个框架时,我们在规模方面遇到了一些挑战。

带宽和数据量限制

Transformer 驱动的推荐系统可以产生极高的预测流量,吞吐量高达 800 万 QPS。如果对一整天或一个月内的每一次预测都收集特征日志,这将转化为 Uber 用例中巨大的数据量,每天约数千亿条记录,每月 5 万亿行。

即使不考虑压缩,所需的存储空间也将达到多 PB 级别,每天约 1.7 PB,而将这些数据从在线预测服务传输到下游管道所需的带宽,随着时间的推移,将使我们付出数百万美元级别的、高得令人望而却步的基础设施成本。在估算端到端基础设施成本(网络、Kafka™、存储)时,数字很快就会超出实际可行的范围,即使只记录基本的特征集也是如此。

过长的特征名称

系统内的特征名称往往冗长且描述性很强,通常包含命名空间和多个聚合层级。例如,store_unique_identifier_operational_meal_period_context_key。为每次预测传输这些长字符串会显著增加负载大小。

对于像Uber Eats 餐厅推荐模型这样的大规模模型,发送所有特征的估计出站数据速率可能增长到每秒约 9.3 GiB,远远超出现有 Kafka 集群的容量。

不必要的特征

客户端提供的特征远多于模型实际使用的特征,导致推理服务的请求负载臃肿。这些重复对带宽和存储需求贡献显著。

并非所有预测都对训练有用

在推荐系统中,打分是在数千个候选对象的批次上进行的,经过过滤和排序后,只有大约 40% 的候选对象被保留下来。这些排序后的候选对象中,只有 5% 最终成为用户设备上的曝光。这意味着:

  • 大多数已打分/排序的候选对象从未真正展示给用户
  • 记录所有这些对象对训练数据管道来说是浪费
  • 存储的大部分数据对模型的增量价值很小

因此,选择性日志记录比记录整个预测宇宙更加高效且更具成本效益。

在线架构深度解析

让我们来看看我们是如何设计在线推理日志以应对规模方面的挑战的。

Data flow diagram for a machine learning pipeline involving Kafka, Flink, and model training with app predictions.

图 4:在线推理日志特征的生成方式。

当预测请求到达推理端点时,服务会照常计算模型特征。在将预测返回给 Eats 应用程序之前,端点会查询特征允许列表(Feature Allow List)以确定应记录哪些特征。只有经过批准的特征才会被序列化并发布到 Kafka,从而最大限度地减少日志记录开销和网络成本。与此同时,Eats 应用会向用户展示预测结果,并在推荐被查看时发出客户端曝光事件。这些曝光事件会独立流式传输到 Flink™,在那里与之前从 Kafka 到达的已记录预测特征进行关联。这会创建一个完整的推理记录,其中既包含服务时使用的精确特征,也包含相应的用户交互。增强后的曝光记录随后被写入另一个 Kafka 主题,然后再持久化到离线存储中。由于每条记录已经包含在线特征值以及用户结果,因此数据可立即用于模型训练,从而消除了昂贵的离线特征关联,并最大限度地减少了训练-服务偏差。

特征允许列表

目前,预测请求通常包含许多模型并未使用的特征。通过实施特征允许列表,我们仅记录从特征存储中获取的必要特征。这种方法:

  • 将负载大小减少 4–5 倍
  • 显著降低 Kafka 容量需求
  • 简化特征日志记录管道

通过枚举实现特征名称别名

平台不会每天在 Kafka 网络管道中传输数十亿次诸如 store_unique_identifier_operational_meal_period_context_key 之类的原始字符串,而是在推理端点序列化时,自动将每个特征名称映射为一个紧凑、确定的整数 ID(即枚举值)。

曝光过滤

并非所有预测都对训练有贡献。推理生成的候选中只有一部分会被用户看到。为了减少不必要的存储和计算,我们使用 Flink 将预测数据与客户端事件进行关联,关联操作在 Kafka 流上执行,时间窗口关联用于处理事件延迟(例如用户曝光在预测数分钟后才到达)。根据分析,我们在内存中仅保留满足第 90 百分位会话到曝光时间所需的最短窗口(几分钟)。这种方法减少了在与客户端事件进行关联时需要保留的预测数量所带来的内存需求。

扩展一个生产级 Flink 作业让我们认识到,分布式系统很少仅受限于计算能力。最艰难的挑战隐藏在表面之下:状态管理、正确性、可观测性和渐进式优化。

Five key engineering lessons for scaling Flink jobs, focusing on profiling, state management, observability, small improvements, and automation.

扩展需要性能剖析

仅增加并行度并不能解决性能瓶颈。通过对单个 Flink 算子进行性能分析,我们识别出了若干瓶颈,并独立调优了 Flink 作业的每个阶段。这进一步印证了每个算子都有其独特的扩展特性,必须基于数据进行优化。最终,我们为预连接(pre-join)算子分配了 512 的并行度,为连接(join)算子分配了 768 的并行度——这是 Uber 部署的最大 Flink 并行度规模——以便其拥有最大的可用吞吐量来执行曝光过滤。

状态管理

我们使用 RocksDB 进行作业状态管理。我们采用的默认检查点(checkpoint)策略导致检查点大小以每小时超过 12 TB 的速度增长,以至于我们向云提供商写入状态的操作开始失败。我们无法继续使用默认的检查点策略。随着流量增长,RocksDB 状态成为延迟的主要来源,因此我们转向了自定义的状态管理策略。我们通过仅存储必要的元数据、在流连接发生后立即积极驱逐记录,以及对传入数据进行去重,来减少状态占用空间。结合细致的状态保留时间调优,这极大地减少了元数据开销,从而实现了高吞吐量。

可观测性助力更优优化

在进行大规模调优变更之前,我们投入精力构建了关于作业输出速率、时间窗口分布和管道行为的详细指标。这些洞察使得每一次优化都能被度量和验证,用数据驱动的工程方法取代了试错式的调优。

高吞吐量源于众多小改进

我们没有依赖单一的优化,而是通过一系列有针对性的增强来提高吞吐量,包括对象复用、类型化负载、配置缓存以及减少序列化开销。单独来看,这些改动的效果并不显著,但它们共同显著降低了 CPU 使用率和垃圾回收压力,并帮助我们将峰值消费者滞后(consumer lag)处理时间减少了约 70%。

在平台中构建验证与自动化能力

可靠的生产系统需要的不仅仅是快速的代码。自动回滚、告警即代码(alerts-as-code)、确定性验证查询,以及模拟类生产环境设置的测试平台,使我们能够在保持正确性、最大限度降低运维开销的同时,安全地演进管道。

向 Transformer 架构的过渡及序列特征的使用

基于 Transformer 的模型不再记录单行条目,而是将日志特征记录为多维的门店数组(例如 store_uuid: [taco_store, McD, burger_store...])。默认情况下,这会给下游 Kafka 负载限制以及为曝光过滤 Flink 作业处理门店级水位线带来巨大问题。该系统转而通过反射缓存检查模型的结构维度以检测数组特征,并在将负载发送到 Kafka 之前,逐元素将数组负载展平为显式的非数组键值记录。这确保了非 Transformer 模型生成的日志与 Transformer 模型生成的新型序列日志能够完美混合,从而在不耗尽资源的情况下训练下一代模型。此外,为了在 Kafka 上扩展如此庞大的记录量,我们以轮询方式将数据分片到多个 Kafka 集群中。

离线消费

一旦启用特征日志记录并开始通过推理端点积累特征,我们就会构建一个可用于训练下一代模型的表。  一旦数据验证完成并积累了足够的数据,我们便使用这个新的数据源来训练生产模型。  我们还会使用旧的 ETL 流水线为相同日期范围生成的表重新训练生产模型,以便进行公平比较。

当这个新训练的模型性能达到可接受水平时(通常意味着在 AUC 和 MAP 等指标上与生产模型持平或更优),我们就会继续训练候选模型,该模型将成为下一次实验的一部分。

结论

在将该系统推广到搜索和各种排序模型后,我们观察到了可衡量的运营改进:

  • 关键特征的不匹配率为 0%,而此前这些特征的不匹配率超过 10%
  • 时效性得到提升,许多优先特征的 SLA 从多天延迟缩短至数小时,从而加快了模型迭代速度

通过特征白名单、名称优化和选择性曝光日志记录,我们显著降低了特征日志记录的开销。这些优化不仅节省了基础设施成本,还提高了流水线的可靠性,使大规模模型训练更加高效和可持续。

致谢

如果没有众多团队成员的贡献,这一功能的推出是不可能实现的。特别感谢交付团队的 Abhi Kune 以及 Michelangelo、存储和流处理团队的工程师们。

封面图片出处:'belllerophon taming pegasus' no.2' 图片受 CC BY 2.0  许可协议保护,作者为 llahbocaj。图片未做任何修改。

Android® 是 Google LLC 的注册商标。

Apache®、Flink™、Hive™ 和 Kafka™ 是 Apache 软件基金会在美国和/或其他国家的注册商标或商标。使用这些标志并不意味着 Apache 软件基金会的认可。

iOS 是 Cisco 在美国和其他国家的商标或注册商标,由 Apple 根据许可使用。

阅读显示设置

调整正文排版

当前浏览器记住选择,应用于文章正文。手机上正文宽度以屏幕可用空间为限。

图表完整性提示

普通文章遇到图表保存问题时显示提示。