实时会话优步行程

0
实时会话优步行程

在某种意义上,优步在现实世界中高效匹配乘客和司机的挑战归结为如何收集、存储和逻辑地安排数据的问题。我们努力通过预测乘客需求来确保低等待时间,同时通过考虑交通和其他因素,让司机尽可能有效地使用平台,这只会扩大数据的范围。

为了更好地集中精力管理构成Uber市场的多个系统上的大量实时数据,我们开发了骑手会话状态机,这是一种对构成单一行程的所有数据事件的流进行建模的方法。

我们将每次出行背后的数据称为一个会话,从用户打开优步应用程序开始。该操作触发一系列数据事件,从乘客实际请求乘车到行程结束。由于每次会议都在有限的时间内进行,我们可以更容易地整理相关数据,供日后分析使用,以进一步提升我们的服务。在其他功能中,将Uber的行程数据分类为会话,可以更容易地理解和发现问题或引入新功能。

请继续阅读,了解我们如何设计这个新的会话状态机,以及在此过程中吸取的经验教训。

骑手会话状态机

我们想要实时捕捉和了解的关键信息之一是单次优步行程的完整生命周期,从乘客打开应用的那一刻到他们到达最终目的地。然而,考虑到我们系统的复杂性和规模,这些数据分布在多个不同的事件流上。

例如,当有人打开优步应用程序时,它会提示他们选择目的地,并在用户日志的事件流中触发一个事件。该应用程序显示该地理区域内可用的产品(uberPOOL、uberX、UberBLACK等)以及每种产品的价格,这些价格由我们的动态定价系统生成,每个价格在印象事件流中以离散事件的形式出现。当乘客选择一款产品时,该请求会发送到我们的调度系统,该系统将乘客与司机合作伙伴匹配,并将他们的车辆分配给该行程。当司机和搭档接车时,他们的应用程序会向调度系统发送一个“接车完成”事件,有效地启动行程。当司机到达目的地并指示乘客已经在他们的应用程序中下车时,它会发送一个“行程完成”事件。

像这样的典型行程生命周期可能跨越六个不同的事件流,事件由骑手应用程序、司机应用程序和优步的后端调度服务器生成。这些不同的事件流贯穿在一次优步行程中。

我们如何对这些事件流进行背景化,以便将它们在逻辑上分组在一起,并迅速为下游数据应用程序提供有用的信息?答案在于定义一个时间限制状态机为完成单个任务的不同用户和服务器生成的事件的流建模。我们将这种由原始动作组成的状态机称为“会话”。

在Uber的行程生命周期中,一个会话由一系列事件组成,从乘客打开他们的应用程序开始,到成功完成他们的行程结束。我们还必须考虑到,并非所有的行程都经过这一系列完整的事件,因为乘客可能会在提出请求后取消行程,或者只是打开应用程序查看票价。由于这些因素,对我们来说,在会话上强制执行时间窗口非常重要。

状态机的图形表示
图1:这个插图显示了Rider Session状态机中的事件流。

当用户打开应用程序时,行程会话开始,在应用程序的日志中生成一个离散事件。当用户浏览他们所在位置的优步产品时,我们的行程定价后端系统会向应用程序发送多个印象,显示每种产品的价格,在会话中启动购物状态。我们可以从应用程序的移动事件流(用于请求事件)和Dispatch系统生成的事件流(记录接收到的所有请求)中收集Request Ride状态。当司机按下应用程序上的“代取完成”按钮时,会话进入“on Trip”状态。当然,当司机按下应用程序上的“行程完成”按钮时,会话就结束了。

由于每个会话都对物理世界中发生的事件进行建模,因此我们的Rider session状态机需要具有弹性,以应对预期之外的事件。例如,乘客可能会在提出要求后取消行程,或者司机的车可能会抛锚或陷入紧急交通状况,迫使司机取消行程。我们通过允许从Request Ride状态转换回Shopping状态来模拟这些场景。

把我们会话生命周期中的所有相关事件放在一个地方可以解锁各种各样的用例,例如:

  • 我们的需求建模团队可以比较应用程序的印象,有多少人打开了应用程序,与实时会话数据,有助于了解骑手在应用程序中查看特定产品后订购该产品的可能性。
  • 我们的预测团队可以看到在特定的时间窗口内给定区域内有多少次处于购物状态,使用这些信息来预测该区域的需求,从而帮助司机了解未来他们最有可能在哪里搭载乘客。

生产中的会话

我们使用火花流在生产环境中实现Rider Session状态机,因为:

  1. 我们的许多提取、转换和加载(ETL)管道都是在Spark上构建的,例如Samza优步之前选择的流媒体平台,对基于状态的流媒体应用(如会话化)没有足够的支持。
  2. 流的火花mapWithState有状态流应用程序的函数被证明是非常通用的,例如提供自动状态过期处理。

ETL管道运行一个一分钟的微批处理窗口,每天处理数十亿个事件。管道在我们的纱集群使用64个8 GB内存的单核容器。输出以状态转换的形式出现,其中包含相关的压缩原始事件数据。输出被发布到Gairos我们内部的地理空间时间序列数据系统。

经验教训

虽然我们的骑手会话状态机在理论上看起来很简单,但将其应用到Uber的用例中却完全是另一回事。以下是我们在将这种新方法应用到现有数据流时所学到的一些关键经验:

  1. 时钟同步:考虑到各种各样的手机和移动操作系统,更不用说用户设置,您永远不能真正相信从移动客户端发送的时间戳。在我们的生产数据中,我们看到时钟漂移从几秒钟到几年。为了解决这个问题,我们决定使用卡夫卡时间戳,即Kafka收到日志消息的时间。然而,我们的移动客户端缓冲了多个日志消息,并将它们发送到一个有效负载中,因此许多消息显示相同的Kafka时间戳。我们最终使用Kafka时间戳和每个消息的事件时间戳进行二次排序。
  2. 检查点健壮性:基于状态的流作业需要将状态定期检查到已复制的文件系统(如HDFS)。该文件系统的延迟可能直接影响作业的性能,特别是当它经常检查点时。单个检查点失败可能导致灾难性的失败,例如整个管道崩溃。
  3. 检查点恢复和回填:任何分布式系统,尤其是那些被设计为在生产中全天候运行的系统,在某些时候必然会失败;例如,节点将会消失,容器可能会被YARN抢占,或者上游系统故障可能会影响下游作业。因此,计划检查点恢复和回填是至关重要的。Spark Streaming用于检查点恢复的默认行为是在尝试从检查点恢复时,在单个批处理中消耗所有积压的事件。我们发现,在工作失败和恢复之间的时间非常长的情况下,这给我们的系统带来了巨大的压力。我们最终修改了DirectKafkaInputDStream能够在检查点恢复时将积压的事件分成适当的批次。
  4. 背压和速率限制:Kafka主题的输入速率从来都不是恒定的。例如,在优步平台上,通勤时间和周末晚上的活动通常会增加。反压力对于减轻不堪重负的工作压力至关重要。当批处理所花费的总时间超过微批处理窗口持续时间时,Spark Streaming的反压力开始生效。它使用PID速率估计器控制后续批次的投入率。我们注意到,在反压期间,估计器的内置默认参数产生了剧烈的振荡和人为的低输入速率,影响了数据的新鲜度。在速率估计器中引入合理的下限,可以更快地从背压中恢复。
  5. 移动日志保真度:移动客户端发送的事件在保真度上有很大差异。在低带宽或弱信号的地方,消息经常丢失或重试并多次发送。客户端可能会因为会话中的低功耗而脱机,所以状态机应该考虑到这一点。我们意识到,监听相关后端系统生成的其他事件流有助于确定我们是否有来自移动客户端的有损数据。经验表明服务器端系统有必要维护它们自己的事件流。

前进

事件顺序处理是一项艰巨的挑战。虽然Spark 2.2中的结构化流原语在处理乱序事件方面看起来很有前途,但我们正在考虑转移到Flink由于它对开箱即用事件时间处理和更广泛的支持在乳房。此外,我们的一些用例可以对会话数据使用粒度,这使得Spark的微批量不可行,这是另一个有利于Flink的点。

如果你对构建用于大规模处理数据的系统感兴趣,可以访问Uber职业页面

评论