Memcached与Redis在内存机制和集群等方面的比较 - Kablamo


Memcached 创建于 2003 年,在用 C 重写之前用 perl 编写。最初是为 livejournal 创建的,它成为 Web 2.0 时代的 goto 堆栈增强之一。Youtube、Reddit、Facebook、Pinterest、Twitter、Wikipedia 等大型网络资产仍在使用它。
其最初的设计目标是利用 Web 服务器上未使用的 RAM,将它们集中在一起以提供大型高速内存缓存。它对 PHP 网站特别有用,允许您将状态存储在无状态编程环境中,而无需写入数据库。
Redis 于 2009 年创建,在 TCL 中进行原型设计,然后也用 C 重写。最初编写它是为了加速 Salvatore(创建者)的启动。它也被包括 GitHub、Instagram、Stackoverflow 和 Craigslist 在内的大型网络资产所使用。
它的原始设计目标是解决 Salvatores 问题并成为一个持久的内存数据结构存储,因此它支持这么多其他数据类型。它成为 Ruby on Rails 堆栈的一个特别受欢迎的补充,可能是由于出色的库支持和集成。
 
MEMCACHED 如何组织内存
Memcached 将内存组织成页面、slabs块和chunks块。当您启动服务时,您可以定义允许 memcached 使用多少MB内存。Memcached 在启动时酒会分配那么多内存,然后(默认情况下)将其分成 1 MB 的页面。这些页面首次分配时是空的,称为空闲页面。
一旦您向 memcached 添加一个项目,它就会查看您添加的内容的大小(以字节为单位),然后为一个空闲页面分配一个 Slab 类。slab 类确定块的数量,包含进入slab 的项目。默认情况下,最小块大小为 80 字节。
假设您将一个 80 字节的项目添加到新启动的 memcached 实例中。它会选择第一个空闲页面,将其slab类设置为支持80字节的块,然后将您的项目放入该slab的第一个块中。因此,给定默认的 1 MB 页面大小,通过分配其 Slab 类以支持 80 字节块,memcached 将能够将 13,107 个 80 字节项目存储到其中。如果您随后添加另一个 80 字节的项目,它将与之前添加的项目进入同一块。
但是,如果您随后添加了一个 500 字节的项目,memcached 将找到一个新的空闲页面,分配一个新的 Slab 大小并将该项目存储在那里。相同大小的其他项目将填充相同的平板页面直至其限制。
在执行此操作时,memcached 会进行权衡,这会浪费缓存空间。
假设您想在 memcached 中存储一个 70 字节的项目。鉴于 80 字节是最小的块大小,当 memcached 存储该项目时,80 字节中有 10 字节的开销仍然未使用。内存仍然会被占用,但是添加一个 10 字节的项目将不会分配给该空间,而是会使用一个新的块,其中有 70 个未使用的字节。
它是 memcached 为确保内存永远不会碎片而进行的设计权衡。
1 MB 页面/slab 大小确实意味着默认情况下您可以存储在 memcached 中的最大项目大小为 1 MB。虽然你可以改变这一点..
如果您安装了 memcached 并按如下方式访问命令行,您实际上可以查看slab/chunk 分配。

memcached -vv

slab class   1: chunk size  80 perslab 13107
slab class   2: chunk size  100 perslab 10485
slab class   3: chunk size  128 perslab  8192
slab class   4: chunk size  160 perslab  6553
slab class   5: chunk size  200 perslab  5242
slab class   6: chunk size  252 perslab  4161
slab class   7: chunk size  316 perslab  3318
slab class   8: chunk size  396 perslab  2647
slab class   9: chunk size  496 perslab  2114


这样,内存永远不会在 memcached 中碎片化。因此不需要压缩它,因此不需要重新排列项目存储的后台进程。此外,无需清理内存,您只需覆盖即可。
有一些负面影响。第一个是浪费可用空间的内存开销。这一切都归结为 memcached 使用的速度/内存权衡。Memcached 可以并且确实重新分配了slab 类。如果slab被清空,它可以重新分配一个新的类。
 
REDIS 如何组织内存
相比之下,Redis 以存储的每个项目为基础分配内存。当一个 item 进入 Redis 时,它通过 malloc 调用分配内存并将 item 存储在空间中。一段时间后,您最终会在内存中出现“漏洞”。这是一个问题,因为这些孔会占用空间,并且可能无法被 Redis 或操作系统使用,具体取决于您尝试存储的项目的大小。
虽然 Redis 在某些时候确实使用了 malloc ,但现在 Redis 实际使用的是jemalloc。这样做的原因是 jemalloc 虽然峰值性能较低,但内存碎片较少,有助于解决 Redis 遇到的框架内存问题。
但是内存碎片还是会发生,所以Redis也有一个内存碎片整理程序叫做defrag 2,它在后台运行,清理内存并关闭内存漏洞。那些老到记得在 DOS 中运行碎片整理的人会明白这里发生了什么,但简而言之,它移动内存中的项目以尝试创建连续的已用内存块。这允许高效添加,因为它们可以附加在最后,并允许 Redis 将内存释放回操作系统。
你可以自己观察Redis中的碎片情况。

redis-cli info memory

 
内存组织意义
因此,了解 Redis 和 memcached 内存使用之间的区别,让我们看看这意味着什么。
Memcached Slab 一旦分配就永远不会改变它们的大小。这意味着有可能毒害您的 memcached 集群并真正浪费内存。如果您使用大量 1 MB 的项目加载空的 memcached 集群,则所有平板都将分配到该大小。一旦发生这种情况,添加一个 80 KB 的项目最终会使您的 80 KB 项目位于 1 MB 块中,浪费大部分内存。如果 Slab 被清空,则可以重新分配它们,
Memcached 也无法向其运行的操作系统释放内存。Redis 可以,如果您清除所有键,或者如果有足够的项目过期,它将开始释放操作系统可以再次使用的内存。这样做的好处取决于您是否正在运行多租户,这在 2021 年不太可能发生。
Memcached 永远不会使用比分配更多的内存,而 Redis 可能会使用,特别是如果您滥用 TTL 值(将在下面的过期中介绍)。Memcached 永远不会碎片化内存,而 Redis 会,尽管它在一定程度上得到了缓解。通过某些奇怪的使用模式,可能会出现高度碎片化的内存,从而导致原本可以保留的密钥过期。
最后,memcached 每个项目限制为 1 MB(这是可配置的但不建议),而 Redis 限制为每个项目存储 512 MB,但也不建议这样做。
 
缓存过期
因此,让我们简要介绍一些更广为人知的缓存过期技术,即最少使用 (LFU) 和最近最少使用 (LRU)。这很重要,因为 Redis 和 memcached 都使用缓存过期算法,以便知道在缓存已满时需要从缓存中删除哪些数据。
Memcached 使用 LRU。Redis 实际上是可配置的,因此您可以使用 LRU、LFU、随机激励、TTL……您在这里有一些选择,但根据我的经验,大多数人倾向于使用 LRU 或 LFU。
LRU 作为算法的问题可以很简单地解释。考虑一个检查缓存的服务,如果该值不存在,则访问后端以获取该值。

  • MEMCACHED 如何使项目过期

memcached 使用 LRU,并用于实现完美的 LRU 实现,实现本身是一个双向链表,其中的项目在访问、添加或更新时会被撞到头部。
当添加新的东西时,或者如果提取并且它的 TTL(生存时间)年龄表明它应该过期,项目就会被删除。链表本身具有用于任何修改的互斥锁,这意味着当 memcached 是多线程的时,由于互斥争用,它可以有效使用的 CPU 内核数量存在上限。在 memcached 中进行的第一个优化之一是,当请求或更新时,每 60 秒才会触发一次。这可以防止热(经常请求或更新)项目导致大量互斥量争用。
通过这种设计,规模上限约为 8 个工作线程。当您可以为台式机购买 128 线程 CPU 时,这有点问题。
同样,正如我们之前发现的,LRU 不是过期问题的最佳解决方案。因此在 2020 年,memcached 开发人员改变了它的工作方式。
Memcached 现在使用现代 LRU 实现,他们在他们的博客中详细描述了该实现。它实际上类似于Varnish 海量存储引擎方法。
Memcached 现在有多个双向链表,其中一些被认为是热点,这会使它们“安全”不被驱逐,因为这些列表中的项目不会过期。当它们移动到列表的末尾时,它们会降到暖列表,最终会降到冷列表。到达末尾的冷列表中的项目被驱逐并可能被杀死。当发生这种情况时,冷或暖列表顶部的项目可以被碰撞到一个更暖的列表,所有东西都会向下流动。
由于这样的项目在使用时上下移动缓存层变得更安全或更可能被驱逐。获得突发活动的项目在缓存中向上移动,在一段时间内变得安全,并且通常足够长,以便它们再次获得突发请求仍然在缓存中。不常访问的项目退出。
这也意味着 memcached 需要一个称为 LRU 爬虫的后台线程,它正在扫描列表,使 TTL 结束的项目过期并适当地重新调整缓存。由于列表拆分,它还改善了互斥量争用问题,并且 memcached 开发人员声明它现在应该扩展到 48 个线程。
  • REDIS 如何过期 LRU?

在 LRU 模式下使用 Redis 时,需要注意的最重要的一点是它不是 LRU 的教科书完美实现。相反,Redis 实现了一个近似的 LRU 算法。Redis 这样做的原因是为了节省内存使用量。
它的工作方式是 Redis 选择随机数量的键(默认为 5)并驱逐最旧的。从 Redis 3.0 开始,它通过保留来自先前运行的一组良好候选者并将其混合到随机选择中来改进该算法。虽然是近似值,但通过保留一个池并选择 5 个键,它实际上能够实现接近理论上完美的 LRU 驱逐。
您可以通过更改 Redis 配置maxmemory-samples 5值来配置样本数,其中较高的值将使用更多的 CPU,但会产生更接近完美的结果。
对于那些想知道选择随机值是否是一个好主意的人,我建议您阅读Dan Luu 博客上的 Caches: LRU v. random 一文
REDIS 如何使LRU过期?  它只是在访问条目时增加一个值,并通过可配置的衰减时间随着时间的推移降低该值。
您可以调整的可配置值是:

lfu-log-factor 10
lfu-decay-time 1

我不会在这里详细介绍 Redis LFU。LRU 通常不如 LFU 有效。
 
那么哪种到期算法更好呢?
从技术上讲,Redis 仍然可以使活动元素过期。概率定律表明这可能会发生,但也不太可能发生。
相比之下,Memcached 只会随着时间的推移使未使用的项目过期。它仍然是 LRU,但减轻了 LRU 的主要缺陷。Memcached 可以使具有长 TTL 的项目过期,以支持使用短 TTL 保留更频繁访问的项目,尽管实际上这对其现代 LRU 实现来说不是问题。
在实践中,两者都工作得很好,并且由于它们添加了一些调整,它们的实际性能与经典 LRU 或 LFU 算法所建议的不符。
 
内存缓存扩展
Memcached 是多线程的,用于后台进程和事件处理。这意味着您可以扩展您的 memcached 实例,为其提供更多 CPU 内核以提高性能。如前所述,2020 年 8 核是 memcached 的有效扩展限制,但由于新的 LRU 设计,现在是 48 核。
Memcached 从创建的那一刻起就可以通过设计扩展到集群中。memcached 中的集群是一种有趣的野兽,因为它们可以根据您的喜好进行扩展,尽管您最终可能不得不编写自己的客户端库来这样做。
Redis 主要是单线程的,有一些后台任务,例如在后台运行的碎片整理程序,但是对于添加/更新/删除事件会遇到核心事件循环。因此,扩展 Redis 的唯一有效方法是在同一台机器上运行多个实例,尽管这会影响内存效率。
然而,Memcached 可以扩展到集群。集群模式可扩展到 16,384 个节点,但建议坚持少于 1,000。
  
MEMCACHED 集群
Memcached 集群使用无共享方法。Memcached 集群实际上只是多个独立的 memcached 服务,它们甚至可以具有个性化的配置设置或缓存大小!为了使用集群,您需要配置客户端库以了解集群中的每个实例,有一些中间件集群管理工具可以为您实现这一点。
大多数 memcached 客户端都内置了集群功能。那么它们是如何做到的呢?客户端如何知道一个键应该驻留在哪个实例上?
通常的第一个答案是哈希键,然后根据集群中的实例数将模数运算符应用于结果。结果是您应该查找或存储哈希键的实例。请注意,如果这成为 CPU 瓶颈,您可以避免使用模数运算符进行一些操作。
Memcached 通过一致性哈希解决了这个问题:您不必担心实例数量增加或减少。
 
REDIS集群
具有多个领导节点的 Redis 集群,这些节点有一些追随者。节点不会代理发送给它们的命令,而是重定向到正确的节点。客户端应该记住映射,以便他们下次可以正确命中节点。
集群本身是一个全网状集群,这意味着每个节点都知道其他每个节点。由于全网状网络在与高节点数通信时会变得非常活跃,它使用 gossip协议来使信息在网络上传播。因此,一个新节点将向其他几个节点宣布自己,并且他们将传递信息,直到每个人都知道新来者。
Redis 集群实际上相当复杂,值得写一篇博文。在Redis的cluser规范页面给出了一个很好的概述,但并是值得看的,如果你想了解更多信息。
严肃地说,如果您确实需要 Redis 集群,请使用托管服务。它将为您节省大量站起来和维护它的时间和精力。
当项目被添加到 Redis 时,它需要键的 CRC16,然后使用 14 位的结果。这提供了 16,384 个可能的值,Redis 可以使用这些值将项目存储到集群中。这就是其最大集群值的原因。这也意味着节点数量存在硬性限制,尽管官方推荐的最大节点数为 1,000 个节点。每个领导者处理值总数的一部分,称为哈希槽,集群在重新配置时知道每个值应该存储在哪里。由于添加或删除了新节点而重新配置时,哈希槽及其值会在领导者之间移动。
 
REDIS 与 MEMCACHED。我应该使用哪一种?
因此,鉴于已经讨论过的所有内容,我应该使用哪个键值缓存?Redis 还是 Memcached?答案是:视上下文情况而定。
不过,您可能可以遵循一些规则。
使用 memcached 的原因,
  • 如果您需要通过投入更多 CPU 来扩大规模
  • 如果你真的在锤击缓存
  • 如果你缓存了很多非常小的值,最好都是一样的大小
  • 如果您自己运行集群,memcached 更容易设置和维护

否则我会建议使用 Redis 并使用它带来的其他东西,比如它可爱的数据类型、流等等。
然而,关于 memcached 和 redis 有一个未提及的秘密。即两者都是网络缓存。
网络缓存实际上很慢!
在我看来,Stack overflow通过使用 L1/L2 缓存策略正确实现了这一点,Stack overflow博客对此进行了深入介绍。
 简而言之,如果可以,您应该使用您的应用程序可以直接调用的内存作为 1 级 (L1) 缓存,如果该值不存在,请在返回之前访问您的 2 级 (L2) Redis/memcached 集群缓存你的真相来源。如果该值在您的 L1 缓存中,则访问时间的差异是 100 ns 与 1 ms,这要快一个数量级。