优步的实时推送平台

优步的实时推送平台

优步构建多边市场,每天在全球每天处理数百万次旅行。我们努力为所有用户构建实时体验。

实时市场的本质使得它们非常活跃。在旅行过程中,有多个参与者可以修改和查看正在进行的旅行的状态,并需要实时更新。这就需要让所有活跃的参与者和应用程序与实时信息保持同步,无论是接送时间、到达时间、屏幕上的线路,还是当你打开应用程序时附近的司机。

增长的一个关键方面是关键用户屏幕中的特色爆炸性,并且对公司的开发人员需要在共享应用屏幕​​上以分散的方式构建实时移动功能。

本文描述了我们如何从轮询更新应用到基于grpc的双向流协议来构建我们的应用体验。

调查更新

优步之旅是参与实体之间的跑步者,如骑手和司机在物理世界中移动。随着旅行进展,这两个实体需要与后端系统和彼此进行更新。

考虑一个方案,river要求骑行,驱动程序在线提供服务。Uber在后端中的匹配系统标识匹配并为驱动程序提供跳闸设备。现在每个人(骑手,驱动程序,后端)都应该与彼此同步。

驱动程序应用程序可以每隔几秒钟轮询服务器,以检查是否提供了新的报价。骑手应用程序可以每隔几秒钟轮询服务器以检查驱动程序是否已分配驱动程序。

这些应用程序的轮询频率取决于他们在轮询的数据的变化率。在像Uber应用程序这样的大型应用程序中,变化率的变化非常广泛,范围从秒到几小时。

从移动应用程序投票的问题

在某些时候,对后端API网关的80%请求正在轮询呼叫。

激进的轮询使应用程序保持响应,但导致更大的服务器资源利用率。轮询频率的任何错误都会导致重要的后端负载和劣化。随着具有实时动态数据需求的功能的数量,这种方法是不可行的,因为它将继续增加后端的大量负载。

轮询导致电池流失更快,应用缓慢和网络级拥塞。这在具有2G / 3G网络或Spotty网络的地方尤其如其明显的城市,其中应用程序对每个轮询尝试多次重试。

随着功能的数量,开发人员试图过载现有轮询API或创建一个新的轮询。在它的高峰时,该应用程序正在轮询了数十个API。每个API都以多种功能重载。这些轮询API最终刚刚成为应用程序的一组有效载荷分布的API,以便轮询其功能。维持API水平对关注的一致性和逻辑分离仍然是一个日益增长的挑战。

冷启动应用程序是轮询策略中最具挑战性的场景。每次打开应用时,所有的功能都想从后端拉出最新的状态来呈现UI。这会导致多个竞争的并发API调用,直到从服务器检索到关键组件,应用程序才能呈现。如果没有优先级,因为所有api都有一些关键信息,所以应用加载时间会持续增加。恶劣的网络条件将进一步加剧冷启动问题。

很明显,我们需要完全改造我们在市场上的各个参与者中如何同步状态。我们开始在一个旅程中建立一个推送消息平台,并提供服务器按需将数据发送到应用程序。

当我们采用这种体系结构时,我们发现了显著的效率改进,但也解决了不同的问题和挑战。在下一节中,我们将描述push基础架构的不同一代,以及这个平台是如何发展的。

取消投票,引入RAMEN

在使用推送消息时是消除投票的自然选择,有很多关于如何建造它的考虑因素。四个主要的设计原则如下。

更容易从轮询迁移到推送

有许多现有的轮询端点为业务提供了动力。新系统必须利用现有轮询api中的有效负载构建业务逻辑,而不需要全部重写它们。

易于发展

开发人员不应该做出巨大不同的事情,以推动数据相对于他们努力开发投票API。

可靠性

应在网络上可靠地发送所有消息,并在交货失败时重试。

电线效率

随着优步在发展中国家的快速发展,数据使用成本对我们的用户来说是一个挑战,对于每天连接该平台几个小时的司机来说尤其如此。该协议必须最小化服务器和移动应用程序之间的数据传输量。

我们将新系统命名为RAMEN (R.ealtime一种同步ssagingNetwork)。

图1:整个系统的高级架构。

决定生成消息

在任何给定时刻,数百名乘客、司机、餐厅、食客和旅程的实时信息都在变化。消息的生命周期从决定“何时”为用户生成消息有效负载开始。

Fireball是一个负责解决“何时推送消息?”问题的微服务。决策的很大一部分被捕获为配置。它侦听整个系统中发生的各种类型的事件,并确定所涉及的用户是否需要推送。

例如,当一个司机“接受”一个提议时,司机和行程实体状态就会改变。此更改将触发Fireball服务。然后,根据配置,Fireball决定应该将哪种类型的推送消息发送给所涉及的市场参与者。通常,一个触发器可以保证向多个用户发送多个消息有效负载。

触发器可以是应该生成推送有效载荷的任何类型的重要事件。例如,如请求乘车,应用程序打开的用户动作,在固定间隔上滴定,消息总线上的后端业务事件或地理出口/入口事件。

所有此类触发器都被过滤并转换为对各种API网关端点的调用。API GetAway需要用户上下文信息,如设备语言环境,设备操​​作系统和应用程序版本,以生成适当的本地化响应有效载荷。火球获取设备上下文拉面服务器,并在将API网关调用时将其添加到标题中。

生成消息有效载荷

源自优步应用程序的所有服务器呼叫都由我们的API网关提供(阅读更多关于网关的演进的更多信息这里)。推送有效载荷以同样的方式生成。

API Gateway负责确定一旦火球确定谁和何时推送消息的“推动什么”。网关调用各种域服务以生成正确的推送有效载荷。

网关中的所有api在如何生成有效负载方面在解剖学上是相似的。但是,这些api被分为拉式和推式api。拉开apis.是从移动设备调用的端点,以执行任何HTTP操作。推动api都是从Fireball调用的端点,并且有一个额外的“Push”中间件,可以拦截pull API的响应并将其转发给Push消息传递系统。

将API Gateway介于两者之间有优点。

  • 拉和推api共享端点的大部分业务逻辑。给定的有效负载可以从拉API无缝切换到推API。例如,无论你的应用是通过pull API调用拉出一个“用户”对象,还是Fireball通过push API调用发送一个“用户”对象,都使用相同的逻辑。
  • 网关处理许多横切问题,如推消息的速率限制、路由和模式验证。

火球和网关都一起生成要在适当的时间发送给用户的推送消息。将其提供给移动设备是“推送消息传递系统”的责任。

推送消息有效载荷的元数据

每个推送消息都有各种用于优化的配置。

优先事项

由于不同的用例产生了数百种不同的有效载荷,所以需要优先考虑最先发送给应用的内容。在下一节中我们将看到,我们采用的协议限制了在单个连接上发送多个并发负载。此外,接收设备的带宽是有限的。

要带来相对优先级的感觉,邮件大致分为三个不同的优先级,并对影响的影响:

  • 高:对于核心用户体验很重要
  • 介质:增量用户体验功能消息
  • 低:高数据有效载荷大小或低频非关键消息

然后使用此优先级配置来管理平台的各种行为。例如,当建立连接时,将消息放在套接字中以优先级的降序。在RPC故障的情况下,服务器侧重试,高优先级消息更加可靠,并且对跨区域复制有支持。

是时候生活了

推送消息是为了改善实时体验。因此,每个消息都有一个定义的生存时间值,范围从几秒到30分钟不等。消息传递系统将持久化消息并重试传递消息,直到活值时间到期。

重复数据删除

此配置确定是否应该在通过各种触发或重试多次生成相同的消息类型时应重复重复使用推送消息。对于我们的大部分用例,发送给定类型的最新推送消息足以满足用户体验,这允许我们降低整体数据传输速率。

消息传递

推送消息系统的最后一个组件是实际的有效负载交付服务。该服务保持着与世界各地数以百万计的移动应用程序的活跃连接,并在它们到达后立即将信息有效载荷联合起来。

世界各地的移动网络提供不同级别的可靠性,因此传输系统需要健壮,以适应故障。我们的系统提供了“至少一次”的交付保证。

拉面递送方案

提供可靠的交付通道,我们必须利用基于TCP的持久连接从应用程序到数据中心中的递送服务。对于2015年的应用程序协议,我们的选择是利用HTTP / 1.1,长轮询,Web套接字或最终服务器发送的事件(SSE)。

根据安全性,支持移动SSE等各种考虑,支持移动SSE和二进制大小的影响。它在Uber的已经支持的HTTP + JSON API堆栈上的简单性和可操作性使其成为当时我们的选择。

然而,SSE是一种单向协议,即,数据只能从服务器发送到应用程序。为了在之前提到的至少一旦提到的保证,需要在应用程序协议之上内置于传递协议中的确认和重试。

在SSE之上定义了一个非常优雅和简单的协议方案。

图2:SSE协议的服务器-客户机交互。

客户端开始在第一个HTTP请求上接收消息/拉面/接收吗?seq = 0在任何新会话的开始处序列号为0。服务器响应HTTP 200和' Content-Type: text/event-stream '以维护SSE连接。

作为下一步,服务器以优先级的降序顺序发送所有挂起的消息,并关联增量序列号。由于底层传输协议是TCP连接,因此如果没有传递具有SEQ#3的消息,则连接应该断开,超时或失败。

客户端现在预计将在下次机会中重新连接它已经看到的最大序列号(在这种情况下SEQ = 2)。这告诉服务器即使第3号写入套接字,它也不会被交付。然后,服务器将重新发送与SEQ = 3开头的相同的消息或任何新优先级消息。此协议构建与执行大多数储存的服务器的流式连接的所需可恢复性,并且在客户端执行非常简单。

为了知道连接是否存在,服务器每4秒发送一个字节大小的心跳消息。如果客户端在长达7秒的时间内没有看到心跳或消息,它将假定连接断开并重新连接。

在上述协议中,每当客户端重新连接更高的序列号时,它充当服务器刷新旧消息的确认机制。在一个好的网络上,用户可以保持最多几分钟的连接,导致服务器继续累积旧消息。要缓解此问题,该应用程序将调用/拉面/ ack?seq = n每30秒一次,不管连接质量如何。

协议的简单性允许非常快速地以许多不同的语言和平台编写客户端。

设备上下文存储

拉面服务器每次建立连接时都会存储设备上下文。将该上下文暴露于火球以访问用户的设备上下文。使用用户和设备参数使用唯一散列生成每个设备上下文的ID。即使用户使用多个设备或应用程序使用不同的设置,也允许均衡推送消息。

消息存储

RAMEN服务器将所有消息保存在内存中,并备份到数据库中。如果连接不稳定,服务器可以一直重试发送,直到TTL过期。

实现细节

第一次生成的RAMEN服务器使用Uber的内部一致散列/分片框架在Node.js中编写了名为“铃声”的碎片框架。林蛙是一个分散的分片系统。所有连接都使用用户UUID分片,并使用Redis作为持久化数据存储。

全球扩大拉面

在明年半场半,推动平台在整个公司中看到了巨大的采用。在峰值时,该系统通过维护高达600,000个并发的流式连接,将每秒超过70,000 QPS推送超过70,000 QPS推送消息。该系统快速成为服务器客户端API基础架构中最容纳的部分。

随着流量和持久连接的数量增加,我们的技术选择也需要缩放。基于RINPOP的分布式分布式是一个非常简单的架构,但不会随着环中的节点数量的数量增加而缩放。铃声库使用了一个用于评估会员资格的八卦协议。随着环的尺寸增加,Gossip协议的收敛时间也增加。

此外,Node.js工人是单线程,并且具有升高的事件循环滞后级别,导致成员资格信息的融合中进一步延迟。这些问题可能导致拓扑信息不一致,导致消息丢失,超时和错误。

2017年初,我们决定是时候重新启动RAMEN协议的服务器实现以继续扩展。对于这个迭代,我们使用了以下技术:Netty, Apache Zookeeper, Apache Helix, Redis和Apache Cassandra。

网状的:Netty是一种广泛使用的和高性能库,用于构建网络服务器和客户端。Netty的ByteBuf允许零复制缓冲区使系统非常高效。

Apache zookeeper:具有一致的网络连接散列允许直接流式流,而不需要在其间的任何存储层。但是,除了分散的拓扑管理中,我们选择与Zookeeper集中共享。Zookeeper是一个非常强大的分布式同步和配置管理系统,可以快速检测连接节点的故障。

Apache螺旋:Helix是一种强大的群集管理框架,适用于Zookeeper的顶部,允许定义自定义拓扑和重新平衡算法。它还摘要从业务逻辑的核心摘要拓扑逻辑。它使用ZooKeeper监控连接的工人并传播分片状态信息变化。它还允许我们编写自定义“领导者 - 追随者”拓扑和自定义逐渐重新平衡算法。

Redis&Apache Cassandra:当我们为多区域云架构做好准备时,有必要正确地复制和存储消息。Cassandra是一个持久的跨区域复制存储。Redis被用作Cassandra之上的容量缓存,以避免与部署或故障转移事件中的分片系统相关的大量集群问题。

图3:新Ramen后端服务器的架构。

史门板:此服务在NetTy上实现RAMEN协议,并具有与处理连接,消息和存储相关的所有逻辑。此服务还包括Apache Helix参与者,该参与者与ZooKeeper建立连接并保持心跳。

Streamgatefe(Streamgate Front End):此服务充当Apache Helix Spector,并侦听Zookeeper的拓扑变化。它实现了反向代理。来自客户 - 火球,网关或移动应用的每个请求都使用拓扑信息分离,并路由到正确的Streamgate工作者。

螺旋控制器:顾名思义,这是一个单独负责运行Apache Helix Controller进程的5节点独立服务,是拓扑管理的大脑。无论何时任何Streamgate节点启动或停止,它都会检测到更改并重新分配分片分区。

我们在过去几年中经营这种架构,并且能够实现基础设施的99.99%的服务器端可靠性。通过此推送基础架构的采用仍在继续增长,支持IOS,Android和Web平台上的十多种不同类型的应用程序。我们已经操作了该系统,并发连接超过1.5米,每秒推出超过250,000条消息。

用gRPC推进基础设施的未来

此服务器端基础架构仍然是稳定的。随着我们在具有各种网络条件和应用程序的新城市服务,我们的重点是继续将推送消息传递到移动设备的长尾可靠性。我们始终如一地尝试新的协议和开发方法来弥补差距。在检查差距时,以下领域正在有助于降低可靠性。

丧失致谢

上面定义的拉面协议被优化以减少数据传输,因此仅在重新连接时每30秒或当重新连接时每30秒报告确认。这导致延迟确认,并且在某些情况下未能确认消息传递。这使得很难区分真实的消息损失和确认请求中的失败。

可怜的连接稳定

保持服务器和客户机之间的正常连接至关重要。跨不同平台的客户端实现在处理错误、超时、回退或应用程序生命周期事件(打开或关闭)、网络状态更改、主机名和数据中心故障转移方面有许多细微的差别。这导致了不同版本间性能的可变性。

交通限制

由于协议在SSE上实施,因此数据传输是单向的。许多新的应用体验需要我们启用双向消息传输。没有实时往返时间测量,确定网络条件,转移速度,减少线路阻塞的头部是不可行的。SSE也是一个基于文本的协议,限制了我们在没有像Base64那样的文本编码的情况下传输二进制有效载荷的能力,导致较大的有效载荷大小。

2019年底,我们开始开发下一代拉面议定书,以解决上述缺点。经过很多考虑,我们选择在GRPC之上建立这个。GRPC是一种广泛采用的RPC堆栈,具有许多语言的客户端和服务器的标准化实现。它对许多不同的RPC方法具有一流的支持,并具有与Quic传输层协议的互操作性。

基于新的GRPC的RAMEN协议从以前的基于SSE的协议扩展了一些关键差异:

  • 现已立即在反向流上发送确认。这提高了确认的可靠性,并略有增加数据传输。
  • 实时确认现在允许我们实时测量RTT并了解网络条件。我们可以区分真正的消息损失和网络损失。
  • 它在协议之上提供抽象层,以支持流量多路复用等功能。它还允许我们尝试应用级网络优先级和流量控制算法,以便在通信的数据使用和延迟方面以更高的效率。
  • 该协议摘要消息有效负载以支持不同类型的序列化。在未来,我们可以探索其他序列化,但在运输层中保持GRPC。
  • 以不同语言的客户端的强大实现还允许我们快速支持不同类型的应用程序和设备。

这项工作目前处于Beta释放,早期信号是有前途的。

最终的想法

推送平台是优步旅行体验的一个组成部分。今天的数百个功能由这个平台提供动力。以下是为什么这个平台在优步成功的问题很少。

关注点分离

在消息触发,创建和交付系统之间清晰地分离责任,允许我们在平台的不同部分转向各个部分,因为业务的需求发生了变化。将递送组件分离为Apache Helix,拓扑逻辑和流的核心业务逻辑非常分开。这允许在完全相同的架构上支持GRPC,但具有不同的线路协议。

行业标准技术

基于行业标准技术的构建使实现更加健壮、长期且具有成本效益。上述系统的维护开销非常小。我们能够通过一个非常高效的团队规模来传递这个平台的价值。根据我们的经验,Helix和Zookeeper非常稳定。

更简单的设计

通过这个协议,我们可以扩展到不同网络条件下的数百万用户、数百种功能和数十种应用程序。协议的简单性使其易于扩展和快速移动。

我们要感谢所有工程师,他采用合作努力在多年来建立这个平台。Uber Bangalore网站的边缘流动团队正在领先这个平台演变的下一阶段。如果您有兴趣解决具有直接业务影响的高可扩展性分布式系统问题,请申请加入我们的团队

注释
前一篇文章 没有代码工作流编排器构建批量和大规模流管道
下一篇文章 优步的多区Kafka灾难恢复
Uday Kiran Medisetty是优步的高级工程师。他领导了将Uber Rider应用从基于单片轮询的架构转移到HTTP/JSON api的实现。2017年,当Uber Rider应用进行重新编写和设计时,他利用之前迁移的经验,将应用从基于拉的架构构建为基于推的架构,并将此推广到公司的其他移动应用。在过去的几年里,他领导了优步核心履行平台的重新架构。
Nilesh Mahajan是Uber的一名员工。他在数据工程团队中加入了优步5年前,并建立了大规模的数据库工具。然后,他4年前加入了推送消息平台队伍,并且在构建多个世代系统并将其缩放到当前需求中的工具。他最近离开了优步开始新的冒险。
Anirudh Raja是Uber的软件工程师II。他加入了推送消息平台1.5年前,作为班加罗尔队的早期成员之一。他目前正在研究GRPC的第三代RAMEN协议,这是测试/推出的Beta,以及文件上传等其他流动计划。
Madan Thangavelu是Uber的工程总监。在过去的6年里,他见证了优步令人兴奋的超高速增长阶段,并为其做出了贡献。他花了4年时间领导优步的网关平台团队。目前,他是优步为实时全球规模的购物和物流系统提供动力的履行平台的工程领导。