HyperAIHyperAI

Command Palette

Search for a command to run...

Garnet:面向应用与服务加速的新一代缓存存储系统

Badrish Chandramouli Lukas M. Maas Vasileios Zois Yoganand Rajasekaran Ted Hart Tal Zaccai Darren Gehring

摘要

近年来,随着数据驱动应用的激增,远程缓存存储系统的重要性急剧上升。以往大多数数据库研究聚焦于传统键值存储系统的各个方面,这类系统以字符串为值,并提供基于 get/set 的简单远程接口。然而,以 Redis 为代表的现代缓存存储系统提供了显著更为丰富的接口,在开发者社区中获得了前所未有的广泛采用。此类缓存存储系统在终端用户应用和大规模服务中的接口与使用场景,对存储、规模、复杂数据类型支持和持久性提出了新的需求。Garnet 是一种新型缓存存储系统,采用 Redis 线路协议以确保兼容性,同时从数据库视角重新思考此类现代缓存存储系统应如何从头设计以满足上述需求。在存储、网络和集群技术栈方面的研究洞见使 Garnet 能够作为无缝替代方案支持完整的 Redis 接口,同时实现更强的数据库特性——线程与节点可扩展性、持久性和事务——以及更优的端到端性能(吞吐量最高提升 100 倍,高百分位延迟降低至 1/4)。这些成果意味着实际应用与服务的端到端成本得以降低。

一句话总结

Microsoft Research 提出 Garnet,一种下一代缓存存储,采用 Redis 线协议以保持兼容性,同时从数据库视角重新设计存储、网络和集群栈,在真实应用和服务中最高实现 100×100\times100× 的吞吐量提升和 4×4\times4× 的高百分位延迟降低。

核心贡献

  • Garnet 是一个兼容 Redis 线协议的缓存存储,从数据库视角重新思考缓存存储设计,采用混合无共享会话设计和共享一切存储引擎,通过支持事务并实现内存优化、可超过内存大小的运行,在复杂堆对象上表达数百个 Redis 命令。

  • Tsavorite 存储引擎提供无闩锁并发、可超过内存大小、内存优化的设计,具有精简式接口、用于事务的乐观监视,以及基于 epoch 的槽位复活;源自 CPR 协议的可调检查点方法能够在不使用预写日志的情况下实现一致检查点,并允许以不同数据丢失容忍度部署为缓存或数据库。

  • Garnet 的集群设计采用基于受 epoch 保护状态机的分片、复制和迁移协议,可作为 RESP 缓存存储的直接替代方案,在真实应用中最高实现 100 倍吞吐量提升,并将高百分位延迟降低至四分之一,从而支持更低的端到端成本,并在 Microsoft 广泛投入生产使用且开源可用。

引言

Redis、Valkey 和 Dragonfly 等缓存存储已成为现代服务的核心,它们通过丰富的远程协议和复杂数据类型支持低延迟缓存、会话状态、限流、排行榜、队列、持久存储以及其他数据密集型工作负载。早期系统大多围绕面向字符串的 get/set 设计、单线程或每核无共享架构,以及内存中的哈希表构建,这可能会在现代多核服务器上限制吞吐量、超内存扩展性、检查点、堆对象支持和可调节的持久性。该研究引入 Garnet,一种 RESP 兼容的下一代缓存存储,并使用无闩锁分层存储引擎 Tsavorite、可插拔网络层、确定性持久性机制以及受 epoch 保护的集群抽象。它在精简存储接口上表达大量 RESP 命令,目标是提供直接替换兼容性,同时实现裸机性能、灵活持久性,以及相比现有缓存存储高得多的吞吐量。

方法

该研究将 Garnet 设计为单节点缓存存储系统,通过网络层、命令解析层和共享存储基底,将远程 RESP 客户端连接到高性能存储引擎。参见框架图。

网络层暴露诸如 TCP 套接字、Unix 域套接字或用户自定义端点之类的端点,包括对低延迟网络栈的支持。远程客户端与这些端点建立会话,并在需要加密通信时执行可选的 TLS 握手。每个会话严格遵循 FIFO,这意味着服务器接收一组按序到达的请求命令,并生成流水线化的响应流。会话内的网络缓冲区由接收它们的线程处理,例如操作系统线程池中的线程,或由内核旁路库选择的线程。该线程可以处理 TLS 解密,然后将数据包传递给会话解析与查询处理逻辑。

传入的数据包包含批量的 RESP 命令。Garnet 首先使用优化的预测性解析器解析每个命令。该解析器设计为优先处理 GET 和 SET 等常见命令,并利用 RESP 语法可预测的结构减少分支预测失败。解析完成后,命令及其参数被路由到对应的命令处理器。这种设计允许单个数据包承载的一批命令无需不必要的线程切换即可被处理,从而改善缓存局部性并降低延迟。

存储操作通过 Tsavorite 处理,Tsavorite 是 Garnet 使用的键值存储层。Garnet 在启动时创建 Tsavorite 实例,并注册支持的命令集合。每个 Garnet 会话持有自己的 Tsavorite 会话,但底层数据存储会跨会话共享。这种架构通过构造保证 Tsavorite 之上各层,如 TLS 处理和解析,具有线程安全性和任务并行性。多个无共享会话在单一共享一切数据存储上运行,使系统获得会话隔离,同时不牺牲性能或可扩展性。

核心存储接口是 RUMDS,代表 Read、Upsert、Modify、Delete 和 Scan。Garnet 查询处理器将 RESP 命令映射到 RUMDS 调用,并将命令参数作为不透明输入传递。这种抽象使 Garnet 能够表达大量现有 RESP 数据命令,同时仍保持足够通用以支持新的命令类别。Read 调用 Reader 回调,将存储值转换为网络响应。Upsert 可以创建或覆盖值,并使用回调进行原地写入或初始记录写入。Modify 通过回调支持原子读取修改写入行为,这些回调决定操作应该创建初始值、原地更新现有内存值,还是在读取较旧状态后复制并更新记录。Delete 插入墓碑记录,或将现有记录标记为已删除。Scan 遍历存活记录,用于执行 DBSIZE 和 SCAN 等操作。

持久性通过将确定性操作写入确定性操作日志(Deterministic Operation Log,DOL)实现。启用持久性时,修改存储的命令会生成 DOL 条目。DOL 记录在 RUMDS 接口处写入日志,而不是写入原始 RESP 命令。每个条目包含足够信息以确定性地重放,包括编码后的命令、参数、数据库版本,以及时间戳或随机值等非确定性输入。DOL 可以写入本地磁盘或分层存储。提交可以周期性地发出,或在单个操作后发出;写穿或确认行为可以根据所需的持久性保证进行调节。

Tsavorite 本身使用双存储布局。字符串存储保存字节序列,包括可以表示为字节数组的数据类型;对象存储保存堆对象,例如复杂数据结构。两个存储中的键都是字符串。每个存储由一个无闩锁并发哈希索引组成,该索引建立在横跨主内存和存储层级的记录日志之上。哈希索引指向记录日志中的逻辑地址,记录按固定大小页面组织。字符串存储在记录内内联保存键和值的字节;对象存储保存指向堆对象的内存指针。当页面被刷新或驱逐时,对象值会被序列化为字节表示以用于磁盘存储。分层通过 IDevice 抽象实现,其中逻辑设备可以协调多个真实存储层级。读取由包含数据的最高层级提供服务,写入可以传播到多个层级。

并发模型由称为 EPSM 的受 epoch 保护状态机支持。EPSM 用于控制平面操作,这些操作必须与正常数据平面工作并发执行,包括哈希表增长、检查点、安全页面刷新、驱逐和键迁移。EPSM 从 REST 阶段开始,并经过一系列阶段。可以注册自定义操作,使其在阶段变更之前执行。阶段变更后,EPSM 将全局 epoch 计数器从 eee 推进到 e+1e+1e+1。执行存储操作的线程进入当前 epoch,读取阶段,并假设在该操作完成且线程离开 epoch 之前阶段保持稳定。一旦所有线程都已越过旧 epoch,EPSM 就可以安全执行下一个阶段操作。该机制使控制平面工作能够在不强制每个应用线程停止的情况下继续推进。实现通过后台任务驱动 EPSM,避免依赖运气不佳的用户线程执行该工作,从而减少延迟尖峰。

EPSM 的阶段转换还支持重要的系统行为。哈希表重新配置可以分配新表,通过屏障阶段同步线程,将记录移动到新表,并在释放旧表后回到 REST。检查点可以将数据库从版本 vvv 移动到版本 v+1v+1v+1,允许旧版本事务排空,同时较新操作继续推进,然后刷新记录日志页面以获得一致快照。槽位迁移可以使用紧凑的布隆过滤器跟踪键批次,并在 queued、migrating、deleting 和 migrated 状态之间转换。这些阶段协调并发读取、写入和事务,无需粗粒度的槽位级锁。下文解释代表性阶段序列,以及存储和日志机制使用的记录头与 DOL 布局。

记录条目以一个紧凑头部开始,其中存储元数据,例如哈希链中前一条记录的逻辑地址、墓碑状态、脏状态、读缓存状态,以及可选字段是否存在。随后是键字节和值字节,以及可选的过期时间戳和实体标签。对于对象存储,内存中的值由指向堆对象的指针表示,磁盘上的值则是序列化后的字节序列。DOL 条目格式同样存储一个紧凑记录,其中包含操作类型、存储版本、会话信息、数据库标识符和操作输入。这些布局旨在最小化空间开销,并支持高效的恢复、复制和并发控制。

Tsavorite 使用无闩锁结构修改结合应用级桶锁进行记录访问,以实现并发 RUMDS 操作。读取沿哈希链进行,从内存开始,必要时移动到磁盘,并使用完整键比较来定位正确记录。Upsert 和 Modify 操作首先搜索可变内存区域。如果目标记录存在,则可以使用原地写入器或更新器回调进行原地更新。如果操作需要新记录,Tsavorite 会使用原子 fetch-and-add 在日志尾部分配空间,通过回调构造记录,并使用 compare-and-swap 尝试安装更新后的哈希链条目。如果 compare-and-swap 失败,已分配空间会被标记为可重用,并重试该操作。额外的更新后回调可用于只有成功结构变更后才应执行的工作,例如写入 DOL。

内存效率通过复活和读缓存处理。过期或已删除的记录会占用内存中的浪费空间,尤其是在原地更新失败或日志增长时。复活机制维护一个可重用记录槽位的空闲列表。当记录可以安全地从哈希链中省略后,它们会被加入空闲列表,未来写入可以重用合适大小的槽位。还可以在哈希索引和主记录日志之间放置一个读缓存,以将频繁访问的记录保留在内存中,减少热工作负载下昂贵的磁盘访问。

多键事务通过将悲观两阶段锁与乐观监视结合来支持。对于标准 RESP 事务、服务端脚本、存储过程和跨键命令,Garnet 首先从排队命令中识别读取集合和写入集合。然后按排序顺序在对应哈希桶上获取共享锁或排他锁,以避免死锁。在获取锁之前,客户端可以选择监视键,记录来自小型共享监视表中的版本号。获取锁之后,Garnet 验证任何被监视的键是否自监视发出以来被修改。如果检测到冲突,事务将被中止。这种混合模型降低了以单键操作为主的工作负载中的阻塞,同时仍提供正确的事务语义。

检查点机制避免了某些早期系统使用的昂贵 process-fork 方法。Garnet 实现了受并发前缀恢复启发的非阻塞快照机制。EPSM 协调从旧数据库版本 vvv 到新数据库版本 v+1v+1v+1 的转换。在排空阶段,来自任一版本的事务可能并发执行。将事务分配给 vvv 还是 v+1v+1v+1 的决策可以在获取锁之后做出,这减少了暂停活动事务的需要。一旦所有 vvv 事务完成,系统会刷新 vvv 记录,以获得前缀一致快照。该设计通过让状态机与各存储解耦,同时扩展到字符串存储和对象存储,使两个存储都能在同一版本转换下经过检查点阶段。

在集群模式下,Garnet 将键空间划分为哈希槽,并将槽位分配给主节点。客户端可以通过任意节点访问数据,每个节点使用槽位分配信息来服务请求,或将客户端重定向到正确的所有者。主节点拥有槽位子集并服务读写请求,而副本节点附加到主节点并服务读取请求。集群配置变更通过 gossip 协议传播。在横向扩展期间,槽位迁移可以通过服务器到服务器通信执行,而不是由客户端驱动迁移。键集合以小批次迁移,EPSM 协调并发访问,使操作在安全的情况下继续,同时在关键传输和删除阶段防止不一致读取或写入。

复制使用 DOL 作为主节点和副本节点之间的同步机制。副本可以根据主节点是否仍保留所需 DOL 地址空间,开始完整或部分同步。如果需要完整快照,主节点可以向副本传输检查点。恢复完成后,主节点将 DOL 记录流式传输到副本。副本将这些记录保存在本地,并异步向自己的 Tsavorite 实例应用 Upsert 和 Modify 操作。由于 DOL 条目包含数据库版本,副本可以跳过已经包含在恢复检查点中的记录,确保每个操作只应用一次。流式快照变体允许主节点通过扫描不可变记录日志区域,然后扫描可变记录日志区域,生成一致的时间点快照,同时不阻塞正常操作,也不强制写入磁盘。

实验

实验在 Azure 虚拟机上,使用标准 RESP 基准测试和受 YCSB 启发的流量,在独立和集群工作负载下评估 Garnet 与 Valkey、KeyDB 和 Dragonfly 的对比。结果显示,Garnet 在键值、批量、大载荷和复杂数据操作中提供显著更高的吞吐量、更低且更稳定的延迟,以及更好的可扩展性;同时,其架构在性能上优于基于锁、工作队列和进程分片的替代方案。在持久性和集群设置中,Garnet 的检查点接近可用存储写入上限,并且相比其他系统能更快完成槽位迁移和复制同步,同时具有更高的吞吐量稳定性。

对比显示,Garnet 在所比较的缓存存储中提供最高的归一化节点吞吐量和最低延迟,同时提供更广泛的功能集合。其优势在检查点、集群迁移和复制中尤为明显;随着副本数量增加,它仍能维持更高吞吐量,并实现比其他系统更低的尾延迟。与 Redis、Valkey、KeyDB 和 Dragonfly 相比,Garnet 具有远高于这些系统的吞吐量;Garnet 提供比 Redis、Valkey、KeyDB 和 Dragonfly 更低的批处理延迟;Garnet 独特地结合内核旁路、分层存储、解耦存储和可调持久性;Garnet 检查点数据比 Valkey、KeyDB 和 Dragonfly 更快;Garnet 迁移集群槽位比 Valkey 和 Dragonfly 更快;Garnet 在多个副本的复制期间保持更高写入吞吐量。

RESP 示例展示原始字符串和列表上的基本操作,包括读取、赋值、条件写入,以及带有过期时间的值。它们还展示一项跨键列表操作,将元素从一个键移动到另一个键。这些命令模式支持多键事务和存储层中的原子跨键行为。原始字符串命令涵盖常见缓存操作,例如读取值、设置值、仅在满足条件时写入,以及带有过期时间的赋值。列表命令包括在源键和目标键之间移动元素,说明跨多个键实例的事务操作。

映射显示,RESP 命令可以使用少量 RUMDS 操作和应用回调表达。GET、SET 和 SET-if-exists 依赖 Read、Upsert 或 Modify 操作,并由回调处理响应格式化、初始写入和条件更新。相关评估报告 Garnet 相比其他系统在检查点、迁移和复制方面具有良好性能。命令处理拆分为前端操作和存储回调。不同 RESP 命令可以通过定制回调共享同一存储接口。被评估的 Garnet 实现能够在检查点、迁移和复制工作负载下保持强劲性能。

实验跨基本吞吐量与延迟行为,以及副本感知工作负载(例如检查点、集群迁移和复制)评估 Garnet,对比 Redis、Valkey、KeyDB 和 Dragonfly。定性结果表明,Garnet 始终提供更高的归一化节点吞吐量、更低的批处理延迟和更强的尾延迟表现,尤其在副本数量增加时更为明显。额外的命令映射评估显示,常见 RESP 字符串和列表操作可以通过少量存储操作和回调表达,支持条件写入、过期时间和原子跨键列表移动。


用 AI 构建 AI

从创意到上线——通过免费 AI 协同编码、开箱即用的环境和最优惠的 GPU 价格,加速您的 AI 开发。

AI 协同编码
开箱即用的 GPU
最优定价

HyperAI Newsletters

订阅我们的最新资讯
我们会在北京时间 每周一的上午九点 向您的邮箱投递本周内的最新更新
邮件发送服务由 MailChimp 提供