所在的位置: js >> js介绍 >> Memcached架构

Memcached架构

北京中科白癜风医院热心公益 https://baike.baidu.com/item/青少年白癜风防治援助项目/22039760

Mmcachd是一种内存中的键值存储,最初是用Prl编写的,后来重写为C语言。它受到Facbook、Ntflix和Wikipdia等公司的欢迎,因为它简单易用。

虽然当谈论到软件描述时,“简单”这个词已经失去了意义,但我认为Mmcachd是少数真正简单的软件之一。Mmcachd没有像持久性或丰富的数据类型这样的花哨特性。甚至分布式缓存也是客户端的责任,而不是Mmcachd服务器的责任。

Mmcachd的后端只有一个任务,即内存中的键值存储。

缓存是逃避行为

Mmcachd被用作缓存来存储耗时的数据库查询或昂贵的HTTP响应。虽然缓存对于可扩展性至关重要,但它应该被视为最后的选择,让我来解释一下。

我认为为每个遇到的耗时查询都运行一个缓存是一种逃避行为。我相信理解性能下降的原因才是关键,否则缓存只是一个权宜之计。如果是数据库查询,看看执行计划,是否需要索引?是否有很多逻辑读取,你是否可以重写查询或包含额外的预测性过滤器以最小化搜索空间?

如果是RPC调用,考虑一下为什么调用本来就是冗余的,你是否可以在客户端消除这些调用。经常情况下,当使用不当时,库和框架会发出一系列查询。这也是为什么黑盒子必须被理解的原因。

在尝试了所有的调优选项后,可能需要使用缓存,这时候Mmcachd是最适合的选择。

Mmcachd架构

在本文中,我将深入探讨Mmcachd的架构以及开发人员为保持其简单性和功能削减所做的努力。我将对某些组件发表自己的观点,认为它们应该是可选的。

本文将涵盖以下主题:

内存管理

线程

最近最少使用(LRU)

读/写

冲突

分布式缓存

演示

什么是Mmcachd?

Mmcachd是一个用作缓存的键值存储。它被设计为简单,因此在某些方面有一些限制。这些限制也可以看作是特性,因为它们使Mmcachd变得透明。

在Mmcachd中,键是字符串,长度限制为个字符。值可以是任意类型,但默认情况下,值的大小限制为MB。键还具有过期日期或存活时间(TTL)。然而,不应依赖于此,因为最近最少使用(LRU)算法可能在访问之前删除过期的键。Mmcachd适用于缓存昂贵的查询,但不应依赖于其作为持久性或可靠存储。在构建应用程序时,始终假设Mmcachd不具备你所需的功能。为最坏情况做计划,希望发生最好的情况。

内存管理

当分配诸如数组、字符串或整数等项目时,它们通常会随机分配到进程内存中的某些位置。这会在物理内存中留下一些未使用的小内存间隙,这个问题被称为碎片化。

当已分配项目之间的间隙继续增加时,就会发生碎片化。这使得很难找到足够大的连续内存块来存储新项目。从技术上讲,可能有足够的内存来存储项目,但内存分散在物理空间的各个位置。

这是否意味着如果没有连续内存块存在,项目无法存储?实际上并非如此,借助虚拟内存的帮助,操作系统给出了应用程序正在使用连续内存块的错觉。在幕后,该块被映射到物理内存中的小区域。

碎片化会导致程序运行变慢,因为系统需要组装内存碎片。虚拟内存映射的开销以及获取本应是单个内存块的多次I/O操作的成本相对较高。这就是为什么我们要尽量避免内存碎片化。

Mmcachd通过预分配MB大小的内存页面来避免碎片化,这也是为什么默认情况下值被限制为MB。

操作系统认为Mmcachd正在使用分配的内存,但实际上Mmcachd尚未在其中存储任何内容。当创建新项目时,Mmcachd会将项目写入分配的页面,强制使项目彼此相邻。这通过将内存管理移至Mmcachd而不是操作系统来避免碎片化。

页面被分割成相等大小的块(Chunk)。每个块的大小由slabclass确定。slabclass定义了块的大小,例如Slabclass的块大小为72字节,而slabclass43的块大小为MB。

项目由键、值和一些元数据组成,并存储在块中。例如,如果项目的大小为40字节,将使用整个块来存储该项目。最接近40字节项目的块大小为72字节,即slabclass,在块中会有32字节的未使用空间。因此,客户端应该聪明地选择适合块的大小的项目,以尽量减少未使用空间。

Mmcachd试图通过将项目放入最合适的slabclass来尽量减少未使用空间。每个slabclass都有多个页面。例如,对于slabclass,每页有4,个块,因为每个块的大小为72字节。如果一个项目小于等于72字节,它将完美地适应该块。但如果项目更大,比如KB,它不适合slabclass。因此,Mmcachd会寻找适合该项目的slabclass。最接近MB块大小的是slabclass43,项目将放在该块中。整个项目适应单个页面。

注意我们不需要为项目分配内存,因为内存已经预先分配好了。

我们来看一个新的例子,假设有一个大小为40字节的新项目,但为该slabclass分配的所有页面都已满,因此无法插入该项目。

Mmcachd通过分配一个新页面并将项目存储在空闲块中来处理这种情况。

线程

Mmcachd接受远程客户端连接,因此必须具备网络功能。Mmcachd使用TCP作为其主要的传输协议。虽然也支持UDP,但默认情况下已禁用,因为在年发生过一次称为反射攻击的攻击事件。

Mmcachd监听线程创建一个TCP套接字来监听2端口。它有一个线程旋转并监听传入的连接。该线程创建套接字并接受传入的连接。

然后,Mmcachd将连接分配给线程池中的一个线程。当建立新连接时,Mmcachd从池中分配一个线程,并将连接的文件描述符分配给该线程。该工作线程现在负责从连接中读取数据。

如果向连接发送数据流或请求获取一个键,则该线程轮询文件描述符以读取请求。每个线程可以承载一个或多个连接,并且线程池中的线程数量可以进行配置。

多年前,线程管理更为关键,因为异步工作负载在年并不像年那样丰富。您看,当线程从连接中读取数据时,这个操作在年代初曾是阻塞的。线程在获取数据之前无法执行其他任何操作。工程师们意识到这是不可扩展的,于是异步I/O应运而生。几乎所有的读取调用现在都是异步的,这意味着线程可以在一个连接上调用读取操作,然后继续处理其他连接。

然而,在Mmcachd中,线程管理仍然很重要,因为读取/写入涉及到一些CPU时间,例如哈希和LRU计算。如果一个线程为所有连接执行这些工作,可能无法扩展。

LRU(最近最少使用)

内存的问题在于它是有限的。即使你设置了合理的过期时间,如果存储了大量的键,内存最终会被填满。当内存已满时,你会怎么做呢?

嗯,作为架构师,你有两个选择。

阻止新插入并向客户端返回错误以释放一些项目

释放旧的未使用项目

Mmcachd选择了后者。这就是我希望他们将其作为一个选项的地方。就好像我在与设计师们争论该怎么做。使用LRU使Mmcachd变得复杂,并剥夺了其纯粹的简洁性。理智的声音已经失去了,取而代之的是客户端的便利性。唉,事实就是这样。

当内存已满时,Mmcachd释放所有长时间未使用的内存中的项目。这也是为什么Mmcachd被称为瞬态内存的另一个原因。即使你将某个键的过期时间设置为一小时,你也不能指望在一小时过期之前该键一直存在。它可以在任何时候释放,这是Mmcachd的另一个限制(或特性!)。

Mmcachd使用一种称为链表LRU(最近最少使用)的数据结构,在内存已满时释放项目。Mmcachd键值存储中的每个项目都在链表中,每个slabclass都有自己的LRU。

如果访问了一个项目,它会从当前位置移动到链表的头部。每次访问项目时都会重复这个过程。结果是,不经常使用的项目将被推到链表尾部,并在内存已满时最终被删除。

这是LRU缓存的整体图示。我从阅读源代码和Mmcachd文档中推导出来的。在图示中,我们有页面和块。每个块都包含在一个LRU缓存中,具有头指针和尾指针,头部和尾部之间的每个项目都彼此链接。

虽然LRU是有用的,但从性能上来说,它也可能非常昂贵。为了维护LRU所必需的锁可能会降低吞吐量并增加应用程序的复杂性。如果LRU可以选择禁用,Mmcachd可以保持简单。这将允许用户分配一定数量的内存给Mmcachd,而无需担心管理LRU的开销。释放项目的责任将变为客户端的责任。

LRU锁定

没有两个线程可以同时更新相同的数据结构。为了解决这个问题,需要更新内存中任何数据结构的线程必须获得互斥锁,而其他线程则等待该互斥锁释放。这是基本的锁定模型,适用于所有应用程序。Mmcachd与LRU数据结构一样,也使用了这种模型。

最初的Mmcachd设计中有一个全局锁,所有操作和LRU管理都是串行的,使得多线程效果不佳。因此,客户端不能同时访问两个不同的项目,所有读取操作都是串行的。

Mmcachd通过更新锁定模型,针对每个slabclass引入了LRU,从而解决了这个问题。这意味着客户端可以同时访问来自不同slabclass的两个项目,而无需等待。然而,当访问来自同一个slabclass的两个项目时,它们仍然是串行的,因为需要更新LRU。后来,通过将LRU更新最小化为每60秒一次,改进了这个问题,允许访问多个项目。然而,这还不够好。

在年,Mmcachd彻底重新设计了LRU,通过按温度将其分解为每个slabclass的子LRU,从而显著减少了锁定并改善了性能,但在同一温度下,锁定仍然存在。

现在你知道为什么我希望LRU是一个可选项了。

读取和写入读取

让我们通过一个在Mmcachd中的读取示例来说明。为了确定给定键的项在内存中的位置,Mmcachd使用哈希表。哈希表是一种关联数组。关联数组的优点是它是连续的,这意味着如果你有一个包含个元素的数组,访问元素7、2、24、33或的速度是一样的,因为你知道索引。一旦你知道了索引,你就可以立即跳转到内存中的那个位置。

在哈希表中,我们没有索引,而是有一个键。关键的技巧是将键转换为索引,然后访问元素。我们取键并计算其哈希值,然后对哈希表大小取模。让我们举个例子。

要读取键“tst”,我们对“tst”进行哈希计算,然后对哈希表数组的大小进行模运算。这将给我们一个介于0和N-之间的数字。这个数字可以用来索引哈希表,并获取键的值。所有这些操作的时间复杂度为O()。

键的值将带您到特定slabclass上的页面。当我们读取值时,我们通过将项推送到LRU链表的头部来更新slabclass的LRU。这需要对LRU数据结构进行互斥锁定,以防止多个线程破坏LRU。

注意,如果尝试读取相同slabclass上的两个项,则读取操作是串行的,因为需要锁定LRU。

当读取键“tst”并获取到项“d”时,LRU被更新,使得“d”现在位于链表的头部。

那么如果我们读取键"buzz",指向项"c"呢?LRU被更新,使得"c"现在紧随"d"之后成为新的头部。

写入

如果我们需要写入一个新值为44字节的键,首先需要计算哈希值,并找到其在哈希表中的索引。如果索引位置为空,将创建一个新的指针,并分配一个slabclass和一个chunk。然后,将项放置在内存中,并将该chunk适应于相应的slabclass。

冲突

由于哈希将键映射到固定大小,两个键可能会哈希到相同的索引位置,从而引发冲突。假设我要写入一个名为“Nani”的新键。“Nani”的哈希值与另一个已存在的键发生了冲突。

为了解决这个问题,Mmcachd使哈希表中的每个索引映射到一个项目链,而不是直接映射到项目。我们将键“Nani”添加到该链中,该链现在有两个项目。当读取键时,需要查找链中的所有项目来确定哪个与所需键匹配,这在最坏情况下的时间复杂度为O(N)。以下是一个示例:

Mmcachd测量这些链的增长情况。如果增长过于剧烈,读取性能可能会受到影响。要读取一个键,必须查找整个冲突链以找到实际的键。冲突链越长,读取速度越慢。

如果读取性能开始下降,Mmcachd会进行哈希调整并重新分配所有项,以使结构扁平化。

分布式缓存

Mmcachd服务器是隔离的,服务器之间不会互相通信。我非常喜欢这种设计的简洁和优雅之处。如果要分发你的键,客户端必须自行处理,你可以构建自己的Mmcachd客户端来实现这一点。

服务器之间的通信会显著复杂化架构,就像在ZooKpr中一样。

Tlnt演示

在这个演示中,我们将启动一系列MmcachdDockr实例。您需要安装Dockr并拥有Dockr账号,因为Mmcachd镜像是受账号保护的。一旦您完成了这些步骤,就可以下载镜像,并且可以启动任意数量的Mmcachd实例。需要注意的是,Mmcachd不支持身份验证,因此您需要自行实现身份验证。

首先,我们要启动一个包含Mmcachd实例的Dockr容器。可以使用以下命令来完成。我们可以添加-d以避免阻塞终端。

dockrrun--nammm-p2:2-dmmcachd

运行dockrps来确保镜像正在运行。

让我们使用tlnt进行测试。运行tlnthussinmac2,将hussinmac替换为您的主机名。一旦连接成功,您就可以发出诸如stats之类的命令。

我们还可以在此控制台中设置键值对。例如,stfoo。foo是键的名称,0是标志,是TTL(生存时间),2是字符数。按下回车键后,它会提示您输入foo键的值。键的值应与在st命令中设置的字符数完全匹配。可以使用gt命令读取键值对:gtfoo。可以使用dlt命令删除键值对:dltfoo。

这个演示的有趣部分是关于Mmcachd的架构。您会注意到,一旦添加了foo键,您可以输入statsslabs来获取slab统计信息。您将看到有一个slab类(查看STAT,其中表示slab类)。此外,您还可以看到关于块、页面、命中次数、使用的总内存量等很多有趣的统计信息...

使用Nod.js进行分布式Mmcachd

为了测试分布式缓存,Nod.js提供了一个智能客户端,支持Mmcachd实例的连接池。首先,让我们从Nod.js连接到一个单独的Mmcachd实例,并写入一些键。

创建一个名为nodmm的文件夹,并进入该文件夹。然后,使用npminit-y初始化项目。接下来,我们将创建一个名为indx.js的文件,并填入以下内容:请注意:您需要将hussinmac替换为您的主机名。

constMEMCACHED=rquir("mmcachd");constsrvrPool=nwMEMCACHED(["hussinmac:2"]);functionrun(){[,2,3,4,5,6,7,8,9].forEach(a=srvrPool.st("foo"+a,"bar"+a,,rr=consol.log(rr)))}run();

这段代码使用了Mmcachd的Nod.js客户端,并初始化了一个Mmcachd服务器池。然后,它将格式为foo:bar、foo2:bar2等的九个键值对添加到Mmcachd服务器池中。

在运行脚本之前,我们需要使用npminstallmmcachd安装Mmcachd的Nod.js客户端。然后,使用以下命令运行代码:

nodindx.js

脚本运行后,您应该能够使用tlnt连接到Mmcachd服务器,并使用gtfoo、gtfoo2等命令读取键值对。所有的键值对将存储在2服务器中。

现在,让我们启动更多的Mmcachd实例:

dockrrun--nammm2-p22:2-dmmcachddockrrun--nammm3-p23:2-dmmcachddockrrun--nammm4-p24:2-dmmcachd

在启动容器后,您需要将这些服务器添加到Nod.js脚本中的服务器池中:

constMEMCACHED=rquir("mmcachd");constsrvrPool=nwMEMCACHED(["hussinmac:2","hussinmac:22","hussinmac:23","hussinmac:24"]);functionrun(){[,2,3,4,5,6,7,8,9].forEach(a=srvrPool.st("foo"+a,"bar"+a,,rr=consol.log(rr)))}run();

运行此脚本后,您将注意到所有键值对将分布到所有4个服务器上。尝试一下!使用tlnt连接到Mmcachd服务器,并运行gtfoo、gtfoo2等命令,查看键值对在哪个服务器上。这是因为Nod.js客户端根据自己的哈希算法选择池中的一个服务器来存储键值对。

对于读取操作,Nod.js的Mmcachd客户端会进行哈希计算,找到包含该键的服务器,并向该服务器发出命令。请注意,这种哈希计算与Mmcachd执行的哈希计算是不同的。

constMEMCACHED=rquir("mmcachd");constsrvrPool=nwMEMCACHED(["hussinmac:2","hussinmac:22","hussinmac:23","hussinmac:24"]);functionrun(){[,2,3,4,5,6,7,8,9].forEach(a=srvrPool.st("foo"+a,"bar"+a,,rr=consol.log(rr)))}functionrad(){[,2,3,4,5,6,7,8,9].forEach(a=srvrPool.gt("foo"+a,(rr,data)=consol.log(data)))}rad();

如果您使用nodindx.js运行此脚本,您会注意到尽管并非所有Mmcachd服务器都拥有全部答案,但所有值都会显示出来。

总结

在本文中,我们讨论了Mmcachd的架构。我们讨论了内存管理的重要性,以及使用slabs和pags来分配内存以避免碎片化的方法。我们还讨论了LRU(最近最少使用)算法,我个人认为它应该是用户可以选择禁用的选项。接下来,我们讨论了线程对于处理大量连接时如何提高性能的重要性。我们通过一些Mmcachd的读写示例来说明这一点,并介绍了锁定对它们的影响。最后,我们讨论了使用Nod进行分布式缓存架构的演示。

如果你喜欢我的文章,点赞,


转载请注明:http://www.aierlanlan.com/rzfs/7372.html