Memcached 的神奇来自两阶段哈希(two-stage hash
)。Memcached 就像一个巨大的、存储了很多
客户端可以把数据存储在多台 Memcached 上。当查询数据时,客户端首先参考节点列表计算出 key 的哈希值(阶段一哈希),进而选中一个节点;客户端将请求发送给选中的节点,然后 Memcached 节点通过一个内部的哈希算法(阶段二哈希),查找真正的数据(item)。
Memcached 最大的好处就是它带来了 极佳的水平可扩展性,特别是在一个巨大的系统中。由于客户端自己做了一次哈希,那么我们很容易增加大量 Memcached 到集群中。Memcached 之间没有相互通信,因此不会增加 Memcached 的负载;没有多播协议,不会网络通信量爆炸(implode)。
Memcached 的集群很好用。内存不够了?增加几台 Memcached 吧;CPU 不够用了?再增加几台吧;有多余的内存?再增加几台吧,不要浪费了。
基于 Memcached 的基本原则,可以相当轻松地构建出不同类型的缓存架构。
把 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 集群的规模,然后您就可以缓存更多的数据。
首先,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 来保证一切!如果您担心节点失效会大大加重数据库的负担,那么您可以采取一些办法。比如您可以增加更多的节点(来减少丢失一个节点的影响),热备节点(在其他节点 down 了的时候接管 IP),等等。
不处理!在 memcached 节点失效的情况下,集群没有必要做任何容错处理。如果发生了节点失效,应对的措施完全取决于用户。节点失效时,下面列出几种方案供您选择:
您不应该这样做!Memcached 是一个非阻塞的服务器。任何可能导致 memcached 暂停或瞬时拒绝服务的操作都应该值得深思熟虑。向 memcached 中批量导入数据往往不是您真正想要的!想象看,如果缓存数据在导出导入之间发生了变化,您就需要处理脏数据了;如果缓存数据在导出导入之间过期了,您又怎么处理这些数据呢?
因此,批量导出导入数据并不像您想象中的那么有用。不过在一个场景倒是很有用。如果您有大量的从不变化的数据,并且希望缓存很快热(warm)起来,批量导入缓存数据是很有帮助的。虽然这个场景并不典型,但却经常发生,因此我们会考虑在将来实现批量导出导入的功能。
如果一个 memcached 节点 down 了让您很痛苦,那么您还会陷入其他很多麻烦。您的系统太脆弱了。您需要做一些优化工作。比如处理 惊群 问题(比如 memcached 节点都失效了,反复的查询让您的数据库不堪重负……这个问题在FAQ的其他提到过),或者优化不好的查询。记住,Memcached 并不是您逃避优化查询的借口。
没有身份认证机制!memcached 是运行在应用下层的软件(身份验证应该是应用上层的职责)。memcached 的客户端和服务器端之所以是轻量级的,部分原因就是完全没有实现身份验证机制。这样,memcached 可以很快地创建新连接,服务器端也无需任何配置。
如果您希望限制访问,您可以使用防火墙,或者让 memcached 监听 unix domain socket。
在 Steven Grimm 和 Facebook 的努力下,memcached 1.2 及更高版本拥有了多线程模式。多线程模式允许 memcached 能够充分利用多个 CPU,并在 CPU 之间共享所有的缓存数据。memcached 使用一种简单的锁机制来保证数据更新操作的互斥。相比在同一个物理机器上运行多个 memcached 实例,这种方式能够更有效地处理 multi gets。
如果您的系统负载并不重,也许您不需要启用多线程工作模式。如果您在运行一个拥有大规模硬件的、庞大的网站,您将会看到多线程的好处。
简单地总结一下:命令解析(memcached 在这里花了大部分时间)可以运行在多线程模式下。memcached 内部对数据的操作是基于很多全局锁的(因此这部分工作不是多线程的)。未来对多线程模式的改进,将移除大量的全局锁,提高 memcached 在负载极高的场景下的性能。
key 的最大长度是 250 个字符。需要注意的是,250 是 memcached 服务器端内部的限制,如果您使用的客户端支持 key 的前缀 或类似特性,那么 key(前缀+原始 key)的最大长度是可以超过 250 个字符的。
我们推荐使用使用较短的 key,因为可以节省内存和带宽。
过期时间最大可以达到 30 天。memcached 把传入的过期时间(时间段)解释成时间点后,一旦到了这个时间点,memcached 就把 item 置为失效状态。这是一个简单但 obscure 的机制。
1MB。如果你的数据大于 1MB,可以考虑在客户端压缩或拆分到多个 key 中。
为什么单个 item 的大小被限制在 1M byte 之内?这是一个大家经常问的问题!
Memcache 客户端仅根据哈希算法来决定将某个 key 存储在哪个节点上,而不考虑节点的内存大小。
因此,您可以在不同的节点上使用大小不等的缓存。但是一般都是这样做的:拥有较多内存的节点上可以运行多个 memcached 实例,每个实例使用的内存跟其他节点上的实例相同。
关于二进制最好的信息当然是二进制协议规范:二进制协议尝试为端提供一个更有效的、可靠的协议,减少客户端 / 服务器端因处理协议而产生的 CPU 时间。
根据 Facebook 的测试,解析 ASCII 协议是 memcached 中消耗 CPU 时间最多的环节。所以,我们为什么不改进 ASCII 协议呢?
实际上,这是一个编译时选项。默认会使用内部的 slab 分配器。您确实应该使用内建的 slab 分配器。最早的时候,memcached 只使用 malloc / free 来管理内存。然而,这种方式不能与 OS 的内存管理很好地工作。反复地 malloc / free 造成了内存碎片,OS 最终花费大量的时间去查找连续的内存块来满足 malloc 的请求,而不是运行 memcached 进程。
slab 分配器就是为了解决这个问题而生的。内存被分配并划分成 chunks,一直被重复使用。因为内存被划分成大小不等的 slabs,如果 item 的大小与被选择存放它的 slab 不是很合适的话,就会浪费一些内存。Steven Grimm 正在这方面已经做出了有效的改进。
所有的被发送到 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 是运行在一台服务器上的,所有的访问都会到达我们的唯一服务器上,这样我们可以根据客户端传来的 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 种解决方案:
特殊说明:Memcached 集群和 Web 服务集群是不一样的,所有 Memcached 的数据总和才是数据库的数据。每台 Memcached 都是部分数据。(一台 Memcached 的数据,就是一部分 MySQL 数据库的数据。)