现阶段正在研究memcached,心血来潮把memcached官方网站上的FAQ翻译了一把,希望对想要使用memcached的同学们有帮助。由于兄弟我对数据库不是很熟,有些关于数据库概念的没有翻译,有些可能直接翻错了,望大家指出。谢谢!
?
?
基本问题
什么是 memcached ?
memcached 是一个高性能的分布式内存的缓存系统。本质上它是通用的,但其目的是为了加速动态 web 应用程序,减轻数据库访问压力而设计的。
?
Danga Interfactive 开发了 memcached 用来提高 LiveJournal.com 网站的速度。这个网站由大批的 web 服务器和数据库服务器构成,以此可以为其一百万用户提供每天两千万次的动态页面访问量。 Memcached 大大降低了数据库的负担,加快了页面的加载时间,提高了资源利用效率,加快了缓存未命中情况下的数据库访问时间。
?
哪里可以得到 memcached ?
memcached 可以从它的官方网站下载: http://www.danga.com/memcached/download.bml .
?
怎样安装 memchched ?
可以参照 yanwenhan 的 memcached 安装 http://yanwenhan.iteye.com/blog/160891 .:
?
哪些环境可以运行 memcached ?
随便哪里,只要你有空闲的内存就可以! Memcached 跑在 linux , BSD , windows 上。它一般很少占用 CPU 资源,这样你就可以运行在任何有空闲内存的地方了。
?
为什么要运行 memcached ?
如果你有一个高流量的网站,大多数的访问会造成数据库高负荷的状况。这时 memcached 能够减轻你数据库的压力。
?
怎样访问 memcached ?
你一般都会用一个客户端类库去访问一个或多个 memcached 服务器。
参考 http://www.socialtext.net/memcached/index.cgi?clients
或者 http://danga.com/memcached/apis.bml
上面的地址列出了现有的类库,他们是用 Perl , C , C# , PHP , Python , Java , Ruby 和 Postgresql 对存储过程以及触发器实现的。
你也可以自己开发客户端类库来实现 memecached 协议,相关协议文档: http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt
?
怎样把 memcached 当作数据库使用?
如果你不想把 memcached 当作缓存使用,而想把它当做数据存储来使用,那你因该直接使用数据库。 MySQL 集群也可以提供类似 memcached 的功能(尽管不是那么容易安装)并支持 HA (高可靠性)数据存储。
?
我能枚举遍历 memcached 服务器内的存储项吗?
不能。 memcached 不支持也不会计划支持这一特性。因为这会相对减慢服务器的速度以及阻塞操作(比较 memcached 正在执行的每样操作)。如上所述, memcached 是缓存而不是数据库。虽然 Tugela 是一个源于 memcached 的系统,但它有点慢,因为它使用 memcached 的方式有点像数据库。
当然, memcached 完全是一个软件,所以从某种角度说,最终的答案可能会是“能遍历 memcached ”,但是,枚举遍历数据项将会减慢和阻塞服务器。对于开发或测试服务器来说,这不算是问题。但对 99.9% 真正的部署服务器来说,回答是“不能”。
?
?
集群构架问题
?
memcached 怎样工作?
memcached 的神奇之处在于它的两步骤哈希方法。它的行为让人觉得它好像是一张巨大的,通过键值对查找的哈希表。给定一个键( key ),存储或取出任意数据。
当查找 memcached 时,客户端首先参照整个 memcached 服务器列表计算出键的哈希值。一旦它选择了一个 memcached 服务器,客户端就发起请求,服务器根据键值在内部查找对应的数据项。
比如,我们有客户端 1 , 2 , 3 和服务器 A , B , C :
客户端 1 想要设置键 "foo" 值 "barbaz" 。客户端 1 参照服务器列表( A , B , C )来哈希 "foo" ,根据 "foo" 的哈希值,最终选择了服务器 B 。然后客户端 1 直接连接服务器 B ,设置键 "foo" ,值 "barbaz" 。
下一步,客户端 2 想要得到键 "foo" 对应的值。客户端 2 运行与客户端 1 相同的客户端类库,并且使用相同的服务器列表( A , B , C )。计算得到键 "foo" 相同的哈希值,这样就知道了这个键在服务器 B 上。然后直接请求键 "foo" 得到相应的值 "barbaz" 。
不同的客户端实现在 memcached 服务器内存储数据的方式不同。一些客户端实现的哈希算法也不一样,但服务器端总是相同。
最后, memcached 本身被实现为基于事件的,非阻塞的服务器。这是一种用来解决 C10K 问题并且可以调整为饥饿方式( scale like crazy )的框架。
?
以上这些最大的好处是什么?
仔细阅读上述条目( memcached 怎样工作?)。在应付大系统时, memcached 最大的好处是其拥有的扩展能力。因为客户端做了哈希计算,我们完全可以把许多 memcached 节点加入到集群之中。集群的节点之间没有会导致过载的相互连接,也没有会引起向心聚爆( implode )的多点传送协议。 It Just Works. Run out of memory? Add a few more nodes. Run out of CPU? Add a few more nodes. Have some spare RAM here and there? Add nodes!
?
memcached 的缓存策略是什么?
memcached 的缓存结构是 LRU (最近最少使用)加上到期失效策略。当你在 memcached 内存储数据项时,你可能会声明它在缓存的失效时间。可以是永久或是存活到未来某一时段。如果 memcached 服务器用完分配的内存,失效的数据被首先替换,然后是最近未使用的数据。
?
memcached 的冗余是怎样实现的?
没有冗余!很惊讶! memcached 是你应用的缓存层。在设计上它没有任何的数据冗余的概念。如果一个节点丢失了它的数据,你可以重新从数据源获取所有数据。你的应用能够在丢失 memcached 实例的情况继续运行,这一点尤其要注意。 Don't write awful queries and expect memcached to be a fix-all! If you're worried about having too much of a spike in database usage during failure, you have some options. You can add more nodes (lessen impact of losing one), hotspares (take over IP address when down), etc.
?
memcached 是怎样应对失效转移的?
没有应对! :) 在 memcached 节点失效时,集群根本不做任何有关失败转移的事情。应对的行为完全取决于用户。当节点失效时,下面有几种方案供你选择。
?????? 1. 忽略它!在失效节点恢复或被替换前,你有许多节点可以应对这个节点失效所带来的影响。
?????? 2. 从服务器列表中移除失效的节点。千万小心!默认情况下,客户端增加或删除服务器列表会让你的缓存失效!因为用来做哈希参照的服务器列表已经改变,大多数键可能会被哈希出不同的值而被定为到不同的服务器。可以在同一时间重启你所有的节点来恢复。
?????? 3. 可以用相同 IP 地址的节点替换失效的节点。这样可以防止哈希紊乱。
?????? 4. 使用一致的哈希算法来增加删除集群中的节点。参考其他的哈希算法。
?
怎样从 memcached 中导出和导入批量数据?
你不能这样做! memcached 是一个我们称之为非阻塞服务器。 memcached 一定会十分仔细地考虑任何能导致服务器即刻暂停应答请求的功能。通常情况下,批量导入数据不会是你真正需要的!考虑一下如果你的数据在导出导入中有所改变的话,你处理的就是脏数据了。另外就是你怎样管理在数据导入期间过期的数据。
所以,导入导出功能并不像你通常想的那样有用。但有个场景它变得十分有用。如果你有大量的不变的数据,导入导出数据可能会有帮助。虽然这根本不是典型的应用场景,但它却是经常发生,所以这个特性可能会在将来出现。
?
Steven Grimm 给了个很好的例子:
http://lists.danga.com/pipermail/memcached/2007-July/004802.html
?
memcached 的验证机制是怎样工作的?
没有验证机制! memcached 位于你应用的下层。完全没有验证机制的部分原因是客户端和服务器端轻量化。这样建立新的连接会很快,也没有服务器端的配置。
如果不想要严格控制访问,你可以使用防火墙,或者可以通过 unix 的 domain socket 来为 memcached 监听。
?
什么是 memcached 的线程?为什么我要用它们?
线程规则!感谢 Steven Grimm 和 Facebook , memcached 1.2 以及更高的本版拥有线程的操作模式。在这儿我不会过多地涉及细节,因为我可能会弄错。线程系统允许 memcached 利用多核 CPU 以及在他们之间共享缓存。它应用一个十分简单的锁机制来控制某些值需要被更新时的同步问题。对比在一台物理机器上运行多个节点实例,线程模式可以使多核 CPU 变的更加有效。
如果你没有出现重负荷的情况,你可能不用去配置线程。如果你用庞大的硬件运行一个庞大的 web 网站,你可能就会看见好处了。
?
在使用 memcached 时,我可能会碰到什么限制?
你可能会看到的最简单的限制是对键以及数据项的大小限制。键被限制在 250 字符之内。数据项不能超过 1M ,因为这是最大的块( slab )值。
?
我能在多个服务器上使用不同大小的缓存吗? memcached 会有效地利用大内存的服务器吗?
memcached 的哈希算法决定了键存储在哪台服务器上,它不会去考虑服务器的内存大小。 But a workaround may be to run multiple memcached instances on your server with more memory with each instance using the same size cache as all your other servers.
?
什么是二进制协议?我应该关注吗?
这个问题的最佳信息在邮件列表上: http://lists.danga.com/pipermail/memcached/2007-July/004636.html
写这篇文章的时候,这个问题还没被解决,没有客户端被发布。
二进制协议是一种高效的,可信赖的 c/s 协议用来加速 CPU 时间。从 Facebook 的测试来看,解析 ASCII 协议是 memcached 对 CPU 时间的最大消耗。所以为什么不去提高它呢? :)
?
memcached 的内存分配是怎样工作的?为什么不用 malloc 或 free 呢?为什么要用 slabs ?
事实上,这是编译时的选项。默认情况下 memcached 用内部的 slab 作为分配器的。你真的很需要使用内建的 slab 分配器。一开始, memcached 的确用 malloc/free 分配所有东西。然而,这并不能同操作系统的内存管理器很好地工作。你的操作系统花在查找连续内存块用以 malloc() 的时间超出了 memcached 本身的操作运行时间。
slab 分配器就是用来解决这个问题的。在 memcached 内部以块为单位来分配和重用内存。因为内存被分为不同大小的 slab ,如果你的数据项没有完全符合服务器选择的 slab 的大小,这的确会导致浪费内存。 Steven Grimm 已经在这方面做了相当有效的改进。
一些对 slab 的改进和折中方案在邮件列表中: http://lists.danga.com/pipermail/memcached/2006-May/002163.html
http://lists.danga.com/pipermail/memcached/2007-March/003753.html
如果你试图用 malloc/free 分配内存,你可以在构建中定义 'USE_SYSTEM_MALLIC' 。它可能没有经过很好测试,所以不用指望能得到开发者的支持。
?
?
性能问题
?
为什么 memcached 没有我的数据库快?
在一对一的比较中, memcached 可能没有你的 SQL 查询快。然而,这并不是它的目标。 memcached 的目标是可伸缩性。随着连接和请求的增加, memcached 将会表现出比单独的数据库解决方案更好的性能。在决定 memcached 不适合你的应用之前,请将你的代码放在高并发的环境中测试。
?
?
客户端类库
?
我能用不同的客户端得到相同的数据吗?
技术上是可以的,但是有些问题你可能会碰到:
?????? 1. 不同的类库可能采用不同的方式序列号数据,比如, Perl 的 Cache::Memcached 会用 Storable 序列化复杂结构对象。其他语言的客户端很可能不能读出这些格式的数据,你可能会考虑用简单的 String 格式序列化对象,这需要外部类库比如 JSON 和 XML 。
?????? 2. 同样,你的数据从某个客户端过来的时候可能被压缩了,但另一个客户端却没有压缩。
?????? 3. 不同的类库可能采用不同的哈希算法。如果你正在连接多个服务器,你的键很可能被不同语言的客户端相应地哈希后并存储。不同语言的客户端可能采用不同的策略来选择服务器进行存储,所以对同一个键来说, Perl 的客户端可能选择服务器 A ,而 Python 客户端选择服务器 B 。 Perl 的 API 还允许你对不同的服务器设置不同的权重,这也可能是造成这一问题的因素。
?
什么是一致的哈希客户端?
参考 http://www.last.fm/user/RJ/journal/2007/04/10/392555
?
?
哈希 / 键分布
数据项到期失效
什么时候失效的数据项会从缓存中删除?
memcached 使用懒失效。当客户端请求数据项时, memcached 在返回数据前会检查失效时间来确定数据项是否已经失效。
同样地,当添加一个新的数据项时,如果缓存已经满了, memcached 就会先替换失效的数据项,然后才是缓存中最少使用的数据项。
?
?
命名空间
memcached 不支持命名空间。然而有几种选择可以模仿他们。
?
用键的前缀模仿命名空间
在键之前加入有意义的前缀。
?
用命名空间删除数据项
尽管 memcached 不支持使用任何类型的通配符或命名空间来完成删除操作,但有一些技巧模拟他们。
在 PHP 中使用一个叫 foo 的命名空间:
$ns_key = $memcache->get("foo_namespace_key");
// if not set, initialize it
if($ns_key===false) $memcache->set("foo_namespace_key", rand(1, 10000));
$my_key = "foo_".$ns_key."_12345";
清除命名空间:
$memcache->increment("foo_namespace_key");
?
?
应用设计
?
在设计应用时,针对缓存有哪些东西是我应该考虑的?
缓存简单的查询结果
查询缓存存储了给定查询语句对应的整个结果集。它最合适缓存那些经常被用到,但不会改变的 SQL 语句,比如载入特定的过滤内容。
$key = md5('SELECT * FROM rest_of_sql_statement_goes_here');
if ($memcache->get($key)) {
` return $memcache->get($key);`
}
else {
` // Run the query and transform the result data into your final dataset form`
` $result = $query_results_mangled_into_most_likely_an_array`
` $memcache->set($key, $result, TRUE, 86400); // Store the result of the query for a day`
` return $result;`
}
记住,如果查询语句对应的结果集改变,该结果集不会展现出来。这种方法不总是有用,但它确实让工作变得比较快。
?
缓存简单的基于行的查询结果
基于行的缓存会检查缓存数据的标识符列表,那些在缓存中的行可以直接取出,不在缓存中的行将会从数据库中取出并以他们自己的键缓存他们,最后加入到最终的数据集中返回。随着时间的过去,大多数数据都会被缓存,这也意味着相比与数据库,查询语句会更多地从 memcached 中得到数据行。如果数据是相当静态的,我们可以设置一个较长的缓存时间。基于行的缓存模式对下面这种搜索情况特别有用:数据集本身很大或是数据集是从多张表中得到,而数据集取决于查询的输入参数但是查询的结果集之间的有重复部分。
比如,如果你有用户 A , B , C , D , E 的数据集。
你去点击一张显示用户 A , B , E 信息的页面。首先, memcached 得到 3 个不同的键,每个对应一个用户去缓存中查找,全部未命中。然后就到数据库中用 SQL 查询得到 3 个用户的数据行,并缓存他们。
现在,你又去点击另一张显示显示 C , D , E 信息的页面。当你去查找 memcached 时, C , D 的数据并没有被命中,但我们命中了 E 的数据。然后从数据库得到 C , D 的行数据,缓存在 memcached 中。
至此以后,无论这些用户信息怎样地排列组合,任何关于 A , B , C , D , E 信息的页面都可以从 memcached 得到数据了。
?
Action flood control
Flood control is the process of throttling user activity, usually for load management. We first try to add a memcache key that uniquely identifies a user and times out after a given interval. If that succeeds, there is no identical key, and thus the user should be allowed to do the action. If the add fails, the user is still in the flood control interval, so shouldn't be allowed to continue their action. If all else fails and the key cannot be added or retrieved, something's wonky with memcache and it's up to you to decide whether to allow action or not (suggested yes to prevent long term memcache issues from stopping all actions).
?
So, if user A makes a comment in thread 7, and you don't want them to be able to comment again for another 60 seconds:
'add' a key (eg) 'noflood:A:7' into memcached. If you get a SUCCESS, the user may post. If you get a NOT_STORED (but not an error!), the key still exists and the user should be warned.
?
Note you may also try fetching a key and doing incr/decr on it if a user should only be allowed to perform an action a certain number of times before being throttled.
?
缓存的不是 SQL 数据
当你第一次缓存你手头除了 SQL 以外的其他结果集时,你可能并没有在意。是的,你可以也应该存储其他的数据。
如果你正在构建一张显示用户信息的页面,你可能得到一段关于用户的信息(姓名,生日,家庭住址,简介)。然后你可能会将 XML 格式的简介信息转化为 HTML 格式,或做其他的一些工作。相比单独存储这些属性,你可能更愿意存储经过渲染的数据块。那时你就可以简单地取出被预处理后的 HTML 直接填充在页面中,这样节省了宝贵的 CPU 时间。
?
使用分层的缓存
很多时候你可以使用局部缓存。我们知道 memcached 可以高速处理大量的缓存数据,但是有时你还是要考虑维护多层的缓存结构。
Peter Zaitsev 已经写了有关本地运行 PHP 的 APC 和本地运行 memcached 的速度比较,使用两者的好处请参考
http://www.mysqlperformanceblog.com/2006/08/09/cache-performance-comparison/
http://www.mysqlperformanceblog.com/2006/09/27/apc-or-memcached/
一般比只有少量数据(产品分类,连接信息,服务器状态变量,应用配置变量),这些信息几乎每页都会访问。缓存他们来让他们尽可能接近处理器是有意义的 , 这可以帮助减少生成页面的时间,并且在 memcached 失效的情况下可以增加可靠性。
?
当你的数据更新时更新你的缓存
你可以做的一个很重要的提高,它可以确保你的缓存无缝地集成到你的应用中去,它就是在数据库数据更新时同步缓存的数据。
用户 A 编辑了他的用户信息。当他保存信息到数据库时,你可能需要更新缓存中的数据,或是简单地删除老的用户信息。如果你马上更新数据,你要防止从数据库读取那些刚刚更新过的数据。当用户习惯性地重新载入他们的用户信息来确认是否修改成功时,数据将从缓存中直接取出,这时他们获得了最新的数据。
这很了不起,因为没有用户想看过期的数据,不是吗?
?
条件竞争和陈旧的数据
当你设计需要缓存数据的应用时,怎样处理条件竞争和陈旧的数据变的很重要。
假设你缓存了显示在边条( sidebar )上最近 5 条评论,你决定每一分钟刷新一次数据。然而,你忘记了边条每秒被刷新 50 次 ! 因此,一旦 60 秒过去,立即就会有 10 多个进程执行相同的 SQL 查询来重装缓存内的数据。每当缓存内容失效时,就会导致一堆的 SQL 查询操作。
更糟的是,你可能有多个进程在更新相同的数据,其中有一个进程更新了错误的数据。这时你就有陈旧的过期的数据要处理了。
组装和重组缓存中的数据时,需要提醒的是检查 memcached ,获取 SQL ,缓存数据,这些操作根本不在一个原子内。
?
How to prevent clobbering updates, stampeding requests
So how does one prevent clobbering your own updates or stampeding during a cache miss? The easiest answer is to avoid the problem. Don't set caches to expire, and update them via cron, or as data is updated. This does not eliminate the possibility of a stampede, but removes it from becoming the norm.
?
Some great ideas from the mailing list also underline another approach:
?
If you want to avoid a stampede if key A expires for its common case (a timeout, for example). Since this is caused by a race condition between the cache miss, and the amount of time it takes to re-fetch and update the cache, you can try shortening the window.
?
First, set the cache item expire time way out in the future. Then, you embed the "real" timeout serialized with the value. For example you would set the item to timeout in 24 hours, but the embedded timeout might be five minutes in the future.
?
Then, when you get from the cache and examine the timeout and find it expired, immediately edit the embedded timeout to a time in the future and re-store the data as is. Finally, fetch from the DB and update the cache with the latest value. This does not eliminate, but drastically reduces the amount of time where a stampede can occur.
?
A decent python example can be found here: http://www.djangosnippets.org/snippets/155/
?
If you have a lot of data excelling at causing this problem, you might also consider using MySQL Cluster for it, or a tiered caching approach
?
Another (pretty cool!) idea is to use Gearman, as noted on the mailing list: http://lists.danga.com/pipermail/memcached/2007-July/004858.html
?
模拟带锁的添加命令
如果你实在需要锁,你可以通过“添加”命令模仿锁的功能。尽管在未命中的情况下它不是那么有用,但如果你用它缓存平常的数据(应用服务器池的元数据)那还是有用的。
比如,你要更新键 A 。
?????? 1. 添加一个 "lock:A" 的键,这个键有一个持续几秒的过期时间(足够长以使你能完成计算和更新,也不要很长,因为如果锁进程挂了,这个键不会立即释放)
?????? 2. 如果添加操作成功了,你就拥有了锁:
????????????? 从缓存获取键 A 的数据。
????????????? 在客户端更改数据。
????????????? 更新缓存键 A 的数据。
????????????? 删除键 "lock:A" ,如果你不需要立即再次更新,就让它存活直到失效。
?? ? 3. 如果添加操作失败,说明有人获取了锁。这时让应用做些合适的事,比如返回老数据,等待后重试,或是其他的。
以上这些操作类似 MySQL 将 GET_LOCK 的 timeout 值设置成 0 。没有办法在 memcached 中通过互斥锁模拟 GET_LOCK() 的 timeout 操作。
预热你的缓存
?
如果你有一个很高访问率的站点,并且你正想加入故障恢复功能或是其他全新的功能,你最终可能会碰到空缓存的问题。一开始缓存是空的,然后一大群人点击你的站点,在填充缓存的过程中,你的数据库可能会承受不住压力。为了解决这一问题,你可以试试任何可行的方法来 " 温暖 " 你的数据库。
?
你可以写一些脚本来缓存通用的页面。你也可以写一个命令行工具来填充缓存。两种方法都可能对你有帮助。你可以在高峰时刻在缓存里填充一些内容。