超越 RAM 的缓存:NVMe 的案例 - Dormando (2018 年 6 月 12 日)
从堆栈的每一层到缓存架构都体现了性能和成本之间的隐含权衡。然而,这些权衡一直在不断变化:随着存储技术的进步、工作负载模式的变化或硬件供求的波动,新的拐点可能会出现。
在这篇文章中,我们探讨了 RAM 价格上涨对缓存系统设计的影响。虽然 RAM 一直都很昂贵,但 DRAM 价格在 2017 年上涨了 50% 以上,高密度 RAM 涉及多插槽 NUMA 机器,从而增加了功耗和总体成本。与此同时,闪存和傲腾等替代存储技术不断改进。它们具有专门的硬件接口、一致的性能、高密度和相对较低的成本。虽然将缓存从 RAM 卸载到 NVMe 或 NVM 设备上越来越具有经济效益,但对性能的影响尚未得到广泛理解。
我们将在 Memcached 的背景下探讨这些设计影响,Memcached 是一种分布式、简单、以缓存为中心的键值存储。有关快速概述,请参阅 关于页面 或 故事教程。
- Memcached 是一个基于 RAM 的键值缓存。它充当一个大型分布式哈希表,数据生命周期由 LRU 控制。最旧的未触碰数据将被逐出以腾出空间用于新数据。
- 非常低的延迟(亚毫秒级)和高吞吐量非常重要。页面可能会多次从 memcached 请求数据,这会导致时间迅速累积。
- Memcached 通过减少对后端系统的查询来大幅降低成本。无论是具有闪存/磁盘驱动器的数据库,还是 CPU 密集型代码(如模板或渲染)。
Memcached 具有一个 名为 extstore 的存储系统,它允许将部分数据保留在磁盘上以供“不太常使用”的键使用,从而释放 RAM。有关其工作原理的完整细分,请参阅链接,但简而言之:键保留在 RAM 中,而值可以拆分到磁盘。最近和频繁访问的键仍然会在 RAM 中保留其值。
此测试是在 Accelerate with Optane 的帮助下完成的,他们提供了硬件和指导。还要感谢 Netflix 采用 extstore,以及他们所有宝贵的反馈。
缓存 RAM 分解
例如,一个 1TB 的数据库在给定时间段(比如 4 小时)内可能只有 20% 的数据是“活跃”的。如果你想缓存所有活跃数据,你可能需要 200G 的内存。在这 200G 的内存中,可能只有 20% 被高度利用。
在使用的缓存内存中,只有 10% 的内存可能负责 90% 的缓存命中率。剩下的 90% 的内存只占 10% 的命中率。
然而,如果你减少 90% 的内存使用量,你的未命中率至少会翻倍,从而使数据库的负载翻倍。根据你的后端系统性能,丢失一些内存会
使后端成本翻倍。
分解内存中的项目,我们可能会发现绝大多数(在上述情况下为 97.2%)只存在于 30% 的内存中。一小部分较大的但仍然
重要的项目占用了另外的 70%。
即使更大的项目也可以很快消耗内存。
请记住,这些较大的项目可能占网络利用率的很大一部分。一个 8k 的请求与 80 多个小的请求占用相同的带宽。
extstore 如何帮助?通过将较大的、最近使用较少的项目的 value 从 key 中分离并指向磁盘,我们可以节省大部分未使用的内存。根据用例,你可以
- 减少总内存:如果你有很多较大的项目,可以将 100G 减少到 30G 或更少。
- 在相同内存下提高命中率:将大型项目移到磁盘,缓存较小的项目的较长尾部,提高命中率并降低后端成本。
- 减少服务器数量:如果你有足够的网络带宽(并且可以处理服务器故障!),你可以减少缓存集群的大小。
- 增加总缓存:每台服务器可以轻松增加数百 GB 的缓存容量。
- 缓存以前过于昂贵的对象:创建新的池,包含更大的对象,缓存来自大数据存储、机器学习训练数据等的大型预计算数据。
其他工作负载变化可以吗?在上面的例子中,缓存命中率是均匀分布的。这个理论系统具有 400,000 的 IO 限制,这应该与高端 SSD 或 Optane 驱动器类似。在这种情况下,不能依赖内存来饱和网络。
在 400,000 IOPS 下,只需要 3072 字节的平均值就可以饱和 10G 网卡。8192 字节用于 25G。在设计良好的集群中,需要额外的空间来应对增长、使用峰值或池中的故障。这意味着平均值可以低至 1024 字节,但是,在 1024b(假设每个 key 有 100 字节的开销)下,extstore 只能将磁盘上的存储量提高到内存的 10 倍。
需要仔细的容量规划
- 可以容忍多少台机器丢失?每台死掉的机器都会带走一部分缓存。
- 需要多少网络带宽?减少服务器数量会使网络更加密集。
- 需要多少 IOPS?大多数访问针对的是最近的数据,减少了对磁盘的依赖。
- 需要什么样的延迟保证?如果基于缓存的磁盘查找仍然比后端快很多
- 项目存活多长时间?SSD 只能承受一定数量的写入,然后就会烧毁。
并非所有人的工作负载都与外部存储兼容。在使用磁盘作为缓存之前,请仔细评估 RAM 在缓存池中的使用方式。如果您只有小型项目、较短的 TTL 或高写入速率,RAM 仍然会更便宜。可以通过监控 SSD 的“每天写入次数”来计算这一点。如果一个 1TB 设备可以在 24 小时内写入 2TB 数据的情况下存活 5 年,那么它的容忍度为 2 DWPD。傲腾的容忍度很高,为 30DWPD,而高端闪存盘的容忍度为 3-6DWPD。
测试设置
测试是在一台英特尔至强机器上进行的,该机器拥有 32 个核心、192GB 内存、一个 4TB SSD 和 3 个 750GB 的傲腾驱动器。测试期间只使用了一个傲腾驱动器。截至撰写本文时,extstore 只能与一个驱动器一起使用,这种配置反映了大多数用户的配置。
- mc-crusher 用于运行测试。具体来说,是 第三个标签,其中包含 test-optane 脚本。
- mc-crusher 主要设计为尽可能快地运行:它不解析响应,在每个系统调用中尽可能多地堆叠查询,并且不尝试计时任何操作。在此测试中,它针对 localhost 运行,但从未使用超过一个 CPU 内核。
-
test-optane 脚本 特别描述了测试中使用的配置。Memcached 被配置为使用 32 个工作线程(机器有 64 个核心,带有超线程)。
- mc-crusher 中的“balloon”程序用于占用 125GB 内存,并将 1 亿个键加载到 memcached 中,以避免 extstore 仅仅使用缓冲池。
在每次测试运行期间,mc-crusher 客户端的数量以及服务器中 extstore IO 线程的数量都会发生变化。IO 线程太少会导致设备无法饱和,而 IO 线程太多会导致设备过载并可能造成队列。
每次测试在预热后运行一分钟。
延迟和吞吐量测量
由于 mc-crusher 不计时结果,因此使用两个脚本生成结果数据
- bench-sample:定期对 memcached 运行“stats”命令,使用其计数器来确定平均吞吐量。数据每隔几秒钟采样一次,并检查了显著的标准差。
- latency-sample:一个脚本,它假装是一个阻塞的 memcached 客户端,并在一段时间内“采样”请求,同时 bench-sample 正在运行。这用于避免诸如“第 95 个百分位数”之类的陷阱,这些陷阱会删除异常值或分组,从而导致误导性的结果。
对于每个测试,都提供了延迟样本的完整细分。采样以每毫秒一次的最大速率进行。
注意:没有使用事件循环,以避免必须确定时间流逝,因为如果一堆事件同时发生,则需要确定等待处理的时间。
测试
进行了三个一般测试
- ASCII 多获取:此模式允许 extstore 使用最少的包来生成响应,以及在内部大量管道化请求。低延迟设备可以更容易地使用此测试达到更高的吞吐量。
- 管道化获取:许多获取请求被堆叠到同一个包中,但 extstore 必须独立地服务每个请求。在这些测试中,extstore 能够轻松地饱和操作系统服务缓冲 I/O 的能力(kswapd 内核线程已达到最大值),但延迟图表显示 optane 能够将延迟保持在闪存驱动器的十分之一。
- 在更高的客户端负载下,管道化获取可能看起来很奇怪:这需要进一步研究,但可能是由内部排队引起的。由于 mc-crusher 非常激进,optane 驱动器能够使用更少的 I/O 线程和 crusher 客户端来饱和系统。在生产工作负载中,optane 将提供更一致的低延迟服务。
- 多获取 + 管道化设置:前两个工作负载是只读的。在此测试中,设置也以大约 1/3 到 1/5 的速率对 memcached 进行。extstore 在读取发生的同时将数据刷新到驱动器。同样,optane 表现出色。
结果
不幸的是,图表中有一些波动;这是由于在测试期间留下了过少的可用 RAM。optane 的性能是一致的,而操作系统难以跟上。
作为参考:针对此确切配置的 memcached(32 个线程等)的纯 RAM 多获取负载测试会导致 **每秒 1800 万个键**。更人为的基准测试使具有多个内核的服务器达到了 **每秒 5000 万个键**。
**使用少量 extstore I/O 线程**,Optane 驱动器能够更接近于饱和 I/O 限制:4 个线程,4 个客户端:230k Optane,40k SSD。延迟细分显示 SSD 通常在等待时间上高出一个数量级,Optane 保持在 10us 桶中,而 SSD 在 100us 中,滑入 1ms。
**使用大量 extstore I/O 线程**,底层操作系统变得饱和,导致 Optane 图表中的波动和排队。同时,SSD 继续从额外的线程资源中获益,这些资源是克服闪存带来的额外延迟所必需的。
对于许多工作负载,SSD 和 Optane 都完全可行。如果大部分读取仍然来自 RAM,而 extstore 用于服务仅大型对象的尾部,它们都能将响应时间保持在 1ms 以内。
**如果你想突破 extstore 的界限**,像 Optane 这样的驱动器将大有帮助
- 高写入容忍度与缓存工作负载相得益彰
- 极低的延迟有助于平滑从磁盘请求缓存数据的权衡。
- 目前小尺寸是有利的:extstore 要求每个磁盘上的值都需要 RAM。375G 到 1TB 的磁盘在特定机器上需要的 RAM 要少得多,而 2TB+ 可能过于密集,无法安全地进行故障转移或避免 NIC 饱和。
测试类型
SSD IO 线程
傲腾 IO 线程
学习
- extstore 刷新器是一个后台线程,与管理 LRU 的代码相结合。在基准测试期间,设置会持续发送到服务器,这会导致 extstore 刷新出现饥饿现象。在生产环境中,插入 memcached 的速率往往呈波浪形,即使在毫秒级范围内也是如此,因此它可以跟上。这将拥有更一致的性能,因为它拥有自己的线程。
- 延迟采样很困难。当前脚本提供了有用的数据,但更好的程序会每毫秒将一个请求放到一个阻塞线程池中,让我们能够确定服务器是否暂停或某些请求是否只是速度慢。还可以保存并绘制每个样本的完整计时。这将可视化来自底层操作系统或驱动器的响应聚类。
- 缓冲 I/O 有局限性。这在之前是已知的,大多数工作负载的顺序是每秒数十万次操作或更少,其中大多数将针对 RAM 而不是 extstore。我们目前专注于稳定性,但最终直接 I/O 和异步 I/O 将能够在高负载下更好地利用设备。
- extstore 的分桶可以实现傲腾与传统闪存的非常有趣的混合。在内部,extstore 将数据组织成磁盘空间的“页面”(通常为 64M)。新项目被聚类到特定的页面。具有较短 TTL 的项目可以聚类在一起。存活页面压缩的项目也会聚类在一起,这减少了随着时间的推移对压缩的需求。所有新项目和/或短 TTL 项目可以放在 375G 傲腾驱动器上,而压缩的项目可以放在 1TB 闪存驱动器上,从而提供更大的成本节约。
结论
由于成本原因目前无法实现的工作负载现在可以实现了。大多数包含混合数据大小和大型池的工作负载可以显著降低成本。
extstore 每个磁盘上的项目都需要 RAM。此图表假设每个项目 100 字节的开销(键 + 元数据),可视化了随着项目大小的增加,RAM 开销如何下降。
DRAM 成本是傲腾的 3-4 倍,是 SSD 的 4-8 倍,具体取决于驱动器。
随着缓存从 RAM 转移到傲腾(或闪存),纯粹用于 RAM 的支出可以降至 1/3。
减少 RAM 减少了对多插槽服务器的依赖以获得非常高的 RAM 密度,NUMA 兼容机器通常是必要的。这些机器有多个插槽、多个 CPU,一半的 RAM 连接到每个 CPU。由于 memcached 效率很高,因此您可以同时减少 RAM,以及减少一半的主板/CPU 甚至电源成本,一旦 RAM 减少。对于特定工作负载,高达 80% 的成本降低是合理的。
高速、低延迟 SSD 开启了数据库和缓存设计的新时代。我们展示了各种用例的高性能数据,既可以降低成本,也可以扩展缓存的使用。