需求
范围内
- 乘客应该能够查看附近位置的可用司机,如下图所示。
图: 显示附近的汽车位置
-
乘客应该能够请求从出发地到目的地的接送。
-
附近的司机会收到乘车通知,其中一名司机会确认乘车
-
接送时间将在车辆被派去接乘客时显示给客户。
-
行程完成后,乘客会在乘车收据中看到行程地图、价格等详细信息。
超出范围
- 自动匹配司机和乘客的方式,在调度车辆时考虑到新司机和乘客等因素
体系结构
为了设计这个系统,必须定义一次旅行的生命周期,如下图所示。
图: 旅行的生命周期
我们可以构建以下组件来支持这个行程生命周期。这些组件将协同工作,以满足构建网约车系统的要求。
司机位置管理器: 该组件将负责维护不断变化的司机位置。它将采集司机的 uber 应用发出的包含他们当前位置的消息,并更新汽车位置索引。 行程调度: 它将负责处理来自用户的行程请求,并调度一名司机响应这些请求。 到达时间计算器: 该组件将负责计算司机接受乘车后到达乘客的时间。 行程记录器: 在行程进行中,它将记录骑行者发送的 GPS 信号。然后,这些 GPS 信号将被记录在数据存储中,该数据存储将被后续的系统使用,如地图匹配器和定价计算器。 地图匹配器: 该组件将负责使用专门用于此目的的算法在实际地图上匹配行程。 价格计算器: 它将使用记录的行程信息来计算用户必须为行程支付的价格。
上面提到的组件可以分为三大类: 出行前组件、出行中组件和数据存储。我们已经在下图中展示了每一个组件。
图: 网约车系统中的高层组件
高层设计
出行前组件
该组件将支持查看附近位置的车辆并根据用户请求调度出行的功能。汽车的位置可以存储在内存中的汽车位置索引中 (后面一节会解释),该索引可以支持高读写请求。司机 app 会持续发送更新后的汽车位置,这些位置会在这个索引中更新。当 rider app 请求附近的车辆位置或发出乘车请求时,该索引将被查询。导致乘车调度的操作序列如下图所示。
图: 行程前组件中的操作顺序
出行开始前有三个操作序列,如上图所示:i) 更新驾驶员位置 ii) 列出附近的驾驶员 iii) 请求搭车。第一个操作序列涉及到更新驾驶员位置,因为驾驶员不断改变他们的位置。第二个操作序列包括获取附近司机列表的步骤。第三个序列包括请求和调度搭车的一系列步骤。
汽车位置索引
我们将在分布式索引中维护驾驶员位置,该索引分布在多个节点上。包含他们的位置、车内人数、是否在旅行的司机信息被聚合为对象。这些驾驶员对象在复制的同时,通过一些分片机制 (例如城市、产品等) 分布在各个节点上。
图: 分布式车位置索引
每个节点维护一个 drivers 对象的列表,可以实时查询,获取附近司机的列表。另一方面,当司机应用程序发送有关其位置或任何其他相关信息的更新时,这些对象也会更新。下面列出了这个索引的一些有趣的特征。
- 它应该存储所有司机的当前位置,并且应该支持按位置和属性 (如司机的行程状态) 进行快速查询。
- 它应该支持高容量的读写。
- 存储在这些索引中的数据将是临时的,我们不需要长期的持久存储。
图: 更新查询汽车位置索引
ETA 计算器
我们需要考虑到路线、交通和天气等因素,在用户的车辆被调度后,向用户显示接车时间。在给定道路网络上的起点和终点的情况下,估计 ETA 的两个主要部分包括 i) 计算从起点到终点的最少成本的路线 (这是时间和距离的函数) ii) 估计穿越路线所需的时间。
图: 样品 ETA 展示给客户
我们可以从用图形表示法表示物理地图开始,以方便路线计算。我们通过节点对每个路口进行建模,通过有向边对每个路段进行建模来实现这一点。我们已经在下图中展示了物理地图的图形表示。
图: 物理地图的图形表示
我们可以使用 Dijkstra 算法等路由算法来寻找源和目的地之间的最短路径。然而,与使用 Dijkstra 算法相关的警告是,为“N”个节点找到最短路径的时间是 O(NlgN),这使得这种方法对于这些叫车平台的功能规模来说是不可行的。我们可以通过分割整个图来解决这个问题,预先计算分区内的最佳路径,并仅与分区的边界进行交互。在下图中,我们展示了这种方法可以帮助将时间复杂度从 O(NlgN) 显著降低到 O(N ' lgn ') 的方式,其中 N ' 是 N 的平方根。
图: 寻找最优路线的分区算法
一旦我们意识到最优路线,我们就会通过考虑交通流量来找到穿越路段的估计时间,交通流量是时间、天气和现实生活事件的函数。我们使用这些交通信息来填充图上的边权重,如下图所示。
图: 利用交通信息确定 ETA
行程中的组件
这个组件处理从旅行开始到完成所涉及的过程。当旅程进行时,它会跟踪旅程的位置。它还负责生成地图路线 (如下图所示),并在完成时计算行程成本。
在下图中,我们展示了旅行进行和完成时所涉及的操作顺序。当乘车过程中,司机应用程序会发布 GPS 位置信息,这些信息会通过 Kafka 流被记录下来,并持久化到位置存储中。行程完成后,Map Matcher 使用原始 GPS 位置生成地图路线。
图: 行程中的组件的顺序图
位置存储/数据模型
这个存储组件将负责存储乘客/司机应用程序在旅途中发送的 GPS 位置流。下面列出了这个商店的一些特征。
- 它应该支持高容量的写入。
- 存储应该是持久的。
- 它应该支持基于时间序列的查询,例如在给定的时间范围内驱动程序的位置。
我们可以使用像 Cassandra 这样的数据存储来持久化驱动程序的位置,从而确保长期的持久性。我们可以使用组合作为分区键,将驱动程序位置存储在数据存储中。这确保了驱动程序位置是按时间戳排序的,并且可以支持基于时间的查询。我们可以将位置点作为序列化消息存储在数据存储中。
地图匹配器
该模块将原始 GPS 信号作为输入,并输出路网上的位置。输入的 GPS 信号包括纬度、经度、速度和航向。另一方面,路网上的位置由纬度 (在实际道路上)、经度 (在实际道路上)、路段 id、道路名称和方向/航向组成。这本身就是一个有趣的挑战,因为原始 GPS 信号可能会远离实际的路网。下图中很少有这样的例子,左上角的盒子对应的是由高楼大厦造成的城市峡谷情况,右下角的盒子是由于 GPS 信号稀疏造成的。map Matcher 生成的地图路线用于两个主要用例:i) 寻找司机在路网上的实际位置 ii) 计算车费价格。
图: 地图匹配的挑战: 城市峡谷,稀疏的 GPS 信号
为了解决地图匹配问题,我们需要确定汽车在旅途中会走的路线。我们可以使用隐马尔可夫模型 (HMM) 的方法来找到全局最优路线 (路段集合; 即 R1, R2, R3,…),根据观测到的 GPS 信号 (O1, O2, O3,…),汽车会走哪条路。在下图中,我们展示了一个路段和 GPS 信号的样本表示。
图:GPS 信号和路段样本
之后,利用发射概率 (从一个路段观察到 GPS 信号的概率) 和转移概率 (从一个路段过渡到另一个路段的概率),用隐马尔可夫模型 (HMM) 来表示 GPS 信号和路段。我们通过使用 HMM 将道路段建模为隐藏状态,将 GPS 信号建模为观测值,在图像中解释了这一场景。
图: 用 HMM 表示 GPS 信号和路段
然后,我们可以在 HMM 上应用基于动态规划的 Viterbi 算法,以找到给定一组观察值的隐藏状态。我们已经展示了 Viterbi 算法在本例中讨论的 HMM 上运行后的输出。
图: Viterbi 算法在上面提到的 HMM 上运行后的输出
优化
所提出的设计有一个问题,就是繁重的读和写都是指向数据存储的。我们可以通过在分布式缓存 (Redis) 中缓存原始驱动程序位置,从而将读流量与写分离,从而优化系统。MapMatcher 利用 Redis 缓存中的原始驱动位置来创建匹配的 map,并将其持久化到数据存储中。
图: 优化将 Cassandra 的读取卸载到 Redis 缓存
我们还可以通过在上面提到的 ETA 计算机制之上应用机器学习来改进预测的 ETA。应用机器学习的第一个挑战涉及 特征工程 提出区域、时间、行程类型和驾驶员行为等特征来预测现实的 ETAs。另一个有趣的挑战是选择可以用于预测 ETAs 的机器学习模型。对于这个用例,我们将依赖于使用 非线性(例如 ETA 与一天的小时数不线性相关) 和 非参数(不同特征与 ETA 之间的交互没有先验信息) 模型,如随机森林,神经网络和 KNN 学习。当 ETA 预测关闭时,我们可以通过监测客户问题来进一步改进 ETA 预测。
有趣的事实: 在这次 演讲 中,数据科学家 Sreeta Gorripaty @ Uber 讨论了 ETA 误差 (实际 ETA - 预测 ETA) 和区域之间的相关性。作为 特征工程 的一部分,ETA 误差的概率密度函数 (PDF) 如下图所示。从图中可以看出,与北美相比,印度的分布尾部较厚。这表明印度的 ETA 预测更差,使得区域成为训练用于 ETA 预测的机器学习模型的一个重要特征
解决瓶颈
在系统中,每个组件由不同的微服务组成,每个微服务执行一个单独的任务。我们将有单独的 web 服务,用于司机位置管理,地图匹配,ETA 计算等。我们可以通过配备备用机制来应对服务故障,从而使系统更具弹性。一些常见的备用机制包括使用最新缓存的数据,甚至在某些情况下使用备用网络服务。混沌工程是一种用于弹性测试的常用方法,在系统中注入故障/中断,并对其性能进行监控,以做出改进,使服务对故障具有更强的弹性。
在下图中,我们展示了 Chaos 平台的架构,该平台可用于在系统中引入中断/故障,并在仪表板上监控这些故障。这些故障是由开发人员通过在文件中传递中断配置的命令行参数来引入的。Chaos 平台中的工作环境触发主机中的代理使用作为输入传递的配置来创建故障。这些工作程序通过流 RPC 维护中断日志和事件,并将这些信息持久化到数据库中。
为了进行弹性测试,我们可以引入两种类型的中断。第一种故障被称为进程级故障,如 SIGKILL、SIGINT、SIGTERM、CPU 节流和内存限制。第二类故障是网络级故障,如入站网络调用的延迟、对外部依赖项的出站调用的丢包以及对数据库的阻塞读/写。我们已经在下图中展示了所有不同类型的网络故障。
扩展要求
在上面的设计中,司机会被告知附近有乘客,他们会接受他们想要的乘车。作为现有实现的延伸,面试官可能会要求扩展系统,以实现司机与乘客的自动匹配。司机会根据司机和乘客之间的距离、司机移动的方向、交通状况等因素与乘客进行匹配。我们可以将天真的匹配骑手的方法应用到最近的司机身上。然而,这种方法并不能解决所谓的 trip_upgrade 的问题,即当一名乘客与一名司机匹配时,同时另一名司机和另一名乘客进入系统。trip_upgrade 的场景如下图所示。
图: 行程升级 - 骑手与驾驶员配对
图: 行程升级——额外的驾驶员和骑手进入系统
有趣的事实: Uber 地图高级数据科学经理 Dawn Woodard 在她在微软研究院的 演讲 中谈到了匹配@Uber 背后的科学。Dawn 谈到了网约车中匹配和动态定价的不同方法,并讨论了用于预测这些算法的关键输入的机器学习和统计方法: 路网中的需求、供应和旅行时间。
参考文献
- Backend of Requesting Ride(Arka) www.youtube.com/watch?v=Azp…
- Mapping(Kiran) www.youtube.com/watch?v=Cht…
- ETA Calculation: www.youtube.com/watch?v=FEe…
- Uber Payments: www.youtube.com/watch?v=Dz6…
- Life of a Trip: youtu.be/GyS3m5SyRuQ…