Garnet: 力压Redis的C#高性能分布式存储数据库

今天看到微软研究院开源了一个新的C#项目,叫Garnet,它实现了Redis协议,可以直接将Redis替换为Garnet,客户端不需要任何修改。根据其官网的信息,简单的介绍一下它。

开源仓库地址:https://github.com/microsoft/garnet
文档地址:https://microsoft.github.io/garnet/

Garnet是微软研究院基于C# .NET8.0开发的一种新型远程缓存存储系统,它设计目的是实现极速、可扩展和低延迟。Garnet能够在单节点内进行线程扩展,并支持分片集群执行,具备复制、检查点、故障转移和事务处理功能。它可以在主内存以及分层存储(如SSD和Azure存储)上运行。Garnet提供丰富的API接口和强大的可扩展性模型。

Garnet使用Redis的RESP协议作为其主要通信协议,因此可以使用大多数编程语言中现成的Redis客户端,例如C#中的StackExchange.Redis。与其他开源缓存存储相比,Garnet在性能、延迟、可扩展性和持久性方面都有显著提升。

需要注意的是,Garnet是微软研究院的一个研究项目,应当作为研究项目来对待。尽管如此,我们是一群对此充满热情的研究人员和开发人员,目前正全职工作于此,以使其尽可能稳定和高效。我们的目标是围绕Garnet建立一个活跃的社区。实际上,Garnet的质量已经足够高,以至于微软的几个一线团队和平台团队已经在内部部署了Garnet多个月。

Garnet提供以下主要优势:

  • 与可比的开源缓存存储相比,在小批量和多客户端会话中,服务器吞吐量(每秒操作数)提高了数个数量级。
  • 在标准云端(Azure)机器上,启用加速TCP的情况下,单个操作的极低延迟(在99.9百分位数时常常少于300微秒),适用于Windows和Linux。
  • 随着客户端数量的增加,无论是否客户端批处理,都能实现更好的可伸缩性。
  • 使用单个共享内存服务器实例,可以利用服务器机器的所有CPU/内存资源(无需节点内集群)。
  • 支持超大内存数据集,可以溢出到本地和云存储设备。
  • 具备数据库功能,如快速检查点和恢复,以及发布/订阅。
  • 支持多节点分片哈希分区(Redis "集群"模式)、状态迁移和复制。
  • 经过全面测试,拥有包括Garnet及其存储层Tsavorite在内的数千个单元测试。
  • 一个易于进化和扩展的C#代码库。

性能

上面的简介显示出Garnet有非常多的优点,不过我最关心的是它的性能到底怎么样,看了基准测试的相关结果,总体还是让我非常吃惊,一起来看看性能到底怎么吧。

具体性能测试详情可以查看链接: https://microsoft.github.io/garnet/docs/benchmarking/results-resp-bench

性能测试环境

我们配置了两台运行Linux(Ubuntu 20.04)的Azure Standard F72s v2虚拟机(每台提供72个虚拟CPU和144 GiB内存),并启用了加速TCP功能。这种SKU的好处在于我们可以确保不会与其他虚拟机共置,这将优化性能。其中一台机器运行不同的缓存服务器,另一台专门用于发出工作负载。我们使用名为Resp.benchmark的基准测试工具来生成所有结果。我们将Garnet与撰写本文时最新的开源版本Redis(v7.2)、KeyDB(v6.3.4)和Dragonfly(v6.2.11)进行了比较。在这些实验中,我们使用均匀随机分布的键(Garnet的共享内存设计在倾斜工作负载下的好处更大)。所有数据在这些实验中都适合内存。基准系统根据可用信息进行了尽可能多的调整和优化。下面,我们总结了我们实验中使用的每个系统的启动配置。

基本命令性能

我们通过改变负载大小、批量大小和客户端线程数,对基本的GET/SET操作的吞吐量和延迟进行了测量。在吞吐量实验中,我们在运行实际工作负载之前,先向Garnet预加载一个小型数据库(1024个键)和一个大型数据库(256M个键)。相比之下,我们的延迟实验是在一个空数据库上进行的,并且是对一个小键空间(1024个键)的GET/SET命令的组合工作负载进行的。

吞吐量 GET

在图1所示的实验中,我们使用了大批量的GET操作(每批4096个请求)和小负载(8字节的键和值)来最小化网络开销。随着客户端会话数的增加,我们观察到Garnet的可扩展性比Redis或KeyDB更好。Dragonfly展示了类似的扩展性,尽管只能达到16个线程。还要注意,DragonFly是一个纯内存系统。总的来说,即使数据库大小(即预加载的不同键的数量)更大(达到2.56亿个键)超过了处理器缓存的大小,Garnet的吞吐量相对于其他系统始终更高。

图1:在数据库大小为(a) 1024个键,和(b) 2.56亿个键的情况下,随着客户端会话数的变化,吞吐量(对数尺度)。

即使对于小批量大小,Garnet也通过获得一致更高的吞吐量,超过了竞争系统,如图2所示。这一结果不受实际数据库大小的影响。


图2:在数据库大小为(a) 1024个键,和(b) 2.56亿个键的情况下,随着批量大小的变化,吞吐量(对数尺度)。

延迟 GET/SET

接下来,我们通过发出80%的GET和20%的SET请求的混合体,来测量各种系统的客户端延迟,并将其与Garnet进行比较。因为我们关心的是延迟,所以我们保持数据库大小较小,同时变化工作负载的其他参数,如客户端线程数、批量大小和负载大小。

图3展示了随着客户端会话数的增加,Garnet的延迟(以微秒计)在各个百分位数上都一直较低且更稳定,与其他系统相比。请注意,这个实验不使用批处理。


图3:在不同的客户端会话数下,延迟变化,(a) 中位数,(b) 第99百分位数,和(c) 第99.9百分位数

Garnet的延迟经过了精细调整,以适应客户端的批处理和高效处理查询系统的多个会话。在我们的下一组实验中,我们将批量大小从1增加到64,并在下面的图中以128个活跃客户端连接绘制不同百分位数的延迟。如图4所示,当批量大小增加时,Garnet保持稳定性并实现了比其他系统更低的整体延迟。


图4:在不同的批量大小下,延迟变化,(a) 中位数,(b) 第99百分位数,和(c) 第99.9百分位数

复杂数据结构性能

Garnet 支持大量不同的复杂数据结构,如Hyperloglog、位图、有序集合、列表等。下面,我们将为其中几个精选的数据结构展示性能指标。

Hyperloglog

Garnet支持其内置的Hyperloglog(HLL)数据结构。该结构使用C#实现,支持更新(PFADD)、计算估算值(PFCOUNT)以及合并(PFMERGE)两个或更多不同的HLL结构的操作。HLL数据结构通常在内存占用方面进行优化。我们的实现也不例外,当非零计数数量较低时采用稀疏表示法,超过给定的固定阈值后采用密集表示法,此时内存节省和解压缩所需额外工作之间的权衡已不再吸引人。为并发系统(如Garnet)有效更新HyperLogLog(HLL)结构至关重要。因此,我们的实验特别关注PFADD的性能,并且有意设计了以下情景来压力测试我们的系统:

  • 大量高争用更新(例如,批量大小为4096,数据库键为1024)随着线程数量的增加或有效载荷大小的增加。几次插入后,构建的HyperLogLog(HLL)结构将转为使用密集表示法。
  • 大量低争用更新(例如,批量大小为4096,数据库键为256M)随着线程数量的增加或有效载荷大小的增加。这种调整将增加构建的HyperLogLog(HLL)结构使用稀疏表示法的可能性。因此,我们的测量将考虑处理压缩数据或为非零值递增分配更多空间的额外开销。

在图5中,我们展示了第一个实验场景的结果。Garnet在高争用情况下扩展性非常好,并且在增加线程数量方面的原始吞吐量一致超过其他所有系统。同样,随着有效载荷大小的增加,Garnet 展示了比其他系统更高的总吞吐量。在所有测试的系统中,我们注意到随着有效载荷大小的增加,吞吐量明显下降。由于固有的TCP网络瓶颈,这种行为是预期之中的。


图 5:数据库大小为1024个键时,(a)客户端会话数量增加,(b)有效载荷大小增加的吞吐量(对数刻度)。

图 6显示了上述第二个实验场景的结果。即使在操作HLL稀疏表示时,Garnet的性能也比任何其他系统都要好,并且在增加客户端会话数量时能够实现一致性更高的吞吐量。同样地,对于增加的有效载荷大小,Garnet通过实现整体更高的吞吐量而胜过竞争对手。请注意,在这两种情况下,由于操作压缩数据的开销,吞吐量与之前的实验相比都有所降低。


图 6:数据库大小为1M个键时,(a)客户端会话数量增加,(b)有效载荷大小增加的吞吐量(对数刻度)。

在图 7中,我们进行了与前面所述相同类型的实验,将客户端会话数量固定为64,有效载荷固定为128字节,同时增加批量大小。请注意,即使对于批量大小为4,Garnet的吞吐量增益也明显高于我们测试的任何其他系统。这表明即使对于小批量大小,我们仍然能够胜过竞争对手的系统。


图 7:通过64个客户端会话增加批量大小的吞吐量(对数刻度),对于数据库有(a)1024个键,(b)1M个键。

Bitmap

Garnet支持对字符串数据类型的一系列位操作符。这些操作符可以在常数时间内(例如GETBIT、SETBIT)或线性时间内(例如BITCOUNT、BITPOS、BITOP)进行处理。为了加快处理速度,对于线性时间操作符,我们使用了硬件和SIMD指令。下面我们将呈现这些操作符子集的基准测试结果,包括两种复杂度类别。与之前类似,我们使用小型数据库大小(1024个键)来评估每个系统在高竞争下的性能,同时通过增加有效载荷大小(1MB)避免所有数据常驻CPU缓存。

在图8中,我们展示了GETBIT和SETBIT命令的性能指标。在这两种情况下,随着客户端会话数量的增加,Garnet始终保持较高的吞吐量和更好的可扩展性。

图8:吞吐量(对数刻度),变化的客户端会话数量,对于数据库大小为1024个键和1MB有效载荷。

在图9中,我们评估了BITOP NOT和BITOP AND(有两个源键)对于增加线程数量和1MB有效载荷大小的性能。Garnet在客户端会话数量增加时保持整体更高的吞吐量,与我们测试的每一个其他系统相比。鉴于我们的数据库大小相对较小(即只有1024个键),在高竞争下它也表现得非常好。


图9:吞吐量(对数刻度),变化的客户端会话数量,对于数据库大小为1024个键和1MB有效载荷。

如图10和图11所示,即使对于小批量大小,Garnet也获得了比我们测试的任何其他系统更高的吞吐量。实际上,即使在批量大小为4的情况下,Garnet也显著更快,观察到吞吐量的明显差异并不需要太多。


图10:吞吐量(对数刻度),对于64个客户端会话的不断增加批量大小的数据库,数据库大小为1024个键和1MB有效载荷。


图11:吞吐量(对数刻度),对于64个客户端会话的不断增加批量大小的数据库,数据库大小为1024个键和1MB有效载荷。

总结

上述对Garnet数据库系统的性能测试,包括基本命令的吞吐量和延迟,以及复杂数据结构的性能。在吞吐量测试中,Garnet在预加载不同大小的数据库后,表现出比Redis或KeyDB更好的可扩展性和更高的吞吐量,无论是在小数据库(1024个键)还是大数据库(256M个键)上。延迟测试显示,Garnet在不同客户端会话数下都保持了较低且稳定的延迟。对于复杂数据结构,如Hyperloglog和Bitmaps,Garnet在处理高争用更新和位操作时,也展现了优越的性能和可扩展性。在所有测试中,Garnet的性能通常优于其他系统,即使在数据库大小、客户端会话数量和负载大小等参数变化时也是如此。

C# .NET以其卓越的性能在技术界备受推崇,这一点从TechEmpower的排名以及众多性能测试中都得到了充分的体现。它拥有一系列高效的编程特性,包括结构体、内存操作、unsafe代码块、Span<T>以及async/await等,这些特性极大地提高了代码的执行效率和开发的灵活性。在过去,使用C# .NET技术的构建的中间件产品并不常见,但.NET的这些先进特性已经证明了其在高性能中间件领域的巨大潜力。随着技术的不断进步,我们有理由相信,C# .NET将继续在这一领域展现出更多的可能性,并推动相关技术的发展和创新。

posted @ 2024-03-19 19:57  InCerry  阅读(9242)  评论(34编辑  收藏  举报