Memcached详解(转)
前言:给大家介绍一个入门Memcached很好的资料,来源为日本某论坛上的一系列连载文章(长野雅广、前坂徹著),国人翻译的也是不错。我此处稍微整理下,可供大家一起学习参考。
本篇文章重点关注以下问题:
- memcached的基础
- 理解memcached的内存存储
- memcached的删除机制和发展方向
- memcached的分布式算法
1. memcached的基础
1.1 memcached是什么?
memcached 是以LiveJournal 旗下Danga Interactive 公司的Brad Fitzpatric 为首开发的一款软件。现在已成为mixi、hatena、Facebook、Vox、LiveJournal等众多服务中提高Web应用扩展性的重要因素。许多Web应用都将数据保存到RDBMS中,应用服务器从中读取数据并在浏览器中显示。但随着数据量的增大、访问的集中,就会出现RDBMS的负担加重、数据库响应恶化、网站显示延迟等重大影响。这时就该memcached大显身手了。memcached是高性能的分布式内存缓存服务器。一般的使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、提高可扩展性。
1.2 Memcached的特征
memcached作为高速运行的分布式缓存服务器,具有以下的特点:
- 协议简单
- 基于libevent的事件处理
- 内置内存存储方式
- memcached不互相通信的分布式
1.2.1 协议简单
memcached的服务器客户端通信并不使用复杂的XML等格式,而使用简单的基于文本行的协议。因此,通过telnet也能在memcached上保存数据、取得数据。下面是例子。
$ telnet localhost 11211 Trying 127.0.0.1... Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. set foo 0 0 3 (保存命令) bar (数据) STORED (结果) get foo (取得命令) VALUE foo 0 3 (数据) bar (数据)
协议文档位于memcached的源代码内,也可以参考以下的URL。
1.2.2 基于libevent的事件处理
libevent是个程序库,它将Linux的epoll、BSD类操作系统的kqueue等事件处理功能封装成统一的接口。即使对服务器的连接数增加,也能发挥O(1)的性能。memcached使用这个libevent库,因此能在Linux、BSD、Solaris等操作系统上发挥其高性能。
1.2.3 内置内存存储方式
为了提高性能,memcached中保存的数据都存储在memcached内置的内存存储空间中。由于数据仅存在于内存中,因此重启memcached、重启操作系统会导致全部数据消失。另外,内容容量达到指定值之后,就基于LRU(Least Recently Used)算法自动删除不使用的缓存。memcached本身是为缓存而设计的服务器,因此并没有过多考虑数据的永久性问题。关于内存存储的详细信息,请参考本文的第 2 章 以后前坂介绍的内容。
1.2.4 memcached不互相通信的分布式
memcached尽管是“分布式”缓存服务器,但服务器端并没有分布式功能。各个memcached不会互相通信以共享信息。那么,怎样进行分布式呢?这完全取决于客户端的实现。本文也将介绍memcached的分布式。
接下来简单介绍一下memcached的使用方法。
1.3 安装Memcached
memcached的安装比较简单,这里稍加说明。memcached支持许多平台:
- Linux
- FreeBSD
- Solaris (memcached 1.2.5以上版本)
- Mac OS X
- Windows
1.3.1 memcached的安装
运行memcached需要本文开头介绍的libevent库。Fedora 8中有现成的rpm包,通过yum命令安装即可。
$ sudo yum install libevent libeventdevel
memcached的源代码可以从memcached网站上下载。本文执笔时的最新版本为1.2.5。Fedora 8虽然也包含了memcached的rpm,但版本比较老。因为源代码安装并不困难,这里就不使用rpm了。
- 下载memcached:http://www.danga.com/memcached/download.bml
memcached安装与一般应用程序相同,configure、make、make install就行了。
$ wget http://www.danga.com/memcached/dist/memcached1.2.5. tar.gz $ tar zxf memcached1.2.5. tar.gz $ cd memcached1.2.5 $ ./configure $ make $ sudo make install
默认情况下memcached安装到/usr/local/bin下。
1.3.2 memcached的启动
从终端输入以下命令,启动memcached。
$ /usr/local/bin/memcached p 11211 m 64m vv slab class 1: chunk size 88 perslab 11915 slab class 2: chunk size 112 perslab 9362 slab class 3: chunk size 144 perslab 7281 中间省略 slab class 38: chunk size 391224 perslab 2 slab class 39: chunk size 489032 perslab 2 <23 server listening <24 send buffer was 110592, now 268435456 <24 server listening (udp) <24 server listening (udp) <24 server listening (udp) <24 server listening (udp)
这里显示了调试信息。这样就在前台启动了memcached,监听TCP端口11211最大内存使用量为64M。调试信息的内容大部分是关于存储的信息,下一章将具体说明。作为daemon后台启动时,只需
$ /usr/local/bin/memcached -p 11211 -m 64m -d
这里使用的memcached启动选项的内容如下:
选项 | 说明 |
-p | 使用的TCP端口。默认为11211 |
-m | 最大内存大小。默认为64M |
-vv | 用very vrebose模式启动,调试信息和错误输出到控制台 |
-d | 作为daemon在后台启动 |
上面四个是常用的启动选项,其他还有很多,通过
$ /usr/local/bin/memcached -h命令可以显示。许多选项可以改变memcached的各种行为,推荐读一读。
1.4 用客户端连接
许多语言都实现了连接memcached的客户端,其中以Perl、PHP为主。仅仅memcached网站上列出的语言就有:
- Perl
- PHP
- Python
- Ruby
- C#
- C/C++
- Lua
2 理解memcached的内存存储
上一章的文章介绍了memcached是分布式的高速缓存服务器。本次将介绍memcached的内部构造的实现方式,以及内存的管理方式。另外,memcached的内部构造导致的弱点也将加以说明。
2.1 Slab Allocation机制:整理内存以便重复使用
最近的memcached默认情况下采用了名为Slab Allocator的机制分配、管理内存。在该机制出现以前,内存的分配是通过对所有记录简单地进行malloc和free来进行的。但是,这种方式会导致内存碎片,加重操作系统内存管理器的负担,最坏的情况下,会导致操作系统比memcached进程本身还慢。Slab Allocator就是为解决该问题而诞生的。
下面来看看Slab Allocator的原理。下面是memcached文档中的slab allocator的目标:the primary goal of the slabs subsystem in memcached was to eliminate memory fragmentation issues totally by using fixedsize memory chunks coming from a few predetermined size classes.
也就是说,Slab Allocator的基本原理是按照预先规定的大小,将分配的内存分割成特定长度的块,以完全解决内存碎片问题。Slab Allocation的原理相当简单。将分配的内存分割成各种尺寸的块(chunk),并把尺寸相同的块分成组(chunk的集合):
而且,slab allocator还有重复使用已分配的内存的目的。也就是说,分配到的内存不会释放,而是重复利用。
Slab Allocation的主要术语:
- Page:分配给Slab的内存空间,默认是1MB。分配给Slab之后根据slab的大小切分成chunk;
- Chunk:用于缓存记录的内存空间;
- Slab Class:特定大小的chunk的组。
2.2 在Slab中缓存记录的原理
memcached根据收到的数据的大小,选择最适合数据大小的slab(图2.2)。memcached中保存着slab内空闲chunk的列表,根据该列表选择chunk,然后将数据缓存于其中。
实际上,Slab Allocator也是有利也有弊。下面介绍一下它的缺点。
2.3 Slab Allocator的缺点
Slab Allocator解决了当初的内存碎片问题,但新的机制也给memcached带来了新的问题。这个问题就是,由于分配的是特定长度的内存,因此无法有效利用分配的内存。例如,将100字节的数据缓存到128字节的chunk中,剩余的28字节就浪费了,如下图所示:
对于该问题目前还没有完美的解决方案,但在文档中记载了比较有效的解决方案。The most efficient way to reduce the waste is to use a list of size classes that closely matches (if that's at all possible) common sizes of objects that the clients of this particular installation of memcached are likely to store.
就是说,如果预先知道客户端发送的数据的公用大小,或者仅缓存大小相同的数据的情况下,只要使用适合数据大小的组的列表,就可以减少浪费。但是很遗憾,现在还不能进行任何调优,只能期待以后的版本了。但是,我们可以调节slab class的大小的差别。接下来说明growth factor选项。
2.4 使用Growth Factor进行调优
memcached在启动时指定Growth Factor因子(通过-f选项),就可以在某种程度上控制slab之间的差异。默认值为1.25。但是,在该选项出现之前,这个因子曾经固定为2,称为“powers of 2”策略。让我们用以前的设置,以verbose模式启动memcached试试看:
$ memcached -f 2 -vv
下面是启动后的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)时的输出(篇幅所限,这里只写到第10组):
slab class 1: chunk size 88 perslab 11915 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
可见,组间差距比因子为2时小得多,更适合缓存几百字节的记录。从上面的输出结果来看,可能会觉得有些计算误差,这些误差是为了保持字节数的对齐而故意设置的。将memcached引入产品,或是直接使用默认值进行部署时,最好是重新计算一下数据的预期平均长度,调整growth factor,以获得最恰当的设置。内存是珍贵的资源,浪费就太可惜了。
接下来介绍一下如何使用memcached的stats命令查看slabs的利用率等各种各样的信息。
2.5 查看memcached的内部状态
memcached有个名为stats的命令,使用它可以获得各种各样的信息。执行命令的方法很多,用telnet最为简单:
$ telnet 主机名 端口号
连接到memcached之后,输入stats再按回车,即可获得包括资源利用率在内的各种信息。此外,输入"stats slabs"或"stats items"还可以获得关于缓存记录的信息。结束程序请输入quit。这些命令的详细信息可以参考memcached软件包内的protocol.txt文档。
$ telnet localhost 11211 Trying ::1... Connected to localhost. Escape character is '^]'. stats STAT pid 481 STAT uptime 16574 STAT time 1213687612 STAT version 1.2.5 STAT pointer_size 32 STAT rusage_user 0.102297 STAT rusage_system 0.214317 STAT curr_items 0 STAT total_items 0 STAT bytes 0 STAT curr_connections 6 STAT total_connections 8 STAT connection_structures 7 STAT cmd_get 0 STAT cmd_set 0 STAT get_hits 0 STAT get_misses 0 STAT evictions 0 STAT bytes_read 20 STAT bytes_written 465 STAT limit_maxbytes 67108864 STAT threads 4 END quit
另外,如果安装了libmemcached这个面向C/C++语言的客户端库,就会安装memstat这个命令。使用方法很简单,可以用更少的步骤获得与telnet相同的信息,还能一次性从多台服务器获得信息。
$ memstat --servers=server1,server2,server3,...
libmemcached可以从下面的地址获得:
2.6 查看slabs的使用状况
使用memcached的创造着Brad写的名为memcachedtool的Perl脚本,可以方便地获得slab的使用情况(它将memcached的返回值整理成容易阅读的格式)。可以从下面的地址获得脚本:
使用方法也极其简单:
$ memcached-tool 主机名:端口 选项
查看slabs使用状况时无需指定选项,因此用下面的命令即可:
$ memcached-tool 主机名:端口
获得的信息如下所示:
# Item_Size Max_age 1MB_pages Count Full? 1 104 B 1394292 s 1215 12249628 yes 2 136 B 1456795 s 52 400919 yes 3 176 B 1339587 s 33 196567 yes 4 224 B 1360926 s 109 510221 yes 5 280 B 1570071 s 49 183452 yes 6 352 B 1592051 s 77 229197 yes 7 440 B 1517732 s 66 157183 yes 8 552 B 1460821 s 62 117697 yes 9 696 B 1521917 s 143 215308 yes 10 872 B 1695035 s 205 246162 yes 11 1.1 kB 1681650 s 233 221968 yes 12 1.3 kB 1603363 s 241 183621 yes 13 1.7 kB 1634218 s 94 57197 yes 14 2.1 kB 1695038 s 75 36488 yes 15 2.6 kB 1747075 s 65 25203 yes 16 3.3 kB 1760661 s 78 24167 yes
各列的含义为:
列 | 含义 |
# | slab class编号 |
Item_Size | Chunk大小 |
Max_age | LRU内最旧的记录的生存时间 |
1MB_pages | 分配给Slab的页数 |
Count | Slab内的记录数 |
Full? | Slab内是否含有空闲chunk |
从这个脚本获得的信息对于调优非常方便,强烈推荐使用。
3. memcached的删除机制和发展方向
memcached是缓存,所以数据不会永久保存在服务器上,这是向系统中引入memcached的前提。本次介绍memcached的数据删除机制,以及memcached的最新发展方向——二进制协议(Binary Protocol)和外部引擎支持。
3.1 memcached在数据删除方面有效利用资源
1. 数据不会真正从memcached中消失
上一章介绍过,memcached不会释放已分配的内存。记录超时后,客户端就无法再看见该记录(invisible,透明),其存储空间即可重复使用。
2. Lazy Expiration
memcached内部不会监视记录是否过期,而是在get时查看记录的时间戳,检查记录是否过期。这种技术被称为lazy(惰性)expiration。因此,memcached不会在过期监视上耗费CPU时间。
3.2 LRU:从缓存中有效删除数据的原理
memcached会优先使用已超时的记录的空间,但即使如此,也会发生追加新记录时空间不足的情况,此时就要使用名为Least Recently Used(LRU)机制来分配空间。顾名思义,这是删除“最近最少使用”的记录的机制。因此,当memcached的内存空间不足时(无法从slab class 获取到新的空间时),就从最近未被使用的记录中搜索,并将其空间分配给新的记录。从缓存的实用角度来看,该模型十分理想。
不过,有些情况下LRU机制反倒会造成麻烦。memcached启动时通过“M”参数可以禁止LRU,如下所示:
$ memcached -M -m 1024
启动时必须注意的是,小写的“m”选项是用来指定最大内存大小的。不指定具体数值则使用默认值64MB。指定“M”参数启动后,内存用尽时memcached会返回错误。话说回来,memcached毕竟不是存储器,而是缓存,所以推荐使用LRU。
3.3 memcached的最新发展方向
memcached的roadmap上有两个大的目标。一个是二进制协议的策划和实现,另一个是外部引擎的加载功能。
3.3.1 关于二进制协议
使用二进制协议的理由是它不需要文本协议的解析处理,使得原本高速的memcached的性能更上一层楼,还能减少文本协议的漏洞。目前已大部分实现,开发用的代码库中已包含了该功能。memcached的下载页面上有代码库的链接。
3.3.2 二进制协议的格式
协议的包为24字节的帧,其后面是键和无结构数据(Unstructured Data)。实际的格式如下(引自协议文档):
如上所示,包格式十分简单。需要注意的是,占据了16字节的头部(HEADER)分为请求头(Request Header)和响应头(Response Header)两种。头部中包含了表示包的有效性的Magic字节、命令种类、键长度、值长度等信息。
看到HEADER格式后我的感想是,键的上限太大了!现在的memcached规格中,键长度最大为250字节,但二进制协议中键的大小用2字节表示。因此,理论上最大可使用65536字节(216)长的键。尽管250字节以上的键并不会太常用,二进制协议发布之后就可以使用巨大的键了。二进制协议从下一版本1.3系列开始支持。
3.4 外部引擎支持
我去年曾经试验性地将memcached的存储层改造成了可扩展的(pluggable)。
MySQL的Brian Aker看到这个改造之后,就将代码发到了memcached的邮件列表。memcached的开发者也十分感兴趣,就放到了roadmap中。现在由我和memcached的开发者Trond Norbye协同开发(规格设计、实现和测试)。和国外协同开发时时差是个大问题,但抱着相同的愿景,最后终于可以将可扩展架构的原型公布了。代码库可以从 memcached 的下载页面 *问。
3.4.1 外部引擎支持的必要性
世界上有许多memcached的派生软件,其理由是希望永久保存数据、实现数据冗余等,即使牺牲一些性能也在所不惜。我在开发memcached之前,在mixi的研发部也曾经考虑过重新发明memcached。
外部引擎的加载机制能封装memcached的网络功能、事件处理等复杂的处理。因此,现阶段通过强制手段或重新设计等方式使memcached和存储引擎合作的困难就会烟消云散,尝试各种引擎就会变得轻而易举了。
3.4.2 简单API设计的成功的关键
该项目中我们最重视的是API设计。函数过多,会使引擎开发者感到麻烦;过于复杂,实现引擎的门槛就会过高。因此,最初版本的接口函数只有13个。具体内容限于篇幅,这里就省略了(可下载附件进行查看)。
3.4.3 重新审视现在的体系
memcached支持外部存储的难点是,网络和事件处理相关的代码(核心服务器)与内存存储的代码紧密关联。这种现象也称为tightly coupled(紧密耦合)。必须将内存存储的代码从核心服务器中独立出来,才能灵活地支持外部引擎。因此,基于我们设计的API,memcached被重构成下面的样子:
重构之后,我们与1.2.5版、二进制协议支持版等进行了性能对比,证实了它不会造成性能影响。在考虑如何支持外部引擎加载时,让memcached进行并行控制(concurrency control)的方案是最为容易的,但是对于引擎而言,并行控制正是性能的真谛,因此我们采用了将多线程支持完全交给引擎的设计方案。以后的改进,会使得memcached的应用范围更为广泛。
4. memcached的分布式算法
4.1 memcached的分布式
正如第 1 章 中介绍的那样,memcached虽然称为“分布式”缓存服务器,但服务器端并没有“分布式”功能。服务器端仅包括第 2 章 、第 3 章 前坂介绍的内存存储功能,其实现非常简单。至于memcached的分布式,则是完全由客户端程序库实现的。这种分布式是memcached的最大特点。
这里多次使用了“分布式”这个词,但并未做详细解释。现在开始简单地介绍一下其原理,各个客户端的实现基本相同。下面假设memcached服务器有node1~node3三台,应用程序要保存键名为“tokyo”、“kanagawa”、“chiba”、“saitama”、“gunma”的数据 。
首先向memcached中添加“tokyo”。将“tokyo”传给客户端程序库后,客户端实现的算法就会根据“键”来决定保存数据的memcached服务器。服务器选定后,即命令它保存“tokyo”及其值。
同样,“kanagawa”、“chiba”、“saitama”、“gunma”都是先选择服务器再保存。接下来获取保存的数据。获取时也要将要获取的键“tokyo”传递给函数库。函数库通过与数据保存时相同的算法,根据“键”选择服务器。使用的算法相同,就能选中与保存时相同的服务器,然后发送get命令。只要数据没有因为某些原因被删除,就能获得保存的值。
这样,将不同的键保存到不同的服务器上,就实现了memcached的分布式。memcached服务器增多后,键就会分散,即使一台memcached服务器发生故障无法连接,也不会影响其他的缓存,系统依然能继续运行。
4.2 分布式算法
4.2.1 根据余数计算分散
根据余数计算分散的分布式方法简单来说,就是“根据服务器台数的余数进行分散”。求得键的整数哈希值,再除以服务器台数,根据其余数来选择服务器。余数计算的方法简单,数据的分散性也相当优秀,但也有其缺点。那就是当添加或移除服务器时,缓存重组的代价相当巨大。添加服务器后,余数就会产生巨变,这样就无法获取与保存时相同的服务器,从而影响缓存的命中率。
4.2.2 Consistent Hashing
Consistent Hashing如下所示:首先求出memcached服务器(节点)的哈希值,并将其配置到0~232的圆(continuum)上。然后用同样的方法求出存储数据的键的哈希值,并映射到圆上。然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过232仍然找不到服务器,就会保存到第一台memcached服务器上。
从上图的状态中添加一台memcached服务器。余数分布式算法由于保存键的服务器会发生巨大变化而影响缓存的命中率,但Consistent Hashing中,只有在continuum上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响。
因此,Consistent Hashing最大限度地抑制了键的重新分布。而且,有的Consistent Hashing的实现方法还采用了虚拟节点的思想。使用一般的hash函数的话,服务器的映射地点的分布非常不均匀。因此,使用虚拟节点的思想,为每个物理节点(服务器)在continuum上分配100~200个点。这样就能抑制分布不均匀,最大限度地减小服务器增减时的缓存重新分布。
通过下文中介绍的使用Consistent Hashing算法的memcached客户端函数库进行测试的结果是,由服务器台数(n)和增加的服务器台数(m)计算增加服务器后的命中率计算公式如下:(1- n/(n+m)) * 100
5 实际使用架构
具体使用案例可参考附件。