Uber的Greenlight Hubs(GLH)在全球拥有超过700个分支机构,为合作车主提供从账户和支付到车辆检查和车主注册等各方面的人工支持。为了给合作车主创造更好的体验并提高客户满意度,Uber的客户优先工程团队开发的内部客户支持系统,是一个通过GLH实现了更加简化和快速的支持申请的解决方案。
客户支持系统包含两个主要功能:为我们的服务专家提供的登记队列系统,以跟踪合作车主进入GLH的情况; 和一个预约系统,让合作车主可以通过Uber合作车主APP安排人工支持的预约。 这些工具自从2017年3月推出以来,已经改善了全球合作车主的的支持服务体验。
向内部解决方案的过渡
随着Uber的发展,我们之前的客户支持技术在为合作车主提供最佳体验上不能很好的扩展。通过开发我们自己的GLH客户支持系统,我们提出了一个既适合我们的可扩展性和定制需求,又改进了现有基础架构以支持新功能的解决方案。
开发我们自己的工具意味着我们可以:
方便获得客户支持需要的信息:我们的登记系统可以让客户支持代表更加方便获得那些解决合作车主关心的问题所需要的相关信息。这种整合有助于减少支持服务的解决时间和改善合作车主使用GLHs的体验。
合作车主交流渠道的聚合:Uber各种支持渠道(包括应用内消息,GLH自身和电话支持)的集中化意味着GLH专家拥有额外的上下文信息,在一个地方统一的解决合作车主的问题。
为合作车主在GLH缩短等待时间:使用我们升级后的系统,合作车主可以通过安排预约来避免在高峰时段发生不必要的等待时间。
为了实现这些目标,为我们的内部客户支持平台开发了两个新工具:登记队列和预约系统。
更加无缝的登记体验
通过在我们的客户支持平台之上设计和实现的实时登记系统,为合作车主提供了更加无缝的支持体验。使用此系统,合作车主会与礼宾人员登记,然后礼宾人员会根据与其帐户关联的电话号码或电子邮件地址找到合作车主的个人资料。
一旦合作车主登记,GLH专家会从该网站的队列中选择他们。合作车主随后会在手机和GLH内的监视器上收到推送消息,告知他们已与专家配对。一旦合作车主与支持站在通知中指定的专家会面,合作车主就会退出登记队列。
我们的实时登记系统还汇总了客户信息,例如过去的旅行和支持信息,使我们的专家能够尽可能有效地解决问题。
图1: 在GLHs, 与专家配对时监控提醒用户
提供实时专家队列
创建这个实时登记解决方案时遇到一些困难。我们面临的一个挑战是在一个专家声明一个合作车主已经得到协助的场景下,防止专家冲突。为了实现这一目标,我们的系统需要提供一个等待支持服务的合作车主的队列(称为我们的GLH站点队列),通过它,专家可以与等待中的合作车主配对,并在合作车主被选中时实时通知他。
由于WebSocket协议支持低延迟的长连接,所以我们利用它通过后端发送队列更新。Go,Uber许多后端服务选择的语言,通过让我们使用管道和协程技术更加方便的将实时更新传输给web客户端。
尽管如此,在使用WebSocket过程中我们遇到了一些有意思的挑战。为了使我们的站点队列能够实时工作,我们决定将特定站点的所有WebSocket连接和队列写入维持在一个固定的主机。这样,当队列中的一个登记或者预约被更新,所有相关的连接客户端也会被更新。 在我们写入并将WebSocket连接到主机之前,使用单个主机处理这些请求需要在应用程序层上进行分片。
我们使用了Ringpop-go,我们的开源可扩展和容错应用层分片用于Go应用,这有助于配置分片密钥,以便具有相同密钥的所有请求都将路由到同一主机。对于我们的分片密钥,我们使用了GLH站点ID,因此在同一个GLH上发生的所有登记都会转到同一主机,并更新相关客户端上的所有站点队列。
图2: 我们的面对面支持体系结构利用拥有特定GLH的主机的前端WebSocket连接。 来自活动数据中心的GLH专家前端和移动客户端的请求通过Ringpop进行分割,并分配给拥有给定GLH的主机。 来自非活动数据中心的请求会重定向到活动数据中心。 与个人支持相关的数据存储在优步内部数据存储的Schemaless中
实现跨数据中心的高可靠性
为确保我们的GLH软件平稳运行,我们需要保证高可用性。为了做到这一点,我们的服务运行在多个数据中心,处理来自全球的请求。如果某个数据中心由于某种不可预知的原因(如中断)宕机,该服务将自行恢复并继续从其他数据中心运行。
鉴于我们使用WebSocket,在多个数据中心中运行该服务带来了一系列困难。 如果数据中心出现故障,我们不得不重新考虑如何正确处理WebSocket。虽然Ringpop分片在跨数据中心运行良好,但由于每次主机离开或进入环时都会发送跨数据中心的请求,因此会增加延迟。
为解决WebSocket降级问题,我们配置了我们的系统,以便每个数据中心都有一个环; 这样,如果具有相同唯一GLH ID的两个请求命中两个不同的数据中心,它只会更新我们承载站点队列的数据中心中的站点队列。 无论请求来自哪个数据中心,我们都会将所有请求转发给固定的数据中心。如果数据中心发生故障,我们会将请求转发给其他数据中心。 我们同时也会将与出现故障的数据中心建立的所有WebSocket连接杀掉,并与新的数据中心重新建立连接。
为了减少在GLH的等待时间并确保我们在高峰时段提供充足的支持,我们推出了一项新功能,让我们的合作车主提前安排GLH预约,只需在UberAPP上轻松点击几下即可。
图3: 我们的面对面支持预约安排流程使合作车主
可以轻松安排我们的Greenlight中心的预约
图4: 当合作伙伴的应用程序到达Greenlight Hub时,合作伙伴会在Uber合作伙伴应用程序中收到签入通知.
尽管合作车主的预约安排很简单,但是幕后还有大量的工作来保证流程尽可能的无缝。例如,GLH管理者可以随时指定有多少专家在其中心工作,以确保他们的团队不会超额预订; 那么当合作车主进入应用程序时,他们只能看到基于专家数量的可用预约数。例如,如果周二早上9点在某个的GLH只有四名专家正在工作,那么该中心的管理者当时可以设置四个预约的能力,从而限制可用预约的数量。
当合作车主安排预约时,他们会出现在GLH的当天预约列表中。当合作车主到达预定的预约时间时,他们可以通过他们的app轻松登记,并通知分配给他们的专家,他们已经到达。构建我们的预约系统包括在后端实施调度系统,在移动设备上添加预约功能,并为我们的GLH管理者开发基于浏览器的日历界面。
建立全球调度系统
受Martin Fowler关于经常性日历事件的论文的启发,我们决定使用核心日历服务构建我们的日程安排系统,具体实现可用的时间间隔(简化为日历间隔),系统将这些时间间隔视为规则来处理这些规范。
在Fowler模型中,这些规则可以由GLH管理者指定和修改,从而允许更灵活的调度。由于调度系统通常有许多需要考虑的边界情况,因此我们逐步构建调度系统以避免范围模糊,并为每一步提供一个功能正常的系统:
我们的第一次迭代使用GLH管理者最初设定的营业时间,并为每个站点指定了全球三名专家的容量,使我们能够慢慢推出测试版本的软件。
我们的第二次迭代使用由GLH管理者设置的日历间隔,允许他们间隔多久设置一次专家池容量。
我们的第三次迭代结合了现有的日历时间间隔,但也允许GLH管理者设置GLH关闭时间(即非营业时间和假日)。
然而,由于Uber的国际影响力,我们很快遇到了时区相关的问题,并且由于系统的各个组件需要协调正在使用哪个时区的环境而加剧了这一问题,例如, GLH时区或合作车主的时区。 另外,我们需要考虑夏令时的变化。 为了解决这些需求,我们采用了以下规则:
与主要后端服务API交互的所有客户端均采用其所选GLH的时区。
所有预约时间在我们的数据库中都会保存为UTC +0时区时间。
主要的后端服务有一个内部层来处理持久层和API层之间的所有时区转换。这使我们能够抽象出日历逻辑并调用与日历相关的内部方法,而无需担心时区问题。
重要的是要注意时区,即UTC偏移,不作为GLH对象的属性存储。 如果是这种情况,那么夏令时改变会导致先前安排的预约时间在任一方向偏移一小时。为了正确处理这个问题,UTC偏移量将根据每个GLH的物理坐标进行动态计算。
时区边缘的情况
在构建我们的调度系统时,我们遇到了一些关于时区的特殊案例。当我们的系统将“日历间隔”转换为当地时区时,出现了一个问题。 由于UTC和当地时间之间的时区变化(取决于相关网站的时区),日期可能不正确。 例如,11月20日5:00 am UTC时间实际上是太平洋标准时间11月19日的下午9:00。因此,重要的是我们不要对相关时间段的日期做出假设,并且在时区跨越多天时进行测试。
另外,当我们将GLH营业时间从UTC时间转换为当地时间时,我们遇到了类似的时区问题。 我们在当地时间节省了我们的工作时间,因为没有日期,我们没有足够的上下文让我们将其用UTC保存。 例如,一个GLH在星期一从上午9点到下午9点可能导致UTC营业时间从周一下午5:00开始周二早上5点结束。 由于没有日期,不清楚这些当地时间提到的小时是一周中的哪天。 因此,每当创建新的日历时间间隔,我们都必须将开放时间从已存储的本地时间转换成UTC时间。 根据业务逻辑所在的位置,这些场景可能需要在Web和移动客户端以及服务器端进行广泛的测试。
对于合作车主实际使用我们的调度系统,我们需要为移动设备构建新的UX。 这涉及到修改支持表单屏幕给合作伙伴除提交按钮之外的选项以获得帮助,以及帮助主屏幕显示他们可能会有的任何即将到来的预约。
还有一些与特定活动相关的新屏幕:选择附近的GLH预约会面,根据该会场的可用选项选择预约的特定日期和时间,确认选择以创建预约,查看详细信息 预订预约并取消预订,并查看有关该网站的详细信息,例如地址。
由于我们与日期和时间打交道,并且因为我们希望我们的服务器API返回结构化的数据(例如ISO 8601),而不是预先格式化的本地化字符串(即用户的偏好语言中的日期)供我们展示,所以我们假设将使用java.util.Date标准。在这个标准中,Date和相应的日历类在处理时区时有许多已知的问题,所以我们想要探索一下其他选项是否能工作的更好。 例如,Joda-Time标准(一种Java 8 API)听起来很有趣,但它还不兼容Android系统这种广泛用于合作车主的设备的系统。
我们最终发现了ThreeTenBP-- Joda-Time的后继者,它将Java 8的时间和日期API引入Java 6和7。然而,以前在Android上使用ThreeTenBP的尝试遭遇启动问题。在启动时,这些库从磁盘加载时区数据库信息,对其进行分析并将其注册到库中以供稍后使用。这个库的特定于Android的包装器以更友好的方式加载数据,但仍然存在阻碍应用程序启动的非平凡磁盘操作。在低至中档设备上进行测试时,这会使Uber合作伙伴应用程序的启动速度减慢超过200毫秒。
我们尝试以多种方式优化ThreeTenBP,例如,通过在不同的线程上执行实际的磁盘操作,以便Application.onCreate的其余部分可以并行发生,并在最后加入线程,从而确保Uber合作伙伴应用程序可以安全地使用 库。 我们也尝试使用其他类似的库,它们在启动时尝试少做或没有IO,但是不能将启动时间降低到合理的延迟。
我们尝试使用方法分析器,让我们惊讶的是,通过解析代码,我们看到在启动过程中大量时间花费在常见的字符串方法中像string.split。根据我们阅读源码,甚至是来自Application.onCreate的步调试器,似乎都没有发生这种情况。 在探查器中,重量级操作汇总到ZoneRulesProvider类中的静态初始化程序中,其中(理论上)懒惰时区数据库提供程序代码正在注册。 由于这个类正在被加载进行注册,即使被注册的对象完全是懒惰的,并且在注册时没有执行任何I / O,也会运行静态初始化块以试图从ServiceLoader / META加载时区数据库META-INF。这是Java服务器中典型的模式,而不是Android。它用了我们避免使用的同样资源下载,由于它在Android上的性能很差。
我们最终修改了ThreeTenBP本身,以便可以轻松重写此静态初始化块的行为。默认实现将保持不变,但会被抽象化在新的ZoneRulesInitializer类后。Android应用程序或库将能够提供自己的实现,以便在库的第一次使用时通过Android资产加载时区数据库。
我们更新了另一个面向Android的ThreeTenBP封装器lazythreetenbp,以利用这个新接口,相当于ThreeTenABP有待更新。使用这个库的启动延迟是零,导致低延迟。但是,在静态模块初始化中有时区数据库的加载的发生,意味着在需要时区数据之前不需要进行任何操作,这在典型的用户会话期间甚至可能不会发生。(Uber App非常大,很少有功能需要操纵日期和时间会用到时区)。
图5: GLH管理者的日历UI指定在任何给定时间段内,特定站点上有多少专家可用。
我们还为GLH管理者构建了一个日历应用程序,可以轻松灵活地配置其网站的营业时间,可用的预约时间以及任意给定小时内的可用专家池。可用时间只能在营业时间内创建。日历中的休息时间变灰。日历还显示当前已安排的预约。
在日历的周视图中,网站管理员可以从开始时间拖放到结束时间以创建可用时间。此外,他们还可以在移动应用程序中添加假期和午餐时间等关闭项目,从而防止网站管理员在现场关闭期间意外增加可用时间。
为了设计这个接口,我们使用Node.js,React / Redux,Styletron进行内联样式,ES2017(ES8)用于JavaScript,Lerna用于存储monorepo的可重用组件,以及其他一些Uber类库/框架,如Bedrock和Superfine。设计能够提供卓越用户体验的日历功能非常复杂,因此创建一个并保持高性能是一项重大挑战。然而,我们并不想妥协我们的简单,可读和可扩展的代码库。另外,我们希望创建一些可重用的React组件,以适应将使用这些组件的其他前端项目。
在我们的软件测试版中,每当日历被拖动时,日历中的许多元素都会被重新渲染。因此,小时范围是动态显示的,即使大多数这些元素没有视觉更新。由于渲染日历中的许多DOM元素,我们通过调整shouldComponentUpdate()生命周期方法来减少需要渲染的元素数量,从而利用React的虚拟DOM。
然后,我们通过使用react-dnd的拖动源来检查日历中的元素是否在开始时间和结束时间的范围内,并仅重新呈现那些元素。另外,我们使得闭包和可用时间的DOM元素不可更新,因为它们不允许重叠,略微提高了性能。结果,在拖放过程中由更新引起的200毫秒延迟减少,使其接近于0。
由于日历应用程序对服务器进行了大量调用,并且包含许多性能调整,所以自开始以来,代码复杂度显着增加。为了保持代码清洁和简单,我们将代码抽取到可重用组件和HOC以及一些环境设置中,并将其转换为前端monorepo。我们将Lerna用于monorepo并发布软件包。通过使用monorepo,几个软件包被存储在一个回购站中,这样可以节省引导新项目的时间,并且可以一次更新多个组件,从而更容易添加跨组件功能或修复错误。另外,为了增强React组件的可重用性,我们使用Styletron代替CSS来进行内联样式。这确保了其他开发人员不需要自己添加CSS,从而避免考虑样式冲突,因为所有样式都直接应用于JavaScript代码中。
开发此产品有助于提高合作车主在GLH上的体验,从而提高客户满意度。迁移到新系统已经将等待时间平均缩短了15%以上,并且一旦与客户支持专家匹配,问题解决时间减少了25%。最重要的是,这些新功能让那些在GLH安排预约的合作车主几乎不需要等待时间。
这只是为我们来自全球的合作车主和客户支持专家准备的众多产品的一小部分。我们一直持续在探索新技术以改善我们用户的GLH体验,从改进我们的分析到合作车主提交申请前主动提供支持服务。
相关阅读:
高可用架构
改变互联网的构建方式
长按二维码 关注「高可用架构」公众号