Memcached(理论篇)
1.Memcached 简介
Memcached 是一个开源的,支持高性能,高并发的分布式内存缓存系统,由 C 语言编写,总共 2000 多行代码。从软件名称上看,前 3 个字符 Mem
就是内存的意思,而接下来的后面 5 个字符 cache
就是缓存的意思,最后一个字符 d
,是 daemon
的意思,代表的是服务器端守护进程模式服务。
Memcached 服务分为服务器端和客户端两部分。其中,服务器端软件的名字形如 Memcached-1.4.24.tar.gz
,客户端软件的名字形如 Memcache-2.25.tar.gz
。
Memcached 软件诞生于 2003 年,最初由 LiveJournal 的 Brad Fitzpatrick 开发完成。Memcache 是整个项目的名称,而 Memcached 是服务器端的主程序名,因其协议简单,应用部署方便,且支持高并发,因此被互联网企业广泛使用,直到现在仍然如此。其官方网站地址:http://memcached.org/。
Memcached 的作用
传统场景中,多数 Web 应用都将数据保存到关系型数据库中(如 MySQL),Web 服务器从中读取数据并在浏览器中显示。但随着数据量增大、访问集中,关系型数据库的负担就会出现加重,响应缓慢,导致网站打开延迟等问题,影响用户体验。
这时就需要 Memcached 软件出马了。使用 Memcached 的主要目的是,通过在自身内存中缓存关系型数据库的查询结果,减少数据库被访问的次数,以提高动态 Web 应用的速度,提高网站架构的并发能力和可扩展性。
Memcached 服务的运行原理是通过在事先规划好的系统内存空间中临时缓存数据库中的各类数据,以达到减少前端业务服务对数据库的直接高并发访问,从而提升大规模网站集群中动态服务的并发访问能力。
生产场景的 Memcached 服务一般被用来保存网站中经常被读取的对象或数据,就像我们的客户端浏览器也会把经常访问的网页缓存起来一样,通过内存缓存来存取对象或数据要比磁盘存取快很多,因为磁盘是机械的。因此,在当今的 IT 企业中,Memcached 的应用范围很广泛。
互联网常见内存缓存服务软件
2.Memcached 的用户与应用场景
2.1 Memcached 常见用途工作流程
Memcached 是一种内存缓存软件,在工作中经常用来缓存数据库的查询数据,数据被缓存在事先与分配的 Memcached 管理的内存中,可以通过 API 或命令的方式存取内存中缓存的这些数据,Memcached 服务内存中缓存的数据就像一张巨大的 Hash 表,每条数据都是以 Key-Value 对的形式存在。
2.2 网站读取 Memcached 数据时工作流程
从逻辑上来说,当程序访问后端数据库获取数据时会优先访问 Memcached 缓存,如果缓存中有数据就直接返回给客户端用户,如果没有合适的数据(没有命中),再去后端的数据库读取数据,读取到需要的数据后,就会把数据返回给客户端,同时还会把读取到的数据缓存到 Memcached 内存中,这样客户端用户再次请求相同的数据时就会直接读取 Memcached 缓存的数据了,这就大大地减轻了后端数据库的压力,并提高了整个网站的响应速度,提升了用户体验。
如上图所示:使用 Memcached 缓存查询的数据来减少数据库压力的具体工作流程如下:
- Web 程序首先检查客户端请求的数据是否在 Memcached 缓存中存在,如果存在,直接把请求的数据返回给客户端,此时不再请求后端数据库。
- 如果请求的数据在 Memcached 缓存中不存在,则程序会去请求数据库服务,把从数据库中取到的数据返回给客户端,同时把新取到的数据缓存一份到 Memcached 缓存中。
2.3 网站更新 Memcached 数据时的工作流程
(1)当程序更新或删除数据时,会首先处理后端数据库中的数据。
(2)在处理后端数据库中数据的同时,也会通知 Memcached,告诉它对应的旧数据失效,从而保证 Memcached 中缓存的数据始终和数据库中一致,这个 数据一致性 非常重要,也是大型网站分布式缓存集群最头疼的问题所在。
(3)如果是在高并发读写场合,除了要程序通知 Memcached 过期的缓存失效外,还可能要通过相关机制,例如在数据库上部署相关程序(如在数据库中设置触发器使用 UDFs),实现当数据库有更新时就把数据更新到 Memcached 服务中,这样一来,客户端在访问新数据时,因预先把更新过的数据库数据复制到 Memcached 中缓存起来了,所以可以减少第一次查询数据库带来的访问压力,提升 Memcached 中缓存的命中率,甚至新浪门户还会把持久化存储 Redis 做成 MySQL 数据库的从库,实现真正的主从复制。
下图为 Memcached 网站作为缓存应用更新数据的流程
下图为 Memcached 服务作为缓存应用通过相关软件更新数据的流程
2.4 Memcached 在企业中的应用场景
2.4.1 作为数据库的查询数据缓存
(1)完整数据缓存
例如:电商的商品分类功能不是经常变动的,因此可以事先放到 Memcached 里,然后再对外提供数据访问。这个过程被称之为 数据预热。此时只需读取缓存,无需读取数据库就能得到 Memcached 缓存里的所有商品分类数据了,所以数据库的访问压力就会大大降低。
为什么商品分类数据可以事先放在缓存里呢?因为商品分类几乎都是由内部人员管理的,如果需要更新数据,更新数据库后,就可以把数据同时更新到 Memcached 里。如果把商品分类数据做成静态化文件,然后通过在前端 Web 缓存或者使用 CDN 加速效果更好。
(2)热点数据缓存
热点数据缓存一般是用于由用户更新的商品,例如淘宝的卖家,在卖家新增商品后,网站程序就会把商品写入后端数据库,同时把这部分数据,放入 Memcached 内存中,下一次访问这个商品的请求就直接从 Memcached 内存中取走了。这种方法用来缓存网站热点的数据,即利用 Memcached 缓存经常被访问的数据。
注:这个过程可以通过程序实现,也可以在数据库上安装相关软件进行设置,直接由数据库把内容更新到 Memcached 中,就相当于 Memcached 是 MySQL 的从库一样。
- 如果碰到电商双 11,秒杀高并发的业务场景,必须要事先预热各种缓存,包括前端的 Web 缓存和后端的数据库缓存。也就是先把数据放入内存预热,然后逐步动态更新。此时,会先读取缓存,如果缓存里没有对应的数据,再去读取数据库,然后把读到的数据放入缓存。如果数据库里的数据更新,需要同时触发缓存更新,防止给用户过期的数据,当然对于百万级别并发还有很多其他的工作要做。
- 绝大多数的网站动态数据都是保存在数据库当中的,每次频繁地存取数据库,会导致数据库性能急剧下降,无法同时服务更多的用户(比如 MySQL 特别频繁的锁表就存在此问题),那么,就可以让 Memcached 来分担数据库的压力。增加 Memcached 服务的好处除了可以分担数据库的压力以外,还包括无须改动整个网站架构,只须简单地修改下程序逻辑,让程序先读取 Memcached 缓存查询数据即可,当然别忘了,更新数据时也要更新 Memcached 缓存。
2.4.2 作为集群节点的 Session 会话共享存储
即把客户端用户请求多个前端应用服务集群产生的 Session 会话信息,统一存储到一个 Memcached 缓存中。由于 Session 会话数据是存储在内存中的,所以速度很快。
下图为 Memcached 服务在企业集群架构中的常见工作位置:
3.Memcached 的特点与工作机制
3.1 Memcached 的特点
- 协议简单。Memcached 的协议实现很简单,采用的是基于文本行的协议,能通过 telnet / nc 等命令直接操作 Memcached 服务存储数据。
- 支持 epoll / kqueue 异步 I/O 模型,使用 libevent 作为事件处理通知机制。简单的说,libevent 是一套利用 C 开发的程序库,它将 BSD 系统的 kqueue,Linux 系统的 epoll 等事件处理功能封装成一个接口,确保即使服务器端的连接数增加也能发挥很好的性能。Memcached 就是利用这个 libevent 库进行异步事件处理的。
- 采用 key / value 键值数据类型。被缓存的数据以 key / value 键值形式存在。
- 全内存缓存,效率高。Memcached 管理内存的方式非常高效,即全部的数据都存放于 Memcached 服务事先分配好的内存中,无持久化存储的设计,和系统的物理内存一样,当重启系统或 Memcached 服务时,Memcached 内存中的数据就会丢失。如果希望重启后,数据依然能保留,那么就可以采用 Redis 这样的持久性内存缓存系统。
- 当内存中缓存的数据容量达到服务启动时设定的内存值时,就会自动使用 LRU 算法(最近最少被使用的)删除过期的缓存数据。也可以在存放数据时对存储的数据设置过期时间,这样过期后数据就自动被清除,Memcached 服务本身不会监控数据过期,而是在访问的时候查看 key 的时间戳判断是否过期。
- 可支持分布式集群。Memcached 没有像 MySQL 那样的主从复制方式,分布式 Memcached 集群的不同服务器之间是互不通信的,每一个节点都独立存取数据,并且数据内容也不一样。通过对 Web 应用端的程序设计或者通过支持 Hash 算法的负载均衡软件,可以让 Memcached 支持大规模海量分布式缓存集群应用。
3.2 Memcached 工作原理与机制
3.2.1 Memcached 工作原理
Memcached 是一套类似 C/S 模式架构的软件,在服务器端启动 Memcached 服务守护进程,可以指定监听本地的IP地址,端口号,并发访问连接数,以及分配了多少内存来处理客户端请求。
3.2.2 Socket 时间处理机制
Memcached 软件是由 C 语言来实现的,全部代码仅有 2000 多行,采用的是异步 epoll / kqueue 非阻塞 I/O 网络模型,其实现方式是基于异步的 libevent 事件单进程,单线程模式。使用 libevent 作为事件通知机制,应用程序端通过指定服务器的 IP 地址及端口,就可以连接 Memcached 服务进行通信。
3.2.3 数据存储机制
需要被缓存的数据以 key / value 键值对的形式保存在服务器端预分配的内存区中,每个被缓存的数据都有唯一的标识 key,操作 Memcached 中的数据就是通过这个唯一标识的 key 进行的。缓存到 Memcached 中的数据仅放置在 Memcached 服务预分配的内存中,而非存储在 Memcached 服务器所在的磁盘上,因此存取速度非常快。
由于 Memcached 服务自身没有对缓存的数据进行持久化存储的涉及,因此,在服务器端的 Memcached 服务进程重启之后,存储在内存中的这些数据就会丢失。且当内存中缓存的数据容量达到启动时设定的内存值时,也会自动使用 LRU 算法删除过期的数据。
开发 Memcached 的初衷仅是通过内存缓存提升访问效率,并没有过多考虑数据的永久存储问题。因此,如果使用 Memcached 作为缓存数据服务,要考虑数据丢失后带来的问题,例如:是否可以重新生成数据,还有,在高并发场合下缓存宕机或重启会不会导致大量请求直接到数据库,导致数据库无法承受,最终导致网站架构雪崩等。
3.2.4 内存管理机制
- 采用 Slab 内存分配机制
- 采用 LRU 对象清除机制
- 采用 Hash 机制快速检索 Item
3.2.5 多线程处理机制
- 多线程处理时采用的是 pthread(POSIX)线程模式。
- 若要激活多线程,可在编译时指定:
./configure --enable-threads
- 锁机制不够完善
- 负载过重时,可以开启多线程(-t 线程数为CPU核数)
3.3 Memcached 预热理念及集群节点的正确重启方法
当需要大面积重启 Memcached 时,首先要在前端控制网站入口的访问流量,然后,重启 Memcached 集群并进行数据预热,所有数据都预热完毕之后,再逐步放开前端网站入口的流量。
为了满足 Memcached 服务数据可以持久化存储的需求,在较早时期,新浪网基于 Memcached 服务开发了一款 NoSQL 软件,名字为 MemcacheDB,实现了在缓存的基础上增加了持久存储的特性,不过目前逐步被更优秀的 Redis 软件取代了。
如果由于机房断电或者搬迁服务器集群到新机房,那么启动集群服务器时,一定要从网站集群的后端依次往前端开启,特别是开启 Memcached 缓存服务器时要提前预热。
4.Memcached 内存管理
4.1 Memcached 内存管理机制解析
(1)malloc 内存管理机制
在讲解 Memcached 内存管理机制前,先来了解 malloc。malloc 的全称是 memory allocation
,即动态内存分配,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态分配内存。
早期的 Memcached 内存管理是通过 malloc 分配内存实现的,使用完后通过 free 来回收内存。这种方式容易产生内存碎片并降低操作系统对内存的管理效率。因此,也会加重操作系统内存管理器的负担,最坏的情况下,会导致操作系统比 Memcached 进程本身还慢,为了解决上述问题,Slab Allocator
内存分配机制就诞生了。
(2)Slab 内存管理机制
现在的 Memcached 是利用 Slab Allocation
机制来分配和管理内存的,过程如下:
提前将大内存分配大小为 1MB 的若干个 slab,然后针对每个 slab 再进行小对象填充,这个小对象称为 chunk,避免大量重复的初始化和清理,减轻了内存管理器的负担。
Slab Allocation
内存分配的原理是按照预先规定的大小,将分配给 Memcached 服务的内存预先分割成特定长度的内存块(chunk),再把尺寸相同的内存块(chunk)分成组(chunks slab class),这些内存块不会释放,可以重复利用,如下图所示。
新增数据对象存储时。因 Memcached 服务器中保存着 slab 内空闲 chunk 的列表,他会根据该列表选择 chunk,然后将数据缓存于其中。当有数据存入时,Memcached 根据接收到的数据大小,选择最适合数据大小的 slab 分配一个能存下这个数据的最小内存块(chunk)。例如:有 100 字节的一个数据,就会被分配存入下面 112 字节的一个内存块中,这样会有 12 字节被浪费,这部分空间就不能被使用了,这也是 Slab Allocator 机制的一个缺点。
(3)Slab Allocation的主要术语
Slab Allocation 主要术语 | 注解说明 |
---|---|
slab class | 内存区类别(48 byte - 1MB) |
slab | 动态创建的实际内存区,即分配给 Slab 的内存空间,默认是 1MB。分配给 Slab 之后根据 slab 的大小切分成 chunk。slab 默认大小为 1048576 byte (1MB)。大于 1MB 的数据会忽略 |
slab classid | Slab class 的 ID |
chunk | 数据区块,固定大小,chunk 初始大小,1.4 版本中是 48 bytes |
item | 实际存储在 chunk 中的数据项 |
(4)Slab内存管理机制特点
提前分配大内存 Slab 1MB,再进行小对象填充 chunk。
避免大量重复的初始化和清理,减轻内存管理器负担。
避免频繁 malloc / free 内存分配导致的碎片
MC 内存管理机制小结
MC 的早期内存管理机制为 malloc(动态内存分配)
malloc(动态内存分配)产生内存碎片,导致操作系统性能急剧下降。
Slab 内存分配机制可以解决内存碎片的问题
Memcached 服务的内存预先分割成特定长度的内存块,称为 chunk,用于缓存数据的内存空间或内存块,相当于磁盘的 block,只不过磁盘的每一个 block 都是相等的,而 chunk 只有在同一个 Slab Class 内才是相等的。
Slab Class 指特定大小(1MB)的包含多个 chunk 的集合或组,一个 Memcached 包含多个 Slab Class,每个 Slab Class 包含多个相同大小的 chunk。
Slab机制也有缺点,例如,Chunk的空间会有浪费等。
4.2 Memcached Slab Allocator内存管理机制的缺点
(1)chunk 存储 item 浪费空间
Slab Allocator 解决了当初的内存碎片问题,但新的机制也给 Memcached 带来了新的问题。这个问题就是,由于分配的是特定长度的内存,因此无法有效利用分配的内存。例如,将 100 字节的数据缓存到 128 字节的 chunk 中,剩余的 28 字节就浪费了,如下图所示:
避免浪费内存的办法是,预先计算出应用存入的数据大小,或把同一业务类型的数据存入一个 Memcached 服务器中,确保存入的数据大小相对均匀,这样就可以减少内存的浪费。
还有一种办法是,在启动时指定 -f
参数,能在某种程度上控制内存组之间的大小差异。在应用中使用 Memcached 时,通常可以不重新设置这个参数,即使用默认值 1.25 进行部署即可。如果想优化 Memcached 对内存的使用,可以考虑重新计算数据的预期平均长度,调整这个参数来获得合适的设置值,命令如下:
-f <factor>chunk size growth factor (default:1.25)!
(2)Slab 尾部剩余空间
假设在 classid=40 中,两个 chunk 占用了 1009384 byte,那么就有 1048576 - 1009384 = 39192 byte 会被浪费掉。解决办法:规划 slab 大小 = chunk 大小 * n 整数倍。
4.3 使用 Growth Factor 对 Slab Allocator 内存管理机制调优
在启动 Memcached 时指定 Growth Factor 因子(通过 -f
选项),就可以在某种程度上控制每组 Slab 之间的差异。默认值 1.25。但是,在该选项出现之前,这个因子曾经被固定为 2。让我们用以前的设置,以 verbose 模式启动 Memcached 试试看:
memcached -f 2 w
启动后的 verbose
slab class 1:chunk size 128 perslab 8192
slab class 2:chunk size 256 perslab 4096
slab class 3:chunk size 512 perslab 2048
slab class 4:chunk size 1024 perslab 1024
slab class 5:chunk size 2048 perslab 512
slab class 6:chunk size 4096 perslab 256
slab class 7:chunk size 8192 perslab 128
slab class 8:chunk size 16384 perslab 64
slab class 9:chunk size 32768 perslab 32
slab class 10:chunk size 65536 perslab 16
slab class 11:chunk size 131072 perslab 8
slab class 12:chunk size 262144 perslab 4
slab class 13:chunk size 524288 perslab 2
可见,从 128 字节的组开始,组的大小依次增大为原来的 2 倍。这样设置的问题是,Slab 之间的差别比较大,有些情况下就相当浪费内存。因此,为尽量减少内存浪费,两年前追加了 growth factor 这个选项。
来看看现在的默认设置(f=1.25
)时的输出:
slab class 1:chunk size 88 perslab 11915 <---88*11915=1048520
slab class 2:chunk size 112 perslab 9362
slab class 3:chunk size 144 perslab 7281
slab class 4:chunk size 184 perslab 5698
slab class 5:chunk size 232 perslab 4519
slab class 6:chunk size 296 perslab 3542
slab class 7:chunk size 376 perslab 2788
slab class 8:chunk size 472 perslab 2221
slab class 9:chunk size 592 perslab 1771
slab class 10:chunk size 744 perslab 1409 <---744*1409=1048520
此时每个 Slab 的大小是一样的,即 1048520,1MB。组间的差距比因子为 2 时小得多,可见,这个值越小,Slab 中 chunk size 的差距就越小,内存浪费也就越小。可见,默认值 1.25 更适合缓存几百字节的对象。从上面的输出结果来看,可能会觉得有些计算误差,这些误差是为了保持字节数的对齐而故意设置的。
当使用 Memcached 或是直接使用默认值进行部署时,最好是重新计算一下数据的预期平均长度,调整 growth factor,以获得最恰当的设置。内存是珍贵的资源,浪费就太可惜了。
4.4 Memcached 的检测过期与删除机制
(1)Memcached 懒惰检测对象过期机制
首先要知道,Memcached 不会主动检测 item 对象是否过期,而是在进行 get 操作时检查 item 对象是否过期以及是否应该删除!
因为不会主动检测 item 对象是否过期,自然也就不会释放已分配给对象的内存空间了,除非为添加的数据设定过期时间或内存缓存满了,在数据过期后,客户端不能通过 key 取出它的值,其存储空间将被重新利用。
Memcached 使用的这种策略为 懒惰检测对象过期策略,即自己不监控存入的 key / value 对是否过期,而是在获取 key 值时查看记录的时间戳(sed key flag exptime bytes),从而检查 key / value 对空间是否过期。这种策略不会在过期检测上浪费 CPU 资源。
(2)Memcached 懒惰删除对象机制
当删除 item 对象时,一般不会释放内存空间,而是做删除标记,将指针放入 slot 回收插槽,下次分配的时候直接使用。
Memcached 在分配空间时,会优先使用已经过期的 key / value 对空间;若分配的内存空间占满,Memcached 就会使用 LRU 算法来分配空间,删除最近最少使用的 key / value 对,从而将其空间分配给新的 key / value 对。在某些情况下(完整缓存),如果不想使用 LRU 算法,那么可以通过 -M
参数来启动 Memcached,这样,Memcached 在内存耗尽时,会返回一个报错信息,如下:
-M rerurn error on memory exhausted(rather than removing items)
(3)下面针对 Memcached 删除机制进行一个小结
- 不主动检测 item 对象是否过期,而是在 get 时才会检查 item 对象是否过期以及是否应该删除。
- 当删除 item 对象时,一般不释放内存空间,而是做删除标记,将指针放入 slot 回收插槽,下次分配的时候直接使用。
- 当内存空间满的时候,将会根据 LRU 算法把最近最少使用的 item 对象删除。
- 数据存入可以设定过期时间,但是数据过期后不会被立即删除,而是在 get 时检查 item 对象是否过期以及是否应该删除。
- 如果不希望系统使用 LRU 算法清除数据,可以用使用
-M
参数。
5.关于 Memcached 的常见问题
Memcached
是怎么工作的?
Memcached 的神奇来自两阶段哈希(two-stage hash
)。Memcached 就像一个巨大的、存储了很多 <key,value> 对的哈希表。通过 key,可以存储或查询任意的数据。
客户端可以把数据存储在多台 Memcached 上。当查询数据时,客户端首先参考节点列表计算出 key 的哈希值(阶段一哈希),进而选中一个节点;客户端将请求发送给选中的节点,然后 Memcached 节点通过一个内部的哈希算法(阶段二哈希),查找真正的数据(item)。
memcached
最大的优势是什么?
Memcached 最大的好处就是它带来了 极佳的水平可扩展性,特别是在一个巨大的系统中。由于客户端自己做了一次哈希,那么我们很容易增加大量 Memcached 到集群中。Memcached 之间没有相互通信,因此不会增加 Memcached 的负载;没有多播协议,不会网络通信量爆炸(implode)。
Memcached 的集群很好用。内存不够了?增加几台 Memcached 吧;CPU 不够用了?再增加几台吧;有多余的内存?再增加几台吧,不要浪费了。
基于 Memcached 的基本原则,可以相当轻松地构建出不同类型的缓存架构。
Memcached
和MySQL
的querycache
相比,有什么优缺点?
把 Memcached 引入应用中,还是需要不少工作量的。MySQL 有个使用方便的 query cache,可以自动地缓存 SQL 查询的结果,被缓存的 SQL 查询可以被反复地快速执行。Memcached 与之相比,怎么样呢?
MySQL 的 query cache 是集中式的,连接到该 query cache 的 MySQL 服务器都会受益。当您修改表时,MySQL 的 query cache 会立刻被刷新(flush)。存储一个 Memcached Item 只需要很少的时间,但是当写操作很频繁时,MySQL 的 query cache 会经常让所有缓存数据都失效。
在多核 CPU 上,MySQL 的 query cache 会遇到扩展问题(scalability issues)。在多核 CPU 上,query cache 会增加一个全局锁(global lock), 由于需要刷新更多的缓存数据,速度会变得更慢。
在 MySQL 的 query cache 中,我们是不能存储任意的数据的(只能是 SQL 查询结果)。而利用 Memcached,我们可以搭建出各种高效的缓存。比如,可以执行多个独立的查询,构建出一个用户对象(user object),然后将用户对象缓存到 Memcached 中。而 query cache 是 SQL 语句级别的,不可能做到这一点。在小的网站中,query cache 会有所帮助,但随着网站规模的增加,query cache 的弊将大于利。
query cache 能够利用的内存容量受到 MySQL 服务器空闲内存空间的限制。给数据库服务器增加更多的内存来缓存数据,固然是很好的。但是,有了 memcached,只要您有空闲的内存,都可以用来增加 memcached 集群的规模,然后您就可以缓存更多的数据。
Memcached
和服务器的local cache
(比如 PHP 的 APC、mmap 文件等)相比,有什么优缺点?
首先,local cache 有许多与上面 query cache 相同的问题。local cache 能够利用的内存容量受到(单台)服务器空闲内存空间的限制。不过,local cache 有一点比 Memcached 和 query cache 都要好,那就是它不但可以存储任意的数据,而且没有网络存取的延迟。
local cache 的数据查询更快。考虑把 highly common 的数据放在 local cache 中吧。如果每个页面都需要加载一些数量较少的数据,考虑把它们放在 local cached 吧。
local cache 缺少集体失效(group invalidation)的特性。在 Memcached 集群中,删除或更新一个key会让所有的观察者觉察到。但是在 local cache 中, 我们只能通知所有的服务器刷新 cache(很慢,不具扩展性),或者仅仅依赖缓存超时失效机制。
local cache 面临着严重的内存限制,这一点上面已经提到。
Memcached
如何实现冗余机制?
不实现!我们对这个问题感到很惊讶。Memcached 应该是应用的缓存层。它的设计本身就不带有任何冗余机制。如果一个 Memcached 节点失去了所有数据,您应该可以从数据源(比如数据库)再次获取到数据。
您应该特别注意,您的应用应该可以容忍节点的失效。不要写一些糟糕的查询代码,寄希望于 Memcached 来保证一切!如果您担心节点失效会大大加重数据库的负担,那么您可以采取一些办法。比如您可以增加更多的节点(来减少丢失一个节点的影响),热备节点(在其他节点 down 了的时候接管 IP),等等。
Memcached
如何处理容错的?
不处理!在 memcached 节点失效的情况下,集群没有必要做任何容错处理。如果发生了节点失效,应对的措施完全取决于用户。节点失效时,下面列出几种方案供您选择:
- 忽略它! 在失效节点被恢复或替换之前,还有很多其他节点可以应对节点失效带来的影响。
- 把失效的节点从节点列表中移除。做这个操作千万要小心!在默认情况下(余数式哈希算法),客户端添加或移除节点,会导致所有的缓存数据不可用!因为哈希参照的节点列表变化了,大部分 key 会因为哈希值的改变而被映射到(与原来)不同的节点上。
- 启动热备节点,接管失效节点所占用的 IP。这样可以防止哈希紊乱(Hashing Chaos)。
- 如果希望添加和移除节点,而不影响原先的哈希结果,可以使用一致性哈希算法(Consistent
Hashing)。您可以百度一下一致性哈希算法。支持一致性哈希的客户端已经很成熟,而且被广泛
使用。去尝试一下吧! - 两次哈希(reshing)。当客户端存取数据时,如果发现一个节点 down 了,就再做一次哈希(哈希算法与前一次不同),重新选择另一个节点(需要注意的时,客户端并没有把 down 的节点从节点列表中移除,下次还是有可能先哈希到它)。如果某个节点时好时坏,两次哈希的方法就有风险了,好的节点和坏的节点上都可能存在脏数据(stale data)。
如何将
Memcached
中item
批量导入导出?
您不应该这样做!Memcached 是一个非阻塞的服务器。任何可能导致 memcached 暂停或瞬时拒绝服务的操作都应该值得深思熟虑。向 memcached 中批量导入数据往往不是您真正想要的!想象看,如果缓存数据在导出导入之间发生了变化,您就需要处理脏数据了;如果缓存数据在导出导入之间过期了,您又怎么处理这些数据呢?
因此,批量导出导入数据并不像您想象中的那么有用。不过在一个场景倒是很有用。如果您有大量的从不变化的数据,并且希望缓存很快热(warm)起来,批量导入缓存数据是很有帮助的。虽然这个场景并不典型,但却经常发生,因此我们会考虑在将来实现批量导出导入的功能。
如果一个 memcached 节点 down 了让您很痛苦,那么您还会陷入其他很多麻烦。您的系统太脆弱了。您需要做一些优化工作。比如处理 惊群 问题(比如 memcached 节点都失效了,反复的查询让您的数据库不堪重负……这个问题在FAQ的其他提到过),或者优化不好的查询。记住,Memcached 并不是您逃避优化查询的借口。
Memcached
是如何做身份验证的?
没有身份认证机制!memcached 是运行在应用下层的软件(身份验证应该是应用上层的职责)。memcached 的客户端和服务器端之所以是轻量级的,部分原因就是完全没有实现身份验证机制。这样,memcached 可以很快地创建新连接,服务器端也无需任何配置。
如果您希望限制访问,您可以使用防火墙,或者让 memcached 监听 unix domain socket。
Memcached
的多线程是什么?如何使用它们?
在 Steven Grimm 和 Facebook 的努力下,memcached 1.2 及更高版本拥有了多线程模式。多线程模式允许 memcached 能够充分利用多个 CPU,并在 CPU 之间共享所有的缓存数据。memcached 使用一种简单的锁机制来保证数据更新操作的互斥。相比在同一个物理机器上运行多个 memcached 实例,这种方式能够更有效地处理 multi gets。
如果您的系统负载并不重,也许您不需要启用多线程工作模式。如果您在运行一个拥有大规模硬件的、庞大的网站,您将会看到多线程的好处。
简单地总结一下:命令解析(memcached 在这里花了大部分时间)可以运行在多线程模式下。memcached 内部对数据的操作是基于很多全局锁的(因此这部分工作不是多线程的)。未来对多线程模式的改进,将移除大量的全局锁,提高 memcached 在负载极高的场景下的性能。
Memcached
能接受的key
的最大长度是多少?
key 的最大长度是 250 个字符。需要注意的是,250 是 memcached 服务器端内部的限制,如果您使用的客户端支持 key 的前缀 或类似特性,那么 key(前缀+原始 key)的最大长度是可以超过 250 个字符的。
我们推荐使用使用较短的 key,因为可以节省内存和带宽。
Memcached
对item
的过期时间有什么限制?
过期时间最大可以达到 30 天。memcached 把传入的过期时间(时间段)解释成时间点后,一旦到了这个时间点,memcached 就把 item 置为失效状态。这是一个简单但 obscure 的机制。
Memcached
最大能存储多大的单个item
?
1MB。如果你的数据大于 1MB,可以考虑在客户端压缩或拆分到多个 key 中。
为什么单个 item 的大小被限制在 1M byte 之内?这是一个大家经常问的问题!
- 简单的回答:因为内存分配器的算法就是这样的。
- 详细的回答:Memcached 的内存存储引擎(引擎将来可插拔…),使用 slabs 来管理内存。内存被分成大小不等的 slabs chunks(先分成大小相等的 slabs,然后每个 slab 被分成大小相等 chunks,不同 slab 的 chunk 大小是不相等的)。chunk 的大小依次从一个最小数开始,按某个因子增长,直到达到最大的可能值。
Memcached
能够更有效地使用内存吗?
Memcache 客户端仅根据哈希算法来决定将某个 key 存储在哪个节点上,而不考虑节点的内存大小。
因此,您可以在不同的节点上使用大小不等的缓存。但是一般都是这样做的:拥有较多内存的节点上可以运行多个 memcached 实例,每个实例使用的内存跟其他节点上的实例相同。
什么是二进制协议,我该关注吗?
关于二进制最好的信息当然是二进制协议规范:二进制协议尝试为端提供一个更有效的、可靠的协议,减少客户端 / 服务器端因处理协议而产生的 CPU 时间。
根据 Facebook 的测试,解析 ASCII 协议是 memcached 中消耗 CPU 时间最多的环节。所以,我们为什么不改进 ASCII 协议呢?
Memcached
的内存分配器是如何工作的?为什么不适用malloc/free
?为何要使用slabs
?
实际上,这是一个编译时选项。默认会使用内部的 slab 分配器。您确实应该使用内建的 slab 分配器。最早的时候,memcached 只使用 malloc / free 来管理内存。然而,这种方式不能与 OS 的内存管理很好地工作。反复地 malloc / free 造成了内存碎片,OS 最终花费大量的时间去查找连续的内存块来满足 malloc 的请求,而不是运行 memcached 进程。
slab 分配器就是为了解决这个问题而生的。内存被分配并划分成 chunks,一直被重复使用。因为内存被划分成大小不等的 slabs,如果 item 的大小与被选择存放它的 slab 不是很合适的话,就会浪费一些内存。Steven Grimm 正在这方面已经做出了有效的改进。
Memcached
是原子的吗?
所有的被发送到 memcached 的单个命令是完全原子的。如果您针对同一份数据同时发送了一个 set 命令和一个 get 命令,它们不会影响对方。它们将被串行化、先后执行。即使在多线程模式,所有的命令都是原子的,除非程序有 bug。
命令序列不是原子的。如果您通过 get 命令获取了一个 item,修改了它,然后想把它 set 回memcached,我们不保证这个 item 没有被其他进程(process,未必是操作系统中的进程)操作过。在并发的情况下,您也可能覆写了一个被其他进程 set 的 item。
memcached 1.2.5 以及更高版本,提供了 gets
和 cas
命令,它们可以解决上面的问题。如果您使用 gets
命令查询某个 key 的 item,memcached 会给您返回该 item 当前值的唯一标识。如果您覆写了这个 item 并想把它写回到 memcached 中,您可以通过 cas
命令把那个唯一标识一起发送给memcached。如果该 item 存放在 memcached 中的唯一标识与您提供的一致,您的写操作将会成功。如果另一个进程在这期间也修改了这个 item,那么该 item 存放在 memcached 中的唯一标识将会改变,您的写操作就会失败。
如何实现集群中的
Session
共享存储?
Session 是运行在一台服务器上的,所有的访问都会到达我们的唯一服务器上,这样我们可以根据客户端传来的 SessionID,来获取 Session,或在对应 Session 不存在的情况下(Session 生命周期到了,或者用户第一次登录),创建一个新的 Session;但是,如果我们在集群环境下,假设我们有两台服务器 A,B,用户的请求会由 Nginx 服务器进行转发(别的方案也是同理),用户登录时,Nginx 将请求转发至服务器 A 上,A 创建了新的 Session,并将 SessionID 返回给客户端,用户在浏览其他页面时,客户端验证登录状态,Nginx 将请求转发至服务器 B,由于 B 上并没有对应客户端发来 SessionID 的 Session,所以会重新创建一个新的 Session,并且再将这个新的 SessionID 返回给客户端,这样,我们可以想象一下,用户每一次操作都有 1 / 2 1/2 1/2 的概率进行再次的登录,这样不仅对用户体验特别差,还会让服务器上的 Session 激增,加大服务器的运行压力。
为了解决集群环境下的 Seesion 共享问题,共有 4 种解决方案:
- 粘性 Session。粘性 Session 是指 Ngnix 每次都将同一用户的所有请求转发至同一台服务器上,即将用户与服务器绑定。
- 服务器 Session 复制。即每次 Session 发生变化时,创建或者修改,就广播给所有集群中的服务器,使所有的服务器上的 Session 相同。
- Session 共享。缓存 Session,使用 Redis,Memcached。
- Session 持久化。将 Session 存储至数据库中,像操作数据一样操作 Session。
Memcached
与Redis
的区别?
- Redis 不仅仅支持简单的 K / V 类型的数据,同时还提供 List,Set,Zset,Hash 等数据结构的存储。而 Memcached 只支持简单数据类型,需要客户端自己处理复杂对象。
- Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。持久化在 RDB(Redis DataBase)、AOF(Append Only File)。
- 由于 Memcached 没有持久化机制,因此宕机所有缓存数据失效。Redis 配置为持久化,宕机重启后,将自动加载宕机时刻的数据到缓存系统中。具有更好的灾备机制。
- Memcached 可以使用 Magent 在客户端进行一致性 hash 做分布式。Redis 支持在服务器端做分布式,如 Twemproxy、Codis、Redis-cluster 等多种分布式实现方式。
- Memcached 的简单限制就是 Key 和 Value 的限制。最大键长为 250 250 250 个字符,可以接受的储存数据不能超过 1 M B 1MB 1MB(可修改配置文件变大),因为这是典型 Slab 的最大值,不适合虚拟机使用。而 Redis 的 Key 长度支持到 512K。
- Redis 使用的是单线程模型,保证了数据按顺序提交。Memcached 需要使用 CAS 保证数据一致性。CAS(Check and Set)是一个确保并发一致性的机制,属于 “乐观锁” 范畴。原理很简单:拿版本号,操作,对比版本号,如果一致就操作,不一致就放弃任何操作
- CPU 利用。由于 Redis 只使用单核,而 Memcached 可以使用多核,所以平均每一个核上 Redis 在存储小数据时比 Memcached 性能更高;而在 100K 以上的数据中,Memcached 性能要高于Redis 。
- Memcache 内存管理:使用 Slab Allocation。原理相当简单,预先分配一系列大小固定的组,然后根据数据大小选择最合适的块存储。避免了内存碎片。(缺点:不能变长,浪费了一定空间)Memcached 默认情况下下一个 Slab 的最大值为前一个的 1.25 1.25 1.25 倍。
- Redis 内存管理: Redis 通过定义一个数组来记录所有的内存分配情况, Redis 采用的是包装的 malloc / free,相较于 Memcached 的内存管理方法来说,要简单很多。由于 malloc 首先以链表的方式搜索已管理的内存中可用的空间分配,导致内存碎片比较多。
Memcached
服务分布式集群如何实现?
特殊说明:Memcached 集群和 Web 服务集群是不一样的,所有 Memcached 的数据总和才是数据库的数据。每台 Memcached 都是部分数据。(一台 Memcached 的数据,就是一部分 MySQL 数据库的数据。)
- 程序端实现。程序加载所有 MC 的 ip 列表,通过对 key 做 hash (一致性哈希算法)。例如:web1 (key) => 对应 A , B , C , D , E , F , G … A,B,C,D,E,F,G… A,B,C,D,E,F,G… 若干台服务器。(通过哈希算法实现)
- 负载均衡器。通过对 key 做 hash(一致性哈希算法)。一致哈希算法的目的是不但保证每个对象只请求一个对应的服务器,而且当节点宕机,缓存服务器的更新重新分配比例降到最低。
参考:Memcached 数据库缓存