优步最近推出了一项新功能:UberEats 上的广告。这种新能力带来了 Uber 需要解决的新挑战,例如广告拍卖、竞标、归因、报告等系统。本文重点介绍我们如何利用开源技术构建 Uber 的第一个“近实时”恰好一次事件处理系统。我们将深入了解我们如何实现一次性处理以及事件处理作业的内部工作原理的细节。

对于每个投放的广告,每个用户都有相应的事件(展示次数、点击次数)。广告事件处理系广告系统)。

这需要一个针对以下方面进行优化的系统:

  1. 速度:
    1. 下游广告系统(广告节奏、预算更新)需要用户生成的广告事件的实时上下文才能履行其职责。
    2. 客户将以最少的延迟看到他们的性能指标。
  2. 可靠性:
    1. 系统在数据完整性方面必须可靠。

      广告事件代表支付给优步的实际资金。

      如果事件丢失,优步就会失去潜在收入。

    2. 我们必须能够准确地向客户展示广告的效果。

      数据丢失会导致广告成功率低报,从而导致客户体验不佳。

  3. 准确性:
    1. 我们不能高估事件。

      重复计算点击次数,导致向广告商收取过高的费用并高估广告的成功率。

      两者都是糟糕的客户体验,这需要处理事件 恰好一次

    2. 优步是投放广告的市场,因此我们的广告归因必须 100% 准确。

为了满足这些需求,我们设计了一个高度依赖 4 项关键开源技术的架构:Apache Flink ^®^ 、Apache Kafka ^®^ 、Apache Pinot ^™^ 和 Apache Hive ^™^ 。以下是选择每种技术的原因。

该系统的核心构建块使用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 成为存储数据的绝佳解决方案,供数据科学家用于报告和数据分析的内部工具。

高层架构

现在我们知道了系统的构建块,让我们深入研究高级架构。

如上图,系统由 3 个 Flink 作业组成,它们摄取和下沉到 Kafka 主题,以及读取和写入依赖服务。它部署在我们称为区域 A 和区域 B 的 2 个区域中,以在区域出现故障时提高弹性。在 Kafka/Flink 方面,我们启用了仅一次语义来保证只有事务提交的消息才会被读取。在深入研究每个 Flink 工作之前,让我们进站谈谈这个系统是如何实现精确一次语义的。

恰好一次

如上所述,我们正在处理的一个主要约束是需要整个系统的恰好一次语义。这是分布式系统中最困难的问题之一,但我们能够通过多种努力来解决它。

首先,我们依靠 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 功能来确保我们永远不会复制具有相同标识符的记录。

Pinot Upsert

优步日志的一个常见挑战是使用卡夫卡中的变化更新比诺中的现有数据,并在实时分析结果中提供准确的视图。例如,财务仪表板需要使用更正后的乘车总费用报告订阅量,而餐厅老板需要分析UberEats订单及其最新交付状态。在广告事件处理案例中,需要检测和消除重复的副本。为了应对这一挑战,尤伯杯的数据基础设施团队为黑做出了贡献,并在实时这一过程中提供了对向上插入的原生支持。这极大地增强了比诺中的数据操作能力,并可以利用大量用例。要了解更多信息,请查看比诺文档上的用户指南。

现在我们将深入研究每个 Flink 工作以探索细微差别:

聚合作业

作业聚合了很多繁重的工作,因此我们将其详细描述为4个组件:数据清理、订单持久性、聚合和记录UUID生成。

数据清理

我们首先从 Mobile Ad Events Kafka 主题中提取代表广告点击和展示的单一广告事件流。使用过滤器运算符,我们可以根据某些参数验证事件,例如某些字段的存在或事件的年龄。使用Keyby运算符,我们将数据划分为逻辑分组,并使用Map运算符从输入流中删除重复事件。我们在重复数据删除映射器函数中利用 Flink 的键控状态来跟踪先前看到的事件。这可确保我们删除重复项并防止某些类型的欺诈。

订单归因的持久性

一旦我们有了“干净”的广告事件,我们就会将它们存储到一个Docstore 表(建立在 Schemaless 之上)中,以供订单归因作业使用。我们选择 Docstore 是因为它快速、可靠,并且允许我们设置在行上停留的时间。这反过来又使系统能够仅在我们的归因窗口存在期间存储事件。

聚合

对于聚合,我们首先根据广告标识符和它们应该对应的分钟时间段的组合来键控事件——我们这样做是为了确保我们始终将事件聚合到正确的时间范围内,而不管迟到。接下来,我们将事件放入一分钟长的Tumbling 窗口中。我们选择一分钟有几个原因:它足够小,对于依赖聚合数据的依赖客户来说是可以接受的;它是一个足够小的分析粒度(一分钟可以累积到一小时、六小时等等);同时,它是一个足够大的窗口,不会因写入而使我们的数据库过载。最后,我们使用聚合函数计算滚动窗口内的所有点击次数和展示次数。从性能的角度来看,聚合函数很棒,因为它们一次只在内存中保存一个事件,允许我们随着广告流量的增加而扩展。

记录 UUID 生成

最后一块是将聚合结果转化为只包含我们需要的信息的形式。作为此转换的一部分,我们生成一个记录 UUID,用作幂等性和重复数据删除键。鉴于在 Flink/Kafka 上启用了一次性语义,我们可以确信,一旦将消息插入到目标 Kafka 主题并提交,那么记录 UUID 可用于 Hive 中的重复数据删除和 Pinot 中的 upsert。

归因工作

归因工作更直接。它从 Kafka 主题中获取订单数据,该主题包含 UberEats 的所有订单数据并过滤掉无效订单事件。然后它会访问由聚合作业填充的 Docstore 表,并查询匹配的广告事件,如果匹配,则我们需要进行归因!然后我们调用一个外部服务,该服务提供有关用于丰富事件的订单的更多详细信息。然后为 Pinot 和 Hive 的幂等性生成唯一且确定性的记录 UUID。最后,我们将数据下沉到 Attributed Orders Kafka 主题,以供联合和加载作业使用。

联合和加载作业

最后一个作业负责将区域 A 和区域 B 中聚合作业输出的事件联合起来,然后将数据下沉到输出主题,该主题将数据分散到 Pinot 和 Hive 中以供最终用户查询。我们必须在区域之间联合事件,因为我们的 Pinot 部署是主动-主动的(它不会在两个区域之间复制),因此我们使用此作业来确保两个区域具有相同的数据。为了保持恰好一次语义,我们利用 Pinot 中的 upsert 功能。

现在我们已经了解了高级架构、我们如何实现精确一次以及每个单独的 Flink 作业的细节,我们可以看到我们如何满足我们的所有要求:

  1. 速度:
    1. 时间的主要瓶颈是 Flink 检查点间隔。鉴于我们的恰好一次约束,我们必须等待检查点被提交,然后事件才能被称为“处理”。默认检查点间隔是 10 分钟,我们是 2 分钟,所以它不完全是“实时”的,但它足以使我们的内部系统保持最新状态并在合理的时间段内为我们的客户报告广告效果.
  2. 可靠性:
    1. 我们从几个不同的过程中获得可靠性。跨区域复制允许我们在出现特定于数据中心的问题时进行故障转移,否则会导致数据丢失。如果处理中出现问题,Flink 的检查点允许我们从中断的地方继续。我们对 Kafka 主题有 3 天的保留期,以防我们还需要进行一些灾难恢复。
    2. 可靠性的最大痛点来自聚合作业本身。如果它出现故障,则处理事件会出现延迟,这可能会导致其他服务出现各种问题,主要是起搏。如果我们处理事件的速度不够快,那么起搏状态将不会更新,并可能导致超支。这是一个仍然需要一些解决方案的领域。
  3. 准确性:
    1. 通过 Kafka/Flink 上的完全一次语义和幂等性以及 Pinot 上的 upsert 的混合,我们能够提供我们需要的准确性保证,我们有信心我们只会处理一次消息。

在这篇博客中,我们展示了我们如何利用开源技术(Flink、Kafka、Pinot 和 Hive)来构建满足快速、可靠和准确系统要求的事件处理系统。我们讨论了在分布式系统中确保完全一次语义的一些挑战,并展示了我们如何通过生成幂等键和依赖 Pinot 的最新功能之一来实现这一点: