[转载]Uber 基于 gRPC 的 下一代推送平台

在上一篇博客文章中,我们讨论了如何从轮询更新应用到基于推送的流程来构建应用体验。

我们所有的应用程序都需要与实时信息同步,无论是通过接送时间、到达时间、屏幕上的路线,还是打开应用程序时附近的司机。我们使用我们的推送平台来传递这些信息,这些信息为实时用户体验提供了动力,正如我们在前面的文章中所描述的,我们强烈建议您在继续之前先回顾一下,以了解体系结构的细节。

这篇博文将介绍我们如何将我们的协议从服务器发送事件(HTTP1.1)改为基于 gRPC 的双向流(QUIC/HTTP3) ,我们面临的挑战,最终的结果,以及一些关键的经验教训。


移动到 gRPC 的动机

在本节中,我们将讨论将 RAMEN (实时异步消息传递网络)从 SSE (Server-Sent Events)转移到 gRPC 作为传递消息的协议的原因。

在开始之前,让我们快速了解一下我们是如何使用 SSE 作为底层协议构建 RAMEN 的。这将有助于理解我们为在每个层上启用 gRPC 所做的更改。

现有的基于 SSE 的体系结构

客户端

下面是对客户端实现的一个快速概述:

图2: RAMEN SSE 架构前端

  1. 移动 RAMEN SSE 客户端通过 SSE/RAMEN/接收端点发起到 RAMEN 服务器的连接,并维护一个持久连接。
  2. RAMEN Server 在生成消息时通过 SSE 连接将所有消息推送到移动客户机。
  3. 然后,Mobile Ramen 模块在收到新消息时通知消费者,消费者是这些消息的最终用户(由特性团队构建)。
  4. Ramen 服务器还每4秒向移动端发送一条心跳信息。移动客户端假设连接中断,如果在7秒内看不到心跳或消息,则重新连接。
  5. 移动Ramen 模块每30秒通过另一个端点/Ramen /ack 向Ramen 服务器发送批量确认。

服务器端

图3: RAMEN SSE 体系结构后端

作为第1部分的快速回顾,以下是服务器端的实现方式:

  1. Streamgate 服务在 Netty 上实现 RAMEN 协议,并具有与处理连接、消息和存储相关的所有逻辑。Redis * 和 Cassandra 用于存储消息。

  2. StreamateFE 服务充当 Apache HelixTM Spectator,并监听来自 Apache ZooKeeperTM 的拓扑更改。它实现了一个反向代理。

  3. Helix Controller,顾名思义,是一个由五个节点组成的独立服务,专门负责运行 Apache Helix Controller 进程,并且是拓扑管理的核心。每当任何 Streamgate 节点启动或停止时,它都会检测到更改并重新分配分片分区。

  4. Streamgate 通过 SSE 端点发送消息,并通过 ack 端点接收确认。


RAMEN 的局限性

我们构建的基于 SSE 的 RAMEN 协议是单向的,并且是向客户端公开的唯一流端点。RAMEN 通过事件流发送消息,但是,消息确认通过常规 RPC 请求每30秒发送一次。

可靠性问题

虽然这个协议运行良好,但是有一些限制我们想要解决。将消息写入 RAMEN 连接后,消息的传递状态未知的时间长达30秒。许多重要的信息,如发送给司机的提议有效期为30秒。这可以防止我们重新发送像驱动程序提供的关键推送消息。

我们希望实现即时、实时的确认,而双向流将为实现这一点提供最有效的方法。然而,我们的协议是建立在 HTTP 1.1之上的,支持双向流的唯一方法是在相反的方向上有两个不同的单向连接。这是次优的,因为它将使基础设施必须处理的连接数量增加一倍,并且还会导致竞态条件,即一个连接建立,而另一个连接没有建立,或者建立在不同的数据中心。另一种方法是在接收到每条消息时为其发送一个新的 HTTP ack 请求。这会导致由于报头而上传的数据量增加,从而导致更高的上行带宽使用率。

心跳/连接管理

流的可靠性很大程度上取决于我们如何处理连接。心跳机制对于了解连接是否活动至关重要。由于各种网络代理和有损移动网络的缓冲区很大,判断连接是否中断的唯一方法是心跳。在 RAMEN 协议中,心跳与消息在同一个流上发送。因此,它们受到队头阻塞的制约。如果通过缓慢的网络发送大型消息,如果客户端有一段时间没有看到心跳,则可能断开连接。相反,对于 HTTP2这样的协议,心跳和控制消息通过不同的流发送,使其更有效率。

二进制数据

RAMEN 在 SSE 上运行,默认情况下该协议是基于文本的。我们发送由换行符分隔的转义 JSON。因此,我们不能有效地发送二进制有效载荷(图像,语音等)和减少常规有效载荷的大小。我们评估了实现发送二进制文件的自定义 SSE 实现,因为我们也控制客户端。然而,这进一步进入了一个非标准的协议领域,并将是痛苦的维持在长期。

利用 QUIC/HTTP3特性

QUIC/HTTP3现在得到了广泛的支持,并且具有流、多路复用、心跳机制、二进制、流控制等优点。我们有 QUIC/HTTP3连接,终止于前端基础设施。因此网关层服务不能利用 QUIC/HTTP3的这些优点。在边缘基础设施层,没有应用程序状态感知,因此不能充分利用这些特性。

连接管理和多语种客户端

所有移动客户端都使用自定义 SSE 实现,该实现使用文本解析和模式匹配来提取流中的单个消息。客户机还使用心跳和 ack 执行连接生命周期管理,这些都很复杂,很难维护。

随着用例和业务线数量的增加,使用 RAMEN 的客户端数量正在快速增长。我们用不同的编程语言为 RAMEN 编写了客户端,以支持不同类型的应用程序。改变协议,迭代特性变得越来越困难。其中一些客户端比如移动应用程序需要进一步的优化,比如流量控制、有效负载大小优化、可见性改进等等。如果没有非常可靠的客户端实现,就很难构建和扩展新协议。

迈向 gRPC

GRPC 是一个高性能、被广泛采用的 RPC 框架,具有跨多种平台和语言的客户端和服务器的标准化实现。下文着重介绍了实现全球区域方案协调的主要原因:

双向流

GRPC 拥有对双向流的一流支持,这是考虑到 RAMEN 的长期愿景时最具吸引力的特性。确认信息可以通过同一个流立即发送,而不需要从移动客户端进行额外的网络调用。这可以显著提高 RAMEN 的确认可靠性。

实时确认允许我们测量 RAMEN 消息的 RTT 并了解网络条件。它们还有助于 RAMEN 后端减少消息队列的内存占用。

QUIC/HTTP3

QUIC/HTTP3基本上消除了这种队头阻塞,与 HTTP2相比,它持续而显著地改善了移动网络的延迟。正如我们从以前的 QUIC 实验中了解到的,QUIC 为 HTTPS 流量的尾端延迟带来了10% -30% 的改善,我们希望在这里也可以利用它来实现 RAMEN gRPC。

GRPC 可以使用 Cronet 作为传输,这允许 RAMEN 从实时流量中重用 QUIC 会话,进一步减少了第一个 RAMEN 消息到移动端的延迟。

工具

GRPC 提供了强大的工具社区支持、性能监控的 CI/CD 和多语言支持。从长远来看,这些好处将大大简化架构,创建新的应用程序、客户端和协议将变得更加容易。

高级用例

GRPC 将使网络优先级和有效负载等特性的开发变得更加容易。尽管 gRPC 不支持现在的优先级排序,但是双流 API 将帮助我们在 gRPC 之上构建伪优先级排序。


数据建模

我们使用 Proto3 Protobuf 来定义服务器和客户端之间的契约。拥有一个定义良好的契约使得在各种 RAMEN 客户机中实现更加容易。从而缓解迁移并确保更少的错误。

RPC 契约

RPC 被定义为一个双向端点,正如我们在前一节中所讨论的。服务器和客户端都将来回保持数据流。在高级别上,服务器将发送消息,客户机将回复确认。

图4: Ramen合同

这就是我们如何在我们的 Protobuf 模式中定义请求和响应契约:

图5: Ramen请求响应模型

请求数据模型

  1. SeqID 用于簿记目的,我们将序列号存储在每个消息中,然后使用它来跟踪传递。
  2. 消息确认插件用于确认消息,特性确认插件用于监听 RAMEN 消息的特性团队插件发送的确认。
  3. 我们还有控制消息,客户机可以在这些消息中指示对服务器的任何运行时更改,例如终止连接。我们还计划在未来将其用于流控制和流优先级排序等高级构造。

响应数据模型

我们以通用的方式对响应建模,其中消息可以归入任何类别,如 RAMEN 消息、控制消息或心跳。

  1. RAMEN 消息包含驱动各种用例的所有实际消息。
  2. 控件消息用于向客户端指示断开连接等。
  3. 心跳每5秒发出一次,以表明连接是健康的。

时序图

图6: RAMEN 时序图


后端更改

在本节中,我们将讨论为了在服务器端启用 gRPC 而必须做出的主要改变。我们决定将 Streamgate 服务作为新的 RAMEN gRPC 连接的后端。出于性能和故障打开的原因,Streamgate 在内存中维护消息和邮箱的缓存。我们不想为基于 gRPC 的连接创建一个单独的运行时,因为我们需要在每个用户的2个位置复制所有这些内容,从而使系统的负载和资源使用增加一倍。这也会导致不一致性和更多影响用户的机会。

图7: RAMEN 的职责和相互作用

这主要是一个外观级别的更改,所有内部业务逻辑保持不变,如消息存储、编排、连接生命周期管理和流控制。因此,可以将这个 facade 从 HTTP 更改为 gRPC,而不存在许多潜在的风险,并且在遇到任何问题时可以回滚到旧的堆栈。GRPC 内部服务器的实现也是基于 Netty 的。因此,我们不会失去 gRPC 迁移带来的任何好处。我们的内部代码可以继续使用字节缓冲区。

流门前端的单独运行时

Streamgate FE 是一个服务,它负责将请求路由到由 userID 分片的正确的 Streamgate 实例。为了启用 gRPC,我们决定将前端运行时分离出来,以便能够无缝地代理 gRPC 请求。GRPC 流量通过新的运行时流入,并被代理到集群中的正确的流门实例,就像旧的 HTTP 流量(SSE 协议)一样。

图8: Streamgate 前端代理架构

我们使用 GRPC 代理来实现路由请求的前端层-Streamgate FE-gRPC (流式和非流式)。我们之所以选择这个实现,是因为它提供了一个简单的接口,用于处理进入前端层的代理请求,并与 Helix 层集成以确保粘性路由。如下所述,我们将 Helix 后端路由插入到 gRPC 代理集成中

图9: 前端代理集成代码块

上面来自我们的 gRPC 代理实现的代码片段展示了我们如何从调用中获取元数据(例如,方法名称,用于路由请求的用户 ID) ,以找出已经与 Streamgate 实例建立的正确的 gRPC 通道(如果已经建立的话) ,然后使用该通道路由请求。

基于 gRPC 的信道管理

我们为每个 Streamgate 主机构建一个 gRPC 通道,流和一元请求都通过相同的通道。当我们开始实现时,我们假设 gRPC 可以在同一个通道中处理流和一元(unary,RPC)调用的混合,并且它最终对我们工作得很好。


移动客户端变化

在本节中,我们将讨论为了支持新的 RAMEN gRPC 协议,必须在客户端进行的主要更改。

RAMEN gRPC 合同执行

为了最大限度地减少 RAMEN 消费者的迁移工作,新的 RAMEN 模块被设计成保持公共接口完整的方式。在移动客户机上,我们编写了一个层来与新的 RAMEN gRPC 客户机交互,解析/读取消息并将其传递给消费者。这确保了消费者可以继续使用这些 RAMEN 消息,而不需要进行任何迁移。

图10: Ramen 移动架构

连接管理

对于移动客户端来说,与后端建立可靠的连接至关重要,这样后端就可以向移动应用程序发送消息,以保持用户的参与,并在 Uber 后端系统有更新时通知用户。

RAMEN 连接可能由于各种原因而中断,例如网络错误、后端断开连接或心跳超时。为了管理和维护稳定的 gRPC 双向连接,移动客户端需要通过 RAMEN 心跳监视连接状态。由于心跳超时或从服务器接收到断开连接控制消息(如果发生任何这些断开连接) ,RAMEN 应该能够在截断二进制指数避退算法内重新建立连接。


网络堆栈更改

图11: Ramen 网络堆栈的变化

RAMEN SSE 构建在 OkHttp 和 iOS 网络库之上,由各种拦截器组成,以支持核心功能,例如故障转移和重定向、网络监视、报头和 OAuth 令牌充实。为了将 RAMEN 迁移到 gRPC,我们必须重新设计移动网络栈,使其具有与 SSE 栈相同的功能。

故障转移和重定向

捕获网络故障或重定向,并在更改主机名时发出呼叫,该主机名将用于从移动应用程序向 Uber 服务器发出的后续网络呼叫。

Header 和 OAuth 令牌

Header 和 OAuth 令牌由拦截器添加到所有 HTTP 请求中。

网络监察

捕获网络性能数据,如端到端延迟、创建连接的时间、到 TLS (传输层安全)的时间、查找主机的时间、响应状态代码、网络错误、请求路径等。此数据主要用于监视网络调用、回归、中断等的性能。

图12: Ramen 移动架构层


关键知识

并发性问题

对于每个连接,我们有一个 stream Observer 实例,用于将消息写入到连接中。建议 (按照 gRPC 规范)同步 onNext 方法,因为它不是线程安全的。当我们不做同样的事情时,我们遇到了问题。

消息流控制

我们使用 gRPC 提供的 ServerCallStreamObserver API 封装了 stream Observer,它提供了比原生 stream Observer API 更丰富的功能。我们的主要用例是,我们希望只有在连接成功引导之后才能从客户机开始接收 acks,因此我们阻塞来自客户机到服务器的任何新消息,直到连接启动的第一条引导消息完成。

图13: ServerCallStreamObserver 集成

优雅的关机处理

关闭处理是另一个棘手的部分,我们必须调用所有活动 stream 观察者的取消方法来确保连接终止。因此,我们将它包装在 ShutdownHandler 包装器中,并保留了一个处理程序映射,当收到关闭信号时,我们必须终止它。

处理丢失的回调

我们注意到,在某些情况下,我们没有正确地接收客户端终止呼叫。当接收到连接回调时,诸如发送心跳之类的计时器任务被取消。但是在那些没有回调的情况下,我们遇到了任务永远运行的情况,导致了非常高的 CPU 和内存使用率。我们添加了一个检查,在每次尝试写入流并取消任何尝试写入流的任务之前,检查流是否可写。

图14: 流可写检查

有效载荷压缩

在开发阶段,我们观察到心跳超时率很高。经过调试,我们发现一些 RAMEN 消息的有效负载大小偏大(> 1MB) ,在极慢的蜂窝连接条件下(Edge 或3G) ,下载整个消息需要20到50秒。在这些情况下,心跳消息被这些巨大的有效负载阻塞,进一步的调查显示后端没有为有效负载启用压缩。在后端添加 Gzip 作为有效负载的压缩之后,我们的实验表明下载这些巨大的有效负载消息只需要5秒钟。

我们还研究了 RAMEN SSE 实现,在这种情况下没有这样的心跳超时,因为底层协议允许读取有效负载的块,而不是像 gRPC 中看到的那样等待整个有效负载到达。

后备机制

我们投资于在移动端构建工具,以便迅速从 gRPC 退回到 SSE 协议。我们在推出过程中遇到了一些问题,在这些问题中,我们意识到拥有一个健壮的回退机制将使我们能够减轻重大故障。回退层的构建是为了通过 gRPC 堆栈检测连接中的故障,如果需要的话,它将迅速回退到基于 SSE 的堆栈。


我们已经在 Android 和 iOS 上在全球所有移动应用程序(Rider、 Driver 和 Eats)上完全推出了 RAMEN over gRPC。我们取得的一些关键成果是:

  1. 由于上述所有更改,gRPC 连接延迟(p95)至少提高了45% 。依赖于 RAMEN 的特性可以提前启动,因为通过改进连接延迟,它们可以通过 RAMEN 提前接收数据。
  2. 所有应用程序的推送成功率都至少提高了1-2% ,RAMEN gRPC 中每个会话发送的平均消息数也有所改善。
  3. 有了来自客户端的实时确认,我们就可以更好地了解 RTT,从而释放机会来明智地使用网络带宽。
  4. 我们在所有客户端都有一个一致的实现,以减少未来发生错误和中断的可能性。

考虑的替代方案

HTTP2直接

解决上述问题的方法之一是创建我们自己的客户机/服务器实现,它可以利用所有 HTTP2特性并支持双向流。虽然 HTTP2规范更容易理解,但是自己实现所有功能要困难得多。实现这一目标需要很长时间,确保跨不同语言/平台实现的一致性将是非常痛苦的。这几乎就像重新发明 gRPC 一样。

反应流

反应套接字/流是另一个解决相同问题的 RPC 框架。然而,围绕其开发和支持的活动并不多。相反,gRPC 更加成熟,更加关注性能和工具。由于 Uber 的其他部分正在转向 gRPC,我们觉得让相同的框架端到端并降低支持多个框架的成本是一个好主意。


消息的二进制序列化

客户机和服务器之间的通信模式目前使用 Thift 编写,通过连接的序列化是 JSON。所以我们有一个 gRPCRAMEN 连接,但是发送的消息都是 JSON 编码的。我们正在努力实现一个中间目标,首先在线上发送二进制编码的信息,在线上编码信息,但保留节俭合同,然后逐步达到我们的最终目标,在线上编码和原型合同。

利用 gRPC 流

当有大型消息阻塞管道并阻止其他消息通过时,gRPC 连接中仍然存在队头阻塞。我们计划在一个 gRPC 连接中利用多个流,并对这些流进行流控制和流优先级排序,如下所述,以减少对关键消息的阻塞。

流量控制和流量优先级排序

HTTP2协议通过设置流优先级描述了一些像网络优先级这样的高级特性。为流设置更高的优先级将能够对请求和响应进行更高的线路级优先级排序。然而,目前还没有 HTTP2的实现支持这个特性,包括 gRPC-Java。此外,我们现在还没有一个很好的流控制机制,在这种机制中,我们可以决定在网络带宽超负荷时缓冲/保存消息。我们希望根据网络条件进行优先排序并发送消息。

我们相信,这些缺点仍然可以通过在应用程序级强制执行优先级来解决。有各种各样的方法来解决这个问题,但是在没有适当的实验/数据的情况下,很难在全球范围内做出这样的选择。然而,gRPC 本身使高阶协议的开发变得更加容易,我们可以很快地尝试这样的想法。现在我们已经从 HTTP 推出了 RAMEN gRPC,我们将评估这些网络优先级排序技术。

我们要感谢所有的工程师,他们共同努力在全球范围内推广 gRPC。优步班加罗尔网站的 Edge Streaming 团队正在领导未来愿景的下一步工作。如果您有兴趣解决对业务有直接影响的大规模分布式系统问题,请申请加入我们的团队

本文档中的所有图表都是在 Lucidchart ((www.lucidchart.com))中创建的

原文地址:www.uber.com/en-GB/blog/ubers-next...

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!