Azure Cosmos DB是一个完全管理的、多租户的、分布式的、无共享的、可横向扩展的数据库,提供星球级的NoSQL功能,提供Apache Cassandra、MongoDB、Gremlin、Tables和核心(SQL)API。Azure Cosmos DB服务的一个核心组件是API网关,它处理所有支持的API之间的请求解析和路由。它现在是在.NET 6上运行。API网关是服务的前门,它每天接受和处理数万亿的请求!鉴于API网关的高吞吐量和可扩展性要求,我们不断投资优化服务的性能,包括依靠新的.NET功能。
API网关最初是在.NET框架上开发的。在.NET框架上,我们提高API网关性能的能力有限,多年来,我们将API网关转移到.NET核心。转移到.NET Core(和.NET 6)释放了许多功能和优化,我们急切地利用这些功能和优化来增强服务。目前,这个关键的基础设施是由.NET 6驱动的,并利用多种.NET性能和可扩展性功能来实现低延迟、高吞吐量的端到端请求处理。
像Azure Cosmos DB这样的服务需要使用低级别的平台功能,我们很高兴.NET 6包含了一套新的功能供我们使用。我们的改变导致CPU利用率整体降低,并将核心请求的端到端P90延迟提高了1500%!这篇文章涵盖了Azure Cosmos DB与.NET的关系,以及使用该框架给服务带来的诸多好处。
Azure Cosmos DB架构
Azure Cosmos DB有一个分层架构,由一个包含用户数据的分区多用户存储节点集群组成,并处理该分区内的分布、复制和请求的处理。数据在这些存储节点中以与API无关的方式存储和处理。除了存储节点外,还有其他集群作为API网关节点。这些API网关接受所有API的请求,解析/翻译/处理这些请求,在分布式存储节点上有效地路由这些请求,并将处理后的响应分派给客户。
图1:Azure Cosmos DB服务的整体架构
API网关节点是多用户的,并在其中承载多个.NET进程,每个进程都处理给定API的请求。每个API网关进程都使用节点的一个资源子集,随着集群负载和状态的变化,这些资源在租户之间进行负载平衡。 也有高级产品,客户可以购买专用的网关容量,以实现高级功能,如贯穿写缓存、数据转换等。高级产品可以在服务中预留资源,如核心、内存,可以为该客户处理半状态工作负载。
为网络密集型场景提供高吞吐量和可预测的低延迟开销是网关的关键要求。由于请求由API网关处理,然后由存储节点处理的分层架构,网关上的请求处理开销需要对终端用户 "有效不可见"。由于我们努力追求最小的处理开销,底层框架的行为和功能成为理解和建立的关键。我们在很大程度上依靠.NET功能来实现这些性能目标。
在API网关中利用.NET
构建高度可扩展的高性能服务需要对底层框架有深入的了解,Azure Cosmos DB利用了.NET运行时的一些特性来实现Azure中的性能目标。请求管道中的许多地方都使用了.NET的特定性能特征,以改善分配情况或减少请求的处理开销。 多年来,每一次.NET升级都对性能有明显的改善。这些都是由于Azure Cosmos DB代码库的变化,作为采用新框架的一部分,以及运行时本身的改进。图2显示了多年来每次框架升级带来的相对改进和吞吐量的增加(越高越好)。
图2:多年来Cassandra API上简单插入/读取查询的相对吞吐量(以2020年3月为基准)。
HTTP协议性能
文档(SQL)、表和Gremlin APIs的传输协议是基于HTTP的。.NET框架提供了一个使用HttpListener的默认实现,利用Windows中的Http.SYS实现来处理请求。这个API围绕着Native回调,为HTTP提供了一个基于请求/响应的模型的服务。然而,这也给缓存与用户连接相关的状态以及在读取一个给定的请求时有多个用户/内核转换留下了显著的性能优势。 API网关从默认的HttpListener栈转移到ASP.NET Core Kestrel栈,该栈在用户模式下实现了整个HTTP协议,并对协议和请求状态进行进程式解析。这也允许在高度可扩展和高性能的Kestrel堆栈中缓存连接绑定状态。目前,Azure Cosmos DB的HTTP框架完全基于Kestrel,此举带来的性能提升如下。:
图3 Kestrel迁移后单个网关进程的吞吐量改进
图4:Kestrel迁移后点读请求的端到端相对改进(越低越好)
Kestrel的好处包括:SSL连接的配置管理更容易,网络和账户配置的连接状态缓存,以及由于在Kestrel传输层和API网关基础设施之间使用共享内存池,缓冲区管理更完善。
缓冲区管理和低分配的API
作为一项网络密集型服务,Azure Cosmos DB的API网关要处理大量的字节缓冲区。很大一部分处理需要将缓冲区从源头复制/处理到汇中(无论是从客户套接字到目标存储节点,还是反过来)。在此基础上,必须对请求进行部分解析,以了解必要的路由信息和租户细节。尽量减少这些工作流程的分配和处理开销在请求的管道中至关重要。
多年来,Azure Cosmos DB大量利用了.NET库中的Span、Memory和MemoryPool实现,包括用于字符串操作的MemoryExtension API,以避免在请求路径中进行分配。在ReadOnlyMemory上使用MemoryExtension APIs交换URI路径提取和UTF-8跨度的直接协议解析,大大减少了API网关中路由请求的开销。在I/O路径中使用Pooled Memory支持的缓冲区,将网络中的缓冲区分配开销减少到请求路径中几乎为0,减少了P99延迟中垃圾收集的影响。
同样,使用字典的内存缓存也很常见,利用ConcurrentDictionary的新的.NET APIs,仅为AddOrUpdate传入一个Context参数,就将请求路径中的委托分配开销降低到了0。
当我们从.NET Framework迁移到Core时,可以看到这些改进的影响,一个特定客户的延迟从迁移中明显改善了5倍。
NUMA亲和性和进程扩展
随着现代CPU架构趋向于多核和非统一内存访问(NUMA),服务也需要适应这种架构。Azure Cosmos DB的网关在具有数百个内核和多个NUMA节点的虚拟机上运行。运行跨越多个NUMA节点的大型进程会产生次优的性能,GC和线程池的跨NUMA交互。这也是一个相当普遍的现象,在运行超过64个内核的进程中(跨Windows的处理器组)。
因此,Azure Cosmos DB将带有Job对象的进程与NUMA节点内的特定核心相联系,并在一个给定的节点上运行多个这样的进程。这允许更好的内存定位,更好地利用缓存,以及更好的性能预测性。它还有助于改善对恶意查询、租户和请求的治理,因为管理这种错误请求的范围仅限于虚拟机中的一个核心子集。根据与.NET团队的讨论,处理器亲和性被推荐为一种普遍良好的方法,以减轻NUMA的内存访问模式的负面影响。
然而,当一个虚拟机中存在几个这样的进程时,在这些进程之间进行动态的连接负载平衡是一个挑战。为了解决这个问题,Azure Cosmos DB利用了.NET 5和6的功能,在一个节点内实现了动态连接复用。对于基于TCP的协议,.NET支持序列化连接状态并在另一个进程中 "恢复 "连接。同样,Kestrel在.NET 6中引入了跨多个进程的负载平衡连接能力。鉴于这些特性,Azure Cosmos DB网关进程可以动态地监测负载指标,并在大型虚拟机上重新平衡传入的连接。
图5:单个进程的吞吐量与多个进程的连接复用Cassandra API的吞吐量(越高越好)。
如图5所示,引入连接复用后,由于进程和内核之间的强大亲和力导致更好的线程/内存定位,在吞吐量方面呈现出近乎线性的扩展。对于写工作负载,我们确实注意到由于API网关外的饱和而产生的瓶颈,我们正在努力解决这些瓶颈。
异步代码处理
API网关服务是高度异步的。.NET中异步代码API的改进为Azure Cosmos DB带来了最大的性能改进。由于增加了ValueTask和围绕异步状态机管理的框架的优化,导致了端到端的性能提高,而应用程序代码的变化却很小。在Azure Cosmos DB网关的每一次.NET升级中都观察到了这些改进(从.NET Core 2.2到3.1,再从.NET Core 3.1到.NET 6.0)。
图6:部署.NET 6.0后的集群级CPU数量
总结
Azure Cosmos DB的API网关是一种低延迟的Azure服务。它以多种方式利用.NET来实现其性能和延时要求。多年来,每一次的.NET升级都产生了多种好处,既提供了更好的性能管理方式的新API,又改进了框架内的现有API和运行时行为。我们正积极与.NET团队合作,采用.NET 7,并期待着在即将到来的.NET版本中获得更多具有高影响力的性能特性。





