通过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流量的可管理性。 我们将管理需求分为三类:
- 可观察性:提供详细的遥测数据,使开发者能够诊断和优化应用性能。
- 政策执行:允许运营商对RPC应用和服务应用自定义策略(例如,访问控制、速率限制、加密)。
- 可升级性: 支持软件升级(如错误修复和新功能),同时尽量减少应用程序的停机时间。
一个自然的问题是:是否有可能在不改变现有RPC库的情况下增加这些属性? 很不幸的是,在当前这种RPC库+sidecar的模式下,不太可能。
Overview
图2给了一个high-level的overview。mRPC服务作为一个non-root、用户态的进程运行,只访问必要的网络设备和a shared-memory region for each application。
共享内存被用来在应用和mRPC服务之间通信,包括control queues和一个data buffer。
整体流程包括以下几个步骤
- 用户定义protocol schema,mRPC schema compiler基于此生成stub code
- 应用部署之后,应用连接到运行在同一台机器上的mRPC服务,并specifies the protocols of interest
- mRPC服务也利用protocol schema来生成、编译、动态加载一个protocol-specific lib。这个库包括了对应这个schema的marshalling和unmarshalling code
- 用户照常使用,只是数据结构是通过参数或者返回值传递的,并且开在shared data buffer上。
- stub和mRPC库管理control queues中的RPC calls和replies,还有data buffer中的allocations和deallocations。
- mRPC服务通过modular engines控制RPC,将要做的模块化。在执行过程中,read from niput queues,执行具体的事项,再enqueue outputs。
本篇文章的核心是operate over RPCs rather than packets。 主要通过以上的架构设计和dynamic binding实现。
mRPC可以
- 支持对单个RPC进行政策修改(这里的单个应该指的是一种)
- 支持对整台机器的统一下放
- 支持下面不同的网络处理功能
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。