Remote Procedure Call as a Managed System Service

通过dynamic binding、将marshalling放在每台机器的一个集中服务上、架构设计,改变了原有的RPC库+sidecar的实现,实现热更新、RPC-as-a-Service、模块化policy enforcement。

本文将RPC编排和策略执行是作为系统服务完成,而不是作为一个链接到每个应用程序的库。

近年来,应用和网络运维团队需要rapid and flexible visibility and control over the flow of RPCs。 包括监测和控制特定类型的RPC的性能,优先级设施和速率限制以满足特定应用的性能和可用性目标,动态插入advanced diagnostics以跟踪微服务网络中的用户请求,以及特定应用的负载平衡以提高缓存的有效性。

现在典型的架构是在sidecar中实现不同的策略。 尽管使用sidecar是functional并且secure的,但是它也是低效的。 应用程序RPC库在运行时根据程序员提供的类型信息将RPC参数编入一个缓冲区。 这个缓冲区通过操作系统的网络协议栈发送,然后转发到sidecar,sidecar通常需要解析和解包网络、虚拟化和RPC头,经常在数据包有效载荷中寻找,以正确执行所需的策略。 然后,它对数据进行重新打包,以便进行传输。

对网络硬件的直接应用级访问,如RDMA或DPDK,提供了高性能,但排除了sidecar策略控制这个可能性。 同样,网卡也越来越复杂,但应用程序或sidecar很难利用这些新功能,因为marshalling工作是在网络协议栈的高层进行的。 对marshalling代码的任何改变都需要重新编译和重启每个应用程序和/或sidecar,损害了端到端的可用性。

简而言之,现有的解决方案可以提供良好的性能,或灵活和可执行的策略控制,但不能同时提供。

Background

Remote Procedure Call

大概介绍了RPC的一些基本概念。

The Need for Manageability

随着基于RPC的分布式应用扩展到大型、复杂的部署场景,越来越需要改善RPC流量的可管理性。 我们将管理需求分为三类:

  1. 可观察性:提供详细的遥测数据,使开发者能够诊断和优化应用性能。
  2. 政策执行:允许运营商对RPC应用和服务应用自定义策略(例如,访问控制、速率限制、加密)。
  3. 可升级性: 支持软件升级(如错误修复和新功能),同时尽量减少应用程序的停机时间。

一个自然的问题是:是否有可能在不改变现有RPC库的情况下增加这些属性? 很不幸的是,在当前这种RPC库+sidecar的模式下,不太可能。

Overview

图2给了一个high-level的overview。mRPC服务作为一个non-root、用户态的进程运行,只访问必要的网络设备和a shared-memory region for each application。

共享内存被用来在应用和mRPC服务之间通信,包括control queues和一个data buffer。

整体流程包括以下几个步骤

  1. 用户定义protocol schema,mRPC schema compiler基于此生成stub code
  2. 应用部署之后,应用连接到运行在同一台机器上的mRPC服务,并specifies the protocols of interest
  3. mRPC服务也利用protocol schema来生成、编译、动态加载一个protocol-specific lib。这个库包括了对应这个schema的marshalling和unmarshalling code
  4. 用户照常使用,只是数据结构是通过参数或者返回值传递的,并且开在shared data buffer上。
  5. stub和mRPC库管理control queues中的RPC calls和replies,还有data buffer中的allocations和deallocations。
  6. mRPC服务通过modular engines控制RPC,将要做的模块化。在执行过程中,read from niput queues,执行具体的事项,再enqueue outputs。

本篇文章的核心是operate over RPCs rather than packets。 主要通过以上的架构设计和dynamic binding实现。

mRPC可以

  1. 支持对单个RPC进行政策修改(这里的单个应该指的是一种)
  2. 支持对整台机器的统一下放
  3. 支持下面不同的网络处理功能

Design

Dynamic RPC Binding

应用程序有不同的RPC schemas,这些schemas最终决定了RPC的marshall方式。 在传统的RPC-as-a-library方法中,protocol compiler会生成marshalling代码,并将其链接到应用程序中。 在我们的设计中,mRPC服务负责marshalling,这意味着app-specific的marshalling代码需要与RPC库解耦,并在mRPC服务本身中运行。

应用程序直接向mRPC服务提交RPC schema(而不是marshall代码)。 mRPC服务生成相应的marshall代码,然后编译并动态加载库。 对于RPC客户端和RPC服务器之间的初始握手,两个mRPC服务检查所提供的RPC schema是否匹配,如果不匹配,客户端的连接将被拒绝。

现在还有三个问题。

首先,in-application user stub和mRPC库的职责是什么? 在mRPC中,应用程序依靠user stub来实现其RPC schema中指定的abstraction。 这意味着我们仍然需要生成胶水代码来维护传统的应用编程接口。 我们的解决方案是提供一个单独的protocol schema compiler,该编译器不受信任,由应用开发者运行,以生成不涉及marshalling和传输的user stub code。 应用程序的RPC stub(在mRPC库的帮助下)在共享内存堆上创建了一个包含RPC元数据的消息缓冲区,其中有指向RPC参数的类型化指针。 消息被放置在一个共享内存队列中,将由mRPC服务来处理。接收方也以类似的方式工作。

第二,这种方法是否会增加RPC连接/绑定的时间? 如果简单地实现,这种设计会增加RPC连接/绑定的时间,因为当RPC客户端第一次连接到相应的服务器时(或者等同于RPC服务器绑定到服务时),mRPC服务必须编译RPC schema并加载产生的marshalling库。 然而,这种延迟对我们的设计来说并不重要,我们可以通过以下方式来缓解它。 mRPC服务在启动应用程序之前接受RPC schema,作为一种预取的形式。给定一个模式后,它就会编译并缓存marshalling代码。 在RPC连接/绑定时,mRPC服务只需根据RPC模式的哈希值执行缓存查找。 如果它存在于缓存中,mRPC服务将加载相关的库;否则,mRPC服务将调用编译器来生成(并随后缓存)该库。这就把连接/绑定的时间从几秒钟减少到几毫秒。

第三,当新的应用程序到来时,现有的应用程序是否面临暂停? 多线程的mRPC服务是一个单一的进程,为许多RPC应用提供服务;然而,不同RPC应用的marshalling引擎并不是共享的。 它们在不同的内存地址中,可以独立地被加载。

Efficient RPC Policy Enforcement and Observability

这边的key idea是:发送方应该marshal一次,并且尽可能地晚;接收方应该unmarshal一次,并且尽可能地早。 在接收方,policy enforacement和observability直接在RPCs上进行操作,然后RPC才被marshal,变成packets。 在接收方,packets掀背unmarshal变成RPCs,然后执行policy和observability操作,然后再将RPCs发送给应用。

数据通过DMA-capable shared memory heaps操作。 中间提了一下如何缓解time-of-use-to-time-of-check攻击。

控制通过shared-memory queues实现。

Live Upgrades

尽管文章的模块化引擎设计与Snap和Click相似,但是这两个工作都不能热升级。 我们将引擎作为插件模块来实现,这些模块是可动态加载的库。我们设计了一种实时升级方法,支持升级、添加或删除数据通路的组件,而不影响其他数据通路。

升级一个引擎。 为了升级一个引擎,mRPC首先将该引擎从其运行时中分离出来(防止它被调度)。 接下来,mRPC销毁并删除旧的引擎,但在内存中保留旧引擎的状态;注意,这个时候引擎是与它的队列分离的,并没有运行。 之后,mRPC加载新的引擎并配置其发送和接收队列。新引擎以旧引擎的状态开始。 如果引擎的状态的数据结构有变化,升级后的引擎负责对状态进行必要的转换(引擎开发者必须实现)。 注意,这也适用于跨数据通路引擎的任何共享状态。最后一步是让mRPC将新的引擎附加到运行时上。

改变data path。 当操作者改变data path以增加或删除一个引擎时,这个过程现在涉及到队列的创建(或破坏)和in-flight RPC的管理。 增加一个引擎的变化是直接的,因为它只涉及到分离和重新配置引擎之间的队列。 移除引擎的变化则更为复杂,因为一些in-flight的RPC可能被维护在内部缓冲区中;例如,速率限制器策略引擎维护一个内部队列,以确保输出队列符合配置的速率。 当引擎被移除时,引擎开发者要负责将这种内部缓冲区冲到输出队列中。

多主机升级或data path改变。 一些涉及到发送方和接收方主机的引擎升级或data path改变,需要仔细管理跨主机的in-flight RPC。

Security Considerations

进行了一些安全讨论。

Advanced Manageability Features

在本节中,我们将介绍我们在策略引擎框架上开发的两个这样的功能,以证明我们的RPC-as-a-managed-service架构的更广泛的效用。

特性1:全局RPC QoS。 mRPC允许根据当前未完成的RPC的全局视图对跨应用工作负载进行集中的RPC调度。 例如,mRPC可以强制执行一种政策,在各应用中优先考虑具有最早截止日期的RPC,以支持延迟SLO,或者优先考虑对延迟敏感的工作负载。 这里的一个挑战是,一个天真的实现可能试图对分布在多个运行时间(即执行线程上下文)的数据路径应用QoS策略。 这将需要每个数据通路上的(复制的)策略引擎共享未完成的RPC的状态,从而带来同步开销。 因此,我们采用了Linux内核中使用的类似策略,在每个运行时间的基础上应用QoS策略,这反而可以使用运行时间的本地存储,而不需要同步。在我们的实现中,我们支持一种QoS策略,根据可配置的阈值大小优先考虑小型RPC。

特点2:避免RDMA性能异常。 众所周知,如果不进行微调,RDMA工作负载可能无法充分利用特定RDMA NIC的能力,而特定的流量模式甚至会导致性能异常(例如,低RDMA吞吐量、暂停帧风暴)。 以前的工作,如ScaleRPC和Flock已经提出了更有效地利用RNIC的技术。 然而,他们的方法是基于库的,只适用于单个应用程序; 因此,他们没有处理多个应用程序工作负载的组合导致RDMA性能不佳的情况。

我们在RDMA传输引擎内实现了一个全局RDMA调度器,它将RPC请求翻译成RDMA消息,并将其发送到RDMA NIC。 在我们的实现中,我们专注于解决穿插的小型和大型分散收集元素(可能是跨RPC以及应用程序)的性能下降问题。 我们将这些元素与一个显式拷贝融合在一起,融合后的元素大小的上限为16KB。