使用 SSD 进行应用数据缓存——Moneta 项目:下一代 EVCache 以实现更好的成本优化

随着  今年早些时候 Netflix 的全球扩张,数据的全球扩张也随之而来在 Active-Active 项目之后, 现在有了 N+1 架构,最新的个性化数据需要随时随地为任何地区的任何成员服务。缓存在成员个性化的持久性故事中起着至关重要的作用,如 之前的博客文章中所述:

Netflix 架构有两个主要组件。第一个是在 AWS 云上运行的控制平面,用于为会员注册、浏览和播放体验提供通用、可扩展的计算。第二个是数据平面,称为 Open Connect,它是我们的全球视频交付网络。本博客介绍了我们如何将 SSD 的强大功能和经济性引入 EVCache——Netflix 使用的主要缓存系统,用于在 AWS 上的控制平面中运行的应用程序。

EVCache 的主要用例之一是为我们超过 8100 万成员中的每个成员充当个性化数据的全球复制存储。除了保存这些数据之外,EVCache 在 Netflix 中还扮演着各种角色,包括充当标准工作集缓存,用于存储订阅者信息等内容。但它最大的作用是个性化。为来自任何地方的任何人提供服务意味着我们必须为我们运营的三个区域中的每个区域的每个成员保存所有个性化数据。这可以在所有 AWS 区域实现一致的体验,并让我们能够在区域中断期间或常规期间轻松转移流量流量整形练习以平衡负载。我们在 之前的博客文章中详细讨论了用于实现这一点的复制系统:

在稳定状态下,我们的地区往往会一遍又一遍地看到相同的成员。对于我们的会员来说,区域之间的切换并不是很常见的现象。尽管他们的数据在所有三个区域的 RAM 中,但每个成员只有一个区域经常使用。由此推断,我们可以看到每个区域对于这些类型的缓存都有不同的工作集。一小部分是热数据,其余的是冷数据。

除了热/冷数据分离之外,将所有这些数据保存在内存中的成本随着我们的会员群而增长。同样,不同的 A/B 测试和其他内部更改可以添加更多数据。对于我们的工作组成员,我们已经拥有数十亿个密钥,而且这个数字只会增长。我们面临着在平衡成本的同时继续支持 Netflix 用例的挑战。为了应对这一挑战,我们引入了一种同时使用 RAM 和 SSD 的多级缓存方案。

利用这种全球请求分配和成本优化的 EVCache 项目被称为 Moneta,以拉丁记忆女神的名字命名,而 Juno Moneta 是 Juno 的资金保护者。

当前架构

我们将讨论 EVCache 服务器的当前架构,然后讨论它是如何发展以支持 SSD 的。

下图显示了 EVCache 的典型部署以及单个客户端实例与服务器之间的关系。EVCache 的客户端将连接到多个 EVCache 服务器集群。在一个区域中,我们有整个数据集的多个副本,由 AWS 可用区分隔。虚线框描绘了区域内副本,每个副本都有完整的数据副本并充当一个单元。我们将这些副本作为单独的 AWS Auto Scaling 组进行管理。有些缓存每个区域有 2 个副本,有些有很多。在可预见的未来,这种高级架构对我们仍然有效,并且不会改变。每个客户端都连接到他们自己区域中所有区域中的所有服务器。写入被发送到所有副本,读取更喜欢拓扑关闭的服务器来处理读取请求。要查看有关 EVCache 架构的更多详细信息,请参阅 我们最初的公告博客文章

在过去几年中发展的服务器是几个进程的集合,其中有两个主要进程:股票 Memcached,一种流行且经过实战考验的内存键值存储,以及 Prana,Netflix 边车进程。Prana 是服务器与 Netflix 生态系统其他部分的挂钩,该生态系统仍然主要基于 Java。客户端直接连接到每个服务器上运行的 Memcached 进程。服务器是独立的,不相互通信。

优化

作为 Netflix 云的最大子系统之一,我们处于独特的位置,可以在我们的大部分云足迹中应用优化。将所有缓存数据保存在内存中的成本随着我们的会员群而增长。一天的个性化批处理的单个阶段的输出可以将超过 5 TB 的数据加载到其专用的 EVCache 集群中。存储这些数据的成本乘以我们存储的全球数据副本的数量。如前所述,不同的 A/B 测试和其他内部更改可以添加更多数据。对于我们的工作组成员而言,我们今天拥有数十亿个密钥,而且这个数字只会增长。

为了利用我们在不同区域观察到的不同数据访问模式,我们构建了一个系统,将热数据存储在 RAM 中,将冷数据存储在磁盘上。这是一个经典的两级缓存架构(其中 L1 是 RAM,L2 是磁盘),但是 Netflix 内部的工程师已经开始依赖 EVCache 的一致、低延迟性能。我们的要求是尽可能降低延迟,使用更平衡的(昂贵的)RAM,利用成本更低的 SSD 存储,同时仍然提供客户期望的低延迟。

内存中 EVCache 集群在 AWS r3 系列实例类型上运行,这些实例类型针对大内存占用进行了优化。通过迁移到 i2 系列实例,我们可以获得 10 倍于我们在 r3 系列上的快速 SSD 存储量(从 r3.xlarge 到 i2.xlarge 的 80 → 800GB),具有相同的 RAM 和 CPU。我们还将实例大小降级为更小的内存。将这两者结合起来,我们就有可能在我们的数千台服务器上大幅优化成本。

Moneta架构

Moneta 项目为 EVCache 服务器引入了两个新进程:Rend 和 Mnemonic。Rend 是一个用 Go 编写的高性能代理,以 Netflix 用例作为开发的主要驱动力。Mnemonic 是基于 RocksDB的磁盘支持键值存储。Mnemonic 重用了处理协议解析(用于讲 Memcached 协议)、连接管理和并行锁定(用于正确性)的 Rend 服务器组件。这三台服务器实际上都使用 Memcached 文本和二进制协议,因此三台服务器之间的客户端交互具有相同的语义。在调试或进行一致性检查时,我们会利用这一点。

以前客户端直接连接到 Memcached,现在它们连接到 Rend。从那里,Rend 将负责 Memcached 和 Mnemonic 之间的 L1/L2 交互。即使在不使用 Mnemonic 的服务器上,Rend 仍然提供我们以前无法从 Memcached 获得的有价值的服务器端指标,例如服务器端请求延迟。Rend 引入的延迟仅与 Memcached 结合使用,平均只有几十微秒。

作为重新设计的一部分,我们可以将这三个流程整合在一起。我们选择在每台服务器上运行三个独立的进程来保持关注点分离。这种设置在服务器上提供了更好的数据持久性。如果 Rend 崩溃,Memcached 和 Mnemonic 中的数据仍然完好无损。一旦客户重新连接到恢复的 Rend 进程,服务器就能够为客户请求提供服务。如果 Memcached 崩溃,我们会丢失工作集,但 L2(助记符)中的数据仍然可用。一旦再次请求数据,它将回到热集中并像以前一样提供服务。如果 Mnemonic 崩溃,它不会丢失所有数据,而只会丢失最近写入的一小部分数据。即使它确实丢失了数据,至少我们的热数据仍在 RAM 中,可供实际使用该服务的用户使用。

Rend

如上所述,Rend 在服务器上实际存储数据的其他两个进程之前充当代理。它是一个使用二进制和文本 Memcached 协议的高性能服务器。它是用 Go 编写的,并且严重依赖 goroutine 和其他语言原语来处理并发。该项目是完全开源的, 可以在 Github 上找到使用 Go 的决定是经过深思熟虑的,因为我们需要比 Java 具有更低延迟的东西(垃圾收集暂停是一个问题),并且对开发人员来说比 C 更高效,同时还要处理数以万计的客户端连接。Go 非常适合这个空间。

Rend 负责管理盒子上的 L1 和 L2 缓存之间的关系。它在内部有几个不同的策略适用于不同的用例。它还具有在将数据插入 Memcached 时将数据切割成固定大小的块的功能,以避免 Memcached 内的内存分配方案的病态行为。这种服务器端分块正在取代我们的客户端版本,并且已经显示出前景。到目前为止,它的读取速度提高了两倍,写入速度提高了 30 倍。幸运的是,从 1.4.25 开始,Memcached 对以前导致问题的不良客户端行为变得更加灵活。将来我们可能会放弃分块功能,因为如果将数据从 L1 中逐出,我们可以依赖 L2 来获取数据。

设计

Rend 的设计是模块化的,以允许可配置的功能。在内部,有几层:连接管理、服务器循环、特定于协议的代码、请求编排和后端处理程序。旁边是一个自定义指标包,它使我们的 sidecar Prana 能够轮询指标信息,同时不会太打扰。Rend 还附带了一个测试客户端库,它有一个单独的代码库。这极大地有助于发现协议错误或其他错误,例如未对齐、未刷新的缓冲区和未完成的响应。

Rend 的设计允许通过实现接口和构造函数来插入不同的后端。为了证明这种设计,一位熟悉代码库的工程师用了不到一天的时间来学习 LMDB 并将其集成为存储后端。这个实验的代码可以在 https://github.com/Netflix/rend-lmdb找到。

生产中的使用

对于 Moneta 服务最好的缓存,有几个不同类别的客户端可供单个服务器服务。一类是热路径中的在线流量,为访问会员请求个性化数据。另一个是来自产生个性化数据的离线和近线系统的流量。这些通常在一夜之间大批量运行,并连续数小时连续写入。

模块化允许我们的默认实现通过将数据直接插入 L2 并智能替换 L1 中的热数据来优化我们的夜间批处理计算,而不是让这些写入在夜间预计算期间破坏我们的 L1 缓存。来自其他区域的复制数据也可以直接插入到 L2 中,因为从其他区域复制的数据在其目标区域中不太可能是“热”的。下图显示了一个 Rend 进程中的多个开放端口,它们都连接到后备存储。借助 Rend 的模块化,只需几行代码,就可以很容易地在不同的端口上引入另一台服务器来处理批处理和复制流量。

性能

Rend 本身的吞吐量非常高。在单独测试 Rend 时,我们在最大化 CPU 之前始终达到网络带宽或数据包处理限制。对于不需要命中后备存储的请求,单个服务器已被驱动到每秒 286 万个请求。这是一个原始但不切实际的数字。使用 Memcached 作为唯一的后备存储,Rend 可以在我们测试的最大实例上同时支持每秒 225k 的插入和每秒 200k 的读取。一个 i2.xlarge 实例配置为同时使用 L1 和 L2(内存和磁盘)和数据分块,用作我们生产集群的标准实例,每秒可以执行 22k 插入(仅使用集合),每秒执行 21k 读取(仅获取),如果两者同时完成,则大约每秒 10k 组和 10k 次获取。这些是我们生产流量的下限,因为测试负载由随机密钥组成,因此在访问期间没有提供数据局部性优势。真实流量会比随机键更频繁地访问 L1 缓存。

作为服务器端应用程序,Rend 在 EVCache 服务器上解锁了各种未来智能的可能性。此外,底层存储与用于通信的协议完全断开。根据 Netflix 的需要,我们可以将 L2 存储移出盒子,将 L1 Memcached 替换为另一个存储,或者更改服务器逻辑以添加全局锁定或一致性。这些不是计划中的项目,但现在我们在服务器上运行了自定义代码,它们是可能的。

Mnemonic

Mnemonic 是我们基于 RocksDB 的 L2 解决方案。它将数据存储在磁盘上。Mnemonic 的协议解析、连接管理和并发控制都由支持 Rend 的同一个库管理。Mnemonic 是另一个插入 Rend 服务器的后端。Mnemonic 项目中的本机库公开了一个由 Rend 处理程序使用的自定义 C API。

Mnemonic 有趣的部分在于包装 RocksDB 的 C++ 核心层。Mnemonic 处理 Memcached 样式的请求,实现每个所需的操作以符合 Memcached 的行为,包括 TTL 支持。它包括一个更重要的功能:它在本地系统上的多个 RocksDB 数据库中对请求进行分片,以减少每个 RocksDB 实例的工作量。原因将在下一节中探讨。

RocksDB

在查看了一些有效访问 SSD 的选项之后,我们选择了 RocksDB,这是一种使用 Log Structured Merge Tree 数据结构设计的嵌入式键值存储。写操作首先插入到内存中的数据结构(memtable)中,该结构在满时刷新到磁盘。当刷新到磁盘时,memtable 成为一个不可变的 SST 文件。这使得大多数写入对 SSD 是连续的,从而减少了 SSD 必须执行的内部垃圾收集量,从而改善了长时间运行的实例的延迟,同时也减少了磨损。

每个单独的 RocksDB 实例在后台完成的一种工作包括压缩。我们最初使用 Level 样式的压缩配置,这是跨多个数据库分片请求的主要原因。然而,当我们使用生产数据和类似生产的流量评估这种压缩配置时,我们发现压缩导致 SSD 的大量额外读/写流量,增加了超过我们认为可接受的延迟。SSD 读取流量有时超过 200MB/秒。我们的评估流量包括写入操作数量很高的长时间,模拟每日批量计算过程。在此期间,RocksDB 不断将新的 L0 记录移动到更高级别,从而导致非常高的写入放大。

为了避免这种开销,我们切换到 FIFO 风格的压缩。在这种配置中,没有进行真正的压缩操作。根据数据库的最大大小删除旧的 SST 文件。记录保留在 0 级磁盘上,因此记录仅按时间跨多个 SST 文件排序。此配置的缺点是读取操作必须在确定缺少密钥之前按时间倒序检查每个 SST 文件。此检查通常不需要磁盘读取,因为 RocksDB 布隆过滤器可防止大部分查询需要在每个 SST 上进行磁盘访问。但是,SST 文件的数量会导致布隆过滤器集的整体有效性低于正常的 Level 样式压缩。

性能

使用最终的压缩配置再次重新运行我们的评估测试,我们能够在预计算负载期间实现读取查询的 99% 延迟约 9 毫秒。预计算加载完成后,在相同级别的读取流量下,第 99 个百分位读取延迟减少到约 600μs。所有这些测试都是在没有 Memcached 和 RocksDB 块缓存的情况下运行的。

为了让这个解决方案适用于更多不同的用途,我们需要减少每个查询需要检查的 SST 文件的数量。我们正在探索诸如 RocksDB 的 Universal 样式压缩或我们自己的自定义压缩等选项,我们可以更好地控制压缩率,从而降低传入和传出 SSD 的数据量。

结论

我们正在分阶段推出我们的解决方案。Rend 目前正在生产中,为我们一些最重要的个性化数据集提供服务。早期的数字显示运行速度更快,可靠性更高,因为我们不太容易出现临时网络问题。我们正在为我们的早期采用者部署 Mnemonic (L2) 后端。虽然我们仍在调整系统的过程中,但结果看起来很有希望,有可能节省大量成本,同时仍然允许 EVCache 一直为用户提供的易用性和速度。

生产部署是一段相当长的旅程,还有很多工作要做:广泛部署、监控、优化、冲洗和重复。EVCache 服务器的新架构使我们能够继续以重要的方式进行创新。如果您想帮助解决云架构中的这个或类似的大问题,请 加入我们

Scott Mansfield  ( sgmansfield )、  Vu Tuan Nguyen、  Sridhar Enugula、  Shashi Madappa 代表 EVCache 团队