优步是如何使用Apache Flink和Kafka实现实时Exactly-Once广告事件处理?


优步最近推出了一项新功能:UberEats 上的广告。这种新能力带来了 Uber 需要解决的新挑战,例如广告拍卖、竞标、归因、报告等系统。本文重点介绍我们如何利用开源技术构建 Uber 的第一个“近实时”恰好一次事件处理系统。我们将深入了解我们如何实现一次性处理以及事件处理作业的内部工作原理的细节。
 
问题陈述:
对于每个投放的广告,每个用户都有相应的事件(展示次数、点击次数)。广告事件处理系广告系统)。
这需要一个针对以下方面进行优化的系统:

  1. 速度:
    1. 下游广告系统(广告节奏、预算更新)需要用户生成的广告事件的实时上下文才能履行其职责。
    2. 客户将以最少的延迟看到他们的性能指标。
  2. 可靠性:
    1. 系统在数据完整性方面必须可靠。广告事件代表支付给优步的实际资金。如果事件丢失,优步就会失去潜在收入。
    2. 我们必须能够准确地向客户展示广告的效果。数据丢失会导致广告成功率低报,从而导致客户体验不佳。
  3. 准确性:[list=1]
  4. 我们不能高估事件。重复计算点击次数,导致向广告商收取过高费用并高估广告的成功率。两者都是糟糕的客户体验,这需要处理事件恰好一次
  5. 优步是投放广告的市场,因此我们的广告归因必须 100% 准确。

  
架构
为了满足这些需求,我们设计了一个严重依赖 4 项关键开源技术的架构:Apache Flink  、Apache Kafka  、Apache Pinot  和 Apache Hive  。以下是选择每种技术的原因。
  • 使用 Apache Flink 进行流处理

该系统的核心构建块使用Apache Flink,这是一种用于近实时处理无界数据的流处理框架。它具有非常适合广告的丰富功能集,例如一次性保证、Kafka 连接器(Uber 的选择消息队列)、用于聚合的窗口函数,并且在 Uber 中得到了很好的集成和支持。 
 
  • 使用 Apache Kafka 的消息队列

Kafka 是 Uber 技术堆栈的基石:我们拥有世界上最大的 Kafka 部署之一,并且做了大量有趣的工作来确保它的性能和可靠性。Kafka 还可以提供一次性保证,并且可以很好地扩展到广告用例。
 
  • 使用 Apache Pinot 进行实时分析

广告事件处理系统的主要目标之一是为我们的客户快速提供性能分析:Apache Pinot 出现了。Pinot 是分布式、可扩展的在线分析处理 (OLAP) 数据存储。它专为分析查询的低延迟交付而设计,并支持通过 Kafka 进行近实时数据摄取。
 
  • 使用 Apache Hive 的数据仓库

Apache Hive是一个数据仓库,它通过允许通过 SQL 查询数据的丰富工具促进读取、写入和管理大型数据集。优步拥有通过 Kafka 的自动化数据摄取流,以及使 Hive 成为存储数据的绝佳解决方案,供数据科学家用于报告和数据分析的内部工具。

 
恰好一次
如上所述,我们正在处理的一个主要约束是要求在整个系统中使用恰好一次语义。这是分布式系统中最困难的问题之一,但我们能够通过多种努力来解决它。 
首先,我们依靠 Flink 和 Kafka 中的一次性配置来确保通过 Flink 处理并沉入 Kafka 的任何消息都是事务性地完成的。Flink 使用启用了“read_committed”模式的 KafkaConsumer,它只会读取事务性消息。作为本博客中讨论的工作的直接结果,Uber 启用了此功能。其次,我们为聚合作业生成的每条记录生成唯一标识符,下面将详细介绍。标识符用于下游消费者中的幂等性和重复数据删除目的。 
第一个 Flink 作业 Aggregation 使用来自 Kafka 的原始事件并按分钟将它们聚合到桶中。这是通过将消息的时间戳字段截断为一分钟并将其与广告标识符一起用作复合键的一部分来完成的。在这一步,我们还为每个聚合结果生成一个随机唯一标识符(记录 UUID)。
每分钟滚动窗口都会触发将聚合结果发送到处于“未提交”状态的 Kafka 接收器,直到下一个Flink 检查点触发。当下一个检查点触发时(每 2 分钟),消息将使用两阶段提交协议转换为“已提交”状态。这确保了存储在检查点中的 Kafka 读取偏移量始终与提交的消息一致。
Kafka 主题的使用者(例如,广告预算服务和联合与加载作业)被配置为仅读取已提交的事件。这意味着所有可能由 Flink 故障引起的未提交事件都将被忽略。所以当 Flink 恢复时,它会再次重新处理它们,生成新的聚合结果,将它们提交给 Kafka,然后它们可供消费者处理。
记录 UUID 用作广告预算服务中的幂等键。对于 Hive,它用作重复数据删除目的的标识符。在 Pinot 中,我们利用 upsert 功能来确保我们永远不会复制具有相同标识符的记录。
 
总结
在这篇博客中,我们展示了我们如何利用开源技术(Flink、Kafka、Pinot 和 Hive)来构建满足快速、可靠和准确系统要求的事件处理系统。我们讨论了在分布式系统中确保完全一次语义的一些挑战,并展示了我们如何通过生成幂等键和依赖 Pinot 的最新功能之一来实现这一点:Upsert。在撰写这篇博文时,该系统每周处理数亿个广告事件,并且每天都在增加。