新 Uber 司机端是如何克服网络延迟问题

1,759 阅读8分钟

Carbon: Optimistic Mode article feature image
Carbon: Optimistic Mode article feature image

本文是 Uber 的客户端工程师团队讲述了如何开发最新版本司机端系列文章中的第三篇,该系列代号 Carbon ,是我们共享出行业务的核心。包括其它功能在内,Uber 司机端使得超过 300 万名司机可以查看费用、里程以及收益情况。2017 年我们结合司机的反馈开始对司机端进行重新设计,并在 2018 年 9 月份启动了该项目。

城市建筑和无线数据技术的竞争意味着在城市中存在一些手机没有信号的黑色区域。这种黑色区域景区更为常见,导致网络质量和阻塞程度频繁的变化。这些问题尤其影响着那些接送乘客的司机们。

可以举一个合适的例子来说明这种问题。假设一个司机到达了非常拥挤的班加罗尔机场终点。乘客想支付现金,司机需要在应用里面操作完成订单来查看最终的金额。把车停在路边,司机端却无法联网。乘客匆忙赶飞机,不能联网就意味着司机就不能结束行程并查看最终的金额。司机可能会继续开下去,增加了额外的时间,也可能增加了行程花费,给司机和乘客都带来了不便。

为了处理这种网络覆盖漏洞和预防这类事件的发生,我们提出了 —— 乐观模式。新版本的司机端可以离线操作,这样司机就可以在没有网络的情况下用最后一次服务端的预估数据来结束行程。乐观模式下司机端可以任何网络下正常工作,极大的提高了司机和乘客的体验。

乐观模式组件

我们之前的司机端版本中支持一些离线能力来收集失败的请求,一旦网络恢复就会上传到服务器进行整理。虽然这种功能有助于预防一些显示错误,但是不能智能的更新应用状态,不能将多个功能堆积在一起,也不能夸会话持久化状态。我们为新版本的司机端开发了下面这个组件来处理这些问题。

乐观请求

司机端的任何组件都可以通过提交一个乐观请求来开始流转。一个乐观请求能够序列化储存到磁盘,对于一个普通的网络请求来说占用的内存非常小,并且每一个乐观请求都对应一个乐观转换。

乐观转换

乐观模式的核心是转换,换句话说,操作转换一个对象从当前状态到乐观状态,也就是,从服务返回的预期结果。转换还能够堆积,一个对象可以经过多次转换。举一个例子来理解下转换:想象一个类Counter有一个属性count。我们可以实现一个转换来增加count属性的值。

Carbon: Optimistic Mode article figure 1

图一:在这个简单的例子中,Counter对象每经过一次增加转换,count属性值就会增加一。

根据业务需求转换既可以是简单的也可以是复杂的。每一个乐观请求都关联一个转换,转换会根据乐观请求返回一个最终的_乐观状态_。当数据从服务端返回时用户是无感知的,这种方式提供了一种平滑的过渡方案。

当客户端提交一个乐观请求时,关联在请求上的转换就会立马生效,应用进入乐观状态,从而完成请求。乐观状态会一直被保持直到收到服务端的真实状态,然后同步应用和服务端。

Carbon: Optimistic Mode article figure 2a

图 2-1: 普通的计数请求失败

Carbon: Optimistic Mode article figure 2b

图 2-2: 在无网络的情况下乐观模式使用转换及时更新数据状态,将来有网络的情况下和服务端进行同步。

乐观流

我们整个应用都在使用 RX streams 传递数据。应用的每个功能都会随着已发布数据流的状态改变作出响应。这种机制使我们能够使用相同的流轻松地将乐观变换应用于对象的最新状态。为了获得乐观状态,我们结合了数据最后的状态和可用的转换。在将数据发布回流并由功能使用之前,数据已经应用了每个转换。随后业务只需简单的根据数据的乐观状态作出响应。

依赖请求

同时也存在一些请求依赖于乐观请求的完成。例如,甚至在后端不知道行程已经开始的情况下发送一个结束行程的请求是不合理的。当我们在等待乐观请求完成的时候,这样的依赖请求将会被放入队列一段时间。如果周期过长,我们会结束这个请求,通知用户网络错误。

设计挑战

我们在这个设计中遇到了一些挑战。我们想要支持多个堆叠的乐观请求,允许在没有网络的情况下完成多个步骤。由于和服务器不同步,我们还需要处理错误地进入乐观状态并且必须回滚到先前状态的情况。确保我们可靠地向司机展示最准确的状态需要进行多次迭代,并持续优化。

兜底转换

乐观模式开启的情况下,应用程序可能会在乐观请求完成之前收到其他的网络数据。

Carbon: Optimistic Mode article figure 3

图 3: 在这个场景中,我们在收到服务器最新的状态之后又进行了乐观转换。

我们继续拿上面用到的计数器的例子来说。应用程序使用增加变换把最终的值变成了 2。然而,这个值还没有和服务端同步。在这期间,收到的其他的网络响应可能还是旧的值 1。乐观模式使用转换更新了这个旧值并且维护这个乐观状态。这就确保了应用程序不会在两种状态之前来回切换,避免给用户产生混乱的体验。

应用重启时如何存活

所有的乐观请求和最新的乐观状态一起被保存在磁盘里,所以它们能够在应用重启的时候得以保留。考虑这么一种情况,一些请求正在排队和服务器同步,但是用户却杀死了应用。在重新启动的时候,乐观请求和状态会从磁盘中加载。这允许用户在重新启动应用时处于相同的状态。乐观请求排队和服务器同步。

显示错误

我们遇到的这个新功能的一个特殊问题是它如何显示错误。乐观模式的请求只应该由于后端中断而失败,并且结果应该是可预测的便于模拟。然而,实践中会出现错误。由于我们使用乐观的流程服务用户,所以一个小错就可能带来很不好的体验。首先,应用程序的状态回滚到之前的乐观状态,不是用户所期望的状态,下个动作可能不太明显。其次,即使之前的状态可能已经无效了,我们也需要用它来接收错误原因来展示。为了处理这些问题,我们在司机端里加入了一个全局处理错误信息的框架,它可以调用内部弹窗框架。

请求出错的情况总是很少见的。对于经常发生的错误,比如行程太短,我们在手机端上实现了检查,以便更好的处理。

节省时间

对司机来说,我们在开始和结束行程上利用乐观模式节省了大量的时间。我们经常可以看到在真实的网络请求完成之前行程已经开始几分钟的情况。截至 2018 年 11 月,我们注意到平均每个乐观操作节省大约 13.5 秒的时间。即使在新司机端的早期阶段,我们每天累计节省司机的时间也超过了一年。

乐观模式的发展

在无网络的情况下能够正常运行的能力在 Ubers 的其他应用程序上面使用的也非常好。设计之初是为了加速开始和结束行程的速度,它还被整合到 Uber Eats 中功能中,当使用现金结算时,可以更快的结束。它还能用到类似于这种可以快速响应后续同步到服务端的业务中,比如对乘客或司机的评价,标记消息已读,和收集交付的指纹。

Uber 司机端系列文章索引

  1. 为什么我们决定重构 Uber 司机端
  2. 使用RIBs重构Uber司机端
  3. 新 Uber 司机端是如何克服网络延迟问题的
  4. Scaling Cash Payments in Uber Eats
  5. How to Ship an App Rewrite Without Risking Your Entire Business
  6. Building a Scalable and Reliable Map Interface for Drivers
  7. Engineering Uber Beacon: Matching Riders and Drivers in 24-bit RGB Colors