Uber 是如何构建实时基础设施每天处理 PB 级数据的

21CTO导读:本文向大家讲述Uber的实时数据基础设施,“揭密”世界最大的实时租车平台的内部流程。

背景

Uber是一家著名的科技与互联网公司。在 2010 年代初推出了一款可让司机和乘客轻松连接的应用程序,从而改变了出租车市场。

Uber 由 Garrett Camp 和 Travis Kalanick 于 2009 年创立。

二人在巴黎参加技术会议时提出来将开发一款拼车应用程序。由于对传统的出租车服务感到失望,他们希望引入一种更高效、更可靠的出行方式,利用技术将乘客与附近的司机联系起来,为乘客提供最大化的便利。

UberCab是 Uber 的第一个版本,于 2010 年在旧金山上线。该应用程序允许用户通过智能手机叫车并付款,票价根据距离与时间综合计算。 

与传统出租车不同的是,Uber司机并不是有执照的专业人士,而是拥有汽车的普通人,他们与Uber签约,并以兼职或全职工作的形式提供乘车服务。

到 2023 年,已经有 1.37 亿人每月使用一次 Uber 或 Uber Eats 优食。此外,到2023 年,Uber 司机完成了 94.4 亿次出行。为了支持平台业务,Uber 积极利用数据分析和机器学习模型进行运营。

从Uber 乘车的动态定价到UberEats Restaurant Manager 仪表盘,所有这些都必须利用实时数据进行高效操作。

在本篇文章里,让我们一起来看 Uber 如何管理并支持众多实时应用程序的幕后基础设施。

语境

数据是从许多来源持续收集的:司机、乘客、餐馆、食客以及商家后端服务。Uber 处理这些数据,以提取有价值的信息,以便为许多实际用例(例如客户激励、欺诈检测和机器学习模型预测)做出实时决策。

实时数据处理在 Uber 的业务中发挥着相当重要的作用。该平台依靠着开源解决方案和内部改进来构建实时基础设施。

从较高层面来看,Uber 的实时数据处理包括以下三个主要领域:

  • 消息平台:允许生产者和订阅者之间进行通信。

  • 流处理:允许将处理逻辑应用于消息流。

  • 在线分析处理 (OLAP):可以接近实时的对所有数据进行分析查询。

每个领域都面临三个基本的扩展挑战:

  • 扩展数据:传入的实时数据总量呈指数级增长。此外,Uber的基础设施分布在多个地理区域,如果想实现高可用性,这意味着系统必须能够大量增加的数据处理,同时保持数据新鲜度、减少端到端延迟和可用性SLA。

  • 扩展用例:随着 Uber 业务的增长,随着组织不同部门之间不同需求的出现,新的用例不断出现。

  • 用户类型扩展:与实时数据系统交互的不同用户具有不同的技术&技能水平,从没有工程背景的业务用户到需要开发复杂的实时数据管道的高级用户。

对基础设施的要求

Uber 的实时基础设施需要以下几点:

  • 一致性:关键应用程序需要所有区域的数据一致性。

  • 可用性:基础设施必须具有 99.99% 的高可用性保证。

  • 新鲜度:大多数用例需要二级新鲜度。这确保了响应特定事件(例如安全事件)的能力。

  • 延迟:某些用例需要对原始数据执行查询,并要求 p99 查询延迟低于 1 秒。

  • 可扩展性:系统可以随着数据量的不断增长而扩展。

  • 成本:Uber需要较低的数据处理和服务成本来确保高运营效率。

  • 灵活性:Uber 必须提供一个编程和声明式接口来表达计算逻辑,以服务不同的用户类别。

构建模块

在本节中,我们将了解 Uber 基础设施的主要逻辑构建块:

  • 存储:该层为其他层提供对象或 blob 存储,并保证写后读一致性。它用于长期存储,应针对高写入速率进行优化。Uber 使用该层将数据回填或引导到流或 OLAP 数据表中。

  • Stream:它充当发布/订阅接口,应该针对读取和写入的低延迟进行优化。它需要对数据进行分区并保证至少一次语义计算。

  • 计算:该层提供流和存储层上的计算。还要求源和接收器之间至少有一种语义。

  • OLAP:该层针对来自流或存储的数据,提供有限的 SQL 功能,还要对其进行优化以服务分析查询。在从不同来源获取数据时,它至少需要一次语义计算。某些用例要求根据主键仅摄取一次数据。

  • SQL是 计算层和 OLAP 层之上的查询层。它将 SQL 语句编译为计算函数,可以应用于流或存储。与OLAP层配合使用,会增强OLAP层的SQL限制能力。

  • API:高层应用程序访问流或计算功能的编程方式。

  • 元数据:管理所有层的各种元数据的简单接口。该层需要元数据版本控制和跨版本的向后兼容性。

以下部分将为大家介绍 Uber 针对相应构建模块采用的开源系统。

Apache Kafka

流式存储

Apache Kafka是业界广泛采用的流行开源事件流系统。

它最初由 LinkedIn 开发,随后于 2011 年初开源。除了性能较好之外,采用 Kafka 的其它几个因素还包括简单性、生态系统成熟度和成熟的开源用户社区。

在 Uber,他们拥有全球最大的 Apache Kafka 部署之一:每天有数万亿条消息和 PB 级数据。Uber 的 Kafka 支持许多工作流程:从乘客和司机应用程序传播事件数据、启用流分析平台或将数据库更改日志发送给下游订阅者。由于Uber独特的规模特征,他们对Kafka进行了以下增强功能的定制:

集群联邦

逻辑集群

Uber 开发了一个联合 Kafka 集群设置,可以向生产者和消费者隐藏集群详细信息:

  • 他们向用户公开“逻辑 Kafka 集群”。用户并不需要知道主题位于哪个集群。

  • 专用服务器集中集群和主题的所有元数据,以将客户端的请求路由到物理集群。

  • 此外,集群联合有助于提高可扩展性。当集群充分被利用时,Kafka服务可以通过添加更多集群来水平扩展。新主题在新集群上无缝创建。

  • 集群联合还简化了主题管理。由于应用程序和客户端较多,在 Kafka 集群之间迁移实时主题需要大量工作。在大多数情况下,该过程需要手动配置以将流量路由到新集群,这会导致消费者重新启动。集群联合有助于将流量重定向到另一个物理集群,而无需重新启动应用程序。

死信队列

失败的消息队列

在某些情况下,下游系统无法处理消息(例如,消息损坏)。最初,有两种选择来处理这种情况:

  • 卡夫卡丢弃这些消息。

  • 系统无限期地重试,这会阻止后续消息的处理。

然而,Uber 有很多场景既不需要数据丢失,也不需要阻塞处理。

为了解决此类用例,Uber在 Kafka 之上构建了死信队列 (Dead Letter Queue)策略:如果消费者在重试后无法处理消息,它将将该消息发布到 DLQ。这样,未处理的消息将被单独处理,不会影响其他消息。

消费者代理

中间层

Uber 已有数以万计的运行 Kafka 的应用程序,调试它们和升级客户端库都很困难。用户还在组织内部使用多种编程语言与 Kafka 交互,这使得提供多语言支持变得非常具有挑战性。

Uber 建立了消费者代理层来应对挑战;代理从 Kafka 读取消息并将它们路由到 gRPC 服务端点。它处理消费者库的复杂性,应用程序只需要采用瘦 gRPC 客户端。当下游服务无法接收或处理某些消息时,代理可以重试路由,并在多次重试失败后将其发送到DLQ。

代理还将 Kafka 中的传递机制从消息轮询更改为基于推送的消息调度。这提高了消费吞吐量并允许更多并发应用程序处理机会。

跨集群复制

集群之间高效的主题复制

由于业务规模较大,Uber在不同的数据中心分别部署和应用多个Kafka集群。此种部署,Uber 需要 Kafka 的跨集群数据复制,原因有两个:

  • 用户需要对各种用例的数据进行全局视图。例如,他们必须整合和分析来自所有数据中心的数据以计算行程指标。

  • Uber 复制了 Kafka 部署,以在发生故障时实现冗余。

Uber 构建并开源了一个名为uReplicator的可靠解决方案,用于 Kafka 复制目的。复制器具有重新平衡算法,可在重新平衡期间将受影响的主题分区的数量保持在尽可能低的水平。

此外,在流量突发的情况下,它可以在运行时将负载重新分配给备用工作人员。

我对 uReplicator 的高级架构进行了一些研究,以下是我的发现:

  • Uber 使用Apache Helix进行 uReplicator 集群管理。

  • Helix控制器负责将主题分区分配给worker、处理主题/分区的添加/删除、检测节点故障以及重新分配那些特定的主题分区。

  • 收到主题/分区复制请求后,Helix 控制器将主题/分区与活动工作线程之间的映射更新到Zookeeper服务,该服务的作用类似于中央状态管理服务。

  • 当映射发生变化时,worker 中的 Helix 代理将收到通知。

  • DynamicKafkaConsumer实例,工作人员将在其中执行复制的任务。

Uber 还开发并开源了另一项名为Chaperone的服务,以确保跨集群复制不会丢失数据。它收集关键统计数据,例如来自每个复制阶段的唯一消息的数量。然后,Chaperone 会比较统计数据,并在出现不匹配时生成警报。

Apache Flink

流处理