分布式存储及应用系统架构分析
文件更改摘要:
修订记录
日期
2010-4-4
创建
修订说明
修订人
批准人
龙兴平
目录
1 Memcached
1.1 Memcached 架构
memcached 是高性能的分布式内存缓存服务器。 一般的使用目的是,通过缓存数据库查询结
果,减少数据库访问次数,以提高动态 Web 应用的速度、 提高可扩展性。
龙兴平 (MSN: lxp8@sina.com)
第 2 页 共 83 页
2010-4-27
1.2 Memcache 实现分析理解
1.2.1 实现结构
Speed
Of course, the primary motivation for caching is speed, so Memcached is designed to be as fast as
possible. The initial prototype of Memcached was written in Perl. Although I love Perl, the
prototype was laughably slow and bloated. Perl trades off memory usage for everything, so a lot
of precious memory was wasted, and Perl can't handle tons of network connections at once.
The current version is written in C as a single-process, single-threaded, asynchronous I/O,
event-based d?mon. For portability and speed, we use libevent (see the on-line Resources section)
for event notification. The advantage of libevent is that it picks the best available strategy for
dealing with file descriptors at runtime. For example, it chooses kqueue on BSD and epoll on
Linux 2.6, which are efficient when dealing with thousands of concurrent connections. On other
systems, libevent falls back to the traditional poll and select methods.
Inside Memcached, all algorithms are O(1). That is, the runtime of the algorithms and CPU used
never varies with the number of concurrent clients, at least when using kqueue or epoll, or with
the size of the data or any other factor.
Of note, Memcached uses a slab allocator for memory allocation. Early versions of Memcached
used the malloc from glibc and ended up falling on their faces after about a week, eating up a lot
of CPU space due to address space fragmentation. A slab allocator allocates only large chunks of
memory, slicing them up into little chunks for particular classes of items, then maintaining
龙兴平 (MSN: lxp8@sina.com)
第 3 页 共 83 页
2010-4-27
freelists for each class whenever an object is freed. See the Bonwick paper in Resources for more
details. Memcached currently generates slab classes for all power-of-two sizes from 64 bytes to
1MB, and it allocates an object of the smallest size that can hold a submitted item. As a result of
using a slab allocator, we can guarantee performance over any length of time. Indeed, we've had
production Memcached servers up for 4–5 months at a time, averaging 7,000 queries/second,
without problems and maintaining consistently low CPU usage.
Another key requirement for Memcached was that it be lockless. All objects are multiversioned
internally and reference counted, so no client can block any other client's actions. If one client is
updating an object stored in Memcached while a dozen others are downloading it, even with one
client on a lossy network connection dropping half its packets, nobody has to wait for anybody
else.
A final optimization worth noting is that the protocol allows fetching multiple keys at once. This
is useful if your application knows it needs to load a few hundred keys. Instead of retrieving them
all sequentially, which would take a fraction of a second in network round-trips, the application
can fetch them all in one request. When necessary, the client libraries automatically split
multi-key loads from the application into separate parallel multi-key loads to the Memcached
instances. Alternatively, applications can provide explicit hash values with keys to keep groups of
data on the same instance. That also saves the client library a bit of CPU time by not needing to
calculate hash values.
Ref:
http://www.linuxjournal.com/article/7451?page=0,2
Memcached 是 danga.com(运营 LiveJournal 的技术团队)开发的一套分布式内存对象缓存
系统,用于在动态系统中减少数据库负载,提升性能。关于这个东西,相信很多人都用过,
本文意在通过对 memcached 的实现及代码分析,获得对这个出色的开源软件更深入的了解,
并可以根据我们的需要对其进行更进一步的优化。末了将通过对 BSM_Memcache 扩展的分
析,加深对 memcached 的使用方式理解。
本文的部分内容可能需要比较好的数学基础作为辅助。
◎Memcached 是什么
在阐述这个问题之前,我们首先要清楚它“不是什么”。很多人把它当作和 SharedMemory
那种形式的存储载体来使用,虽然 memcached 使用了同样的“Key=>Value”方式组织数据,
但是它和共享内存、APC 等本地缓存有非常大的区别。Memcached 是分布式的,也就是说
它不是本地的。它基于网络连接(当然它也可以使用 localhost)方式完成服务,本身它是一
个独立于应用的程序或守护进程(Daemon 方式)。
Memcached 使用 libevent 库实现网络连接服务,理论上可以处理无限多的连接,但是它和
Apache 不同,它更多的时候是面向稳定的持续连接的,所以它实际的并发能力是有限制的。
在保守情况下 memcached 的最大同时连接数为 200,这和 Linux 线程能力有关系,这个数值
龙兴平 (MSN: lxp8@sina.com)
第 4 页 共 83 页
2010-4-27
是可以调整的。关于 libevent 可以参考相关文档。 Memcached 内存使用方式也和 APC 不同。
APC 是基于共享内存和 MMAP 的,memcachd 有自己的内存分配算法和管理方式,它和共
享内存没有关系,也没有共享内存的限制,通常情况下,每个 memcached 进程可以管理 2GB
的内存空间,如果需要更多的空间,可以增加进程数。
◎Memcached 适合什么场合
在很多时候,memcached 都被滥用了,这当然少不了对它的抱怨。我经常在论坛上看见有
人发贴,类似于“如何提高效率”,回复是“用 memcached”,至于怎么用,用在哪里,用
来干什么一句没有。memcached 不是万能的,它也不是适用在所有场合。
Memcached 是“分布式”的内存对象缓存系统,那么就是说,那些不需要“分布”的,不
需要共享的,或者干脆规模小到只有一台服务器的应用,memcached 不会带来任何好处,
相反还会拖慢系统效率,因为网络连接同样需要资源,即使是 UNIX 本地连接也一样。 在
我之前的测试数据中显示,memcached 本地读写速度要比直接 PHP 内存数组慢几十倍,而
APC、共享内存方式都和直接数组差不多。可见,如果只是本地级缓存,使用 memcached
是非常不划算的。
Memcached 在很多时候都是作为数据库前端 cache 使用的。因为它比数据库少了很多 SQL
解析、磁盘操作等开销,而且它是使用内存来管理数据的,所以它可以提供比直接读取数
据库更好的性能,在大型系统中,访问同样的数据是很频繁的,memcached 可以大大降低
数据库压力,使系统执行效率提升。另外,memcached 也经常作为服务器之间数据共享的
存储媒介,例如在 SSO 系统中保存系统单点登陆状态的数据就可以保存在 memcached 中,
被多个应用共享。
需要注意的是,memcached 使用内存管理数据,所以它是易失的,当服务器重启,或者
memcached 进程中止,数据便会丢失,所以 memcached 不能用来持久保存数据。很多人的
错误理解,memcached 的性能非常好,好到了内存和硬盘的对比程度,其实 memcached 使
用内存并不会得到成百上千的读写速度提高,它的实际瓶颈在于网络连接,它和使用磁盘
的数据库系统相比,好处在于它本身非常“轻”,因为没有过多的开销和直接的读写方式,
它可以轻松应付非常大的数据交换量,所以经常会出现两条千兆网络带宽都满负荷了,
memcached 进程本身并不占用多少 CPU 资源的情况。
◎Memcached 的工作方式
以下的部分中,读者最好能准备一份 memcached 的源代码。
Memcached 是传统的网络服务程序,如果启动的时候使用了-d 参数,它会以守护进程的方
式执行。创建守护进程由 daemon.c 完成,这个程序只有一个 daemon 函数,这个函数很简
单(如无特殊说明,代码以 1.2.1 为准):
CODE:
#include
#include
龙兴平 (MSN: lxp8@sina.com)
第 5 页 共 83 页
2010-4-27
#include
int
daemon(nochdir, noclose)
int nochdir, noclose;
{
int fd;
switch (fork()) {
case -1:
return (-1);
case 0:
break;
default:
_exit(0);
}
if (setsid() == -1)
return (-1);
if (!nochdir)
(void)chdir(”/”);
if (!noclose && (fd = open(”/dev/null”, O_RDWR, 0)) != -1) {
(void)dup2(fd, STDIN_FILENO);
(void)dup2(fd, STDOUT_FILENO);
(void)dup2(fd, STDERR_FILENO);
if (fd > STDERR_FILENO)
(void)close(fd);
}
return (0);
}
这个函数 fork 了整个进程之后,父进程就退出,接着重新定位 STDIN 、 STDOUT 、
STDERR 到空设备, daemon 就建立成功了。
Memcached 本身的启动过程,在 memcached.c 的 main 函数中顺序如下:
1 、调用 settings_init() 设定初始化参数
2 、从启动命令中读取参数来设置 setting 值
3 、设定 LIMIT 参数
4 、开始网络 socket 监听(如果非 socketpath 存在)( 1.2 之后支持 UDP 方式)
5 、检查用户身份( Memcached 不允许 root 身份启动)
6 、如果有 socketpath 存在,开启 UNIX 本地连接(Sock 管道)
7 、如果以 -d 方式启动,创建守护进程(如上调用 daemon 函数)
龙兴平 (MSN: lxp8@sina.com)
第 6 页 共 83 页
2010-4-27
8 、初始化 item 、 event 、状态信息、 hash 、连接、 slab
9 、如设置中 managed 生效,创建 bucket 数组
10 、检查是否需要锁定内存页
11 、初始化信号、连接、删除队列
12 、如果 daemon 方式,处理进程 ID
13 、event 开始,启动过程结束, main 函数进入循环。
在 daemon 方式中,因为 stderr 已经被定向到黑洞,所以不会反馈执行中的可见错误信息。
memcached.c 的主循环函数是 drive_machine ,传入参数是指向当前的连接的结构指针,根
据 state 成员的状态来决定动作。
Memcached 使 用 一 套 自 定 义 的 协 议 完 成 数 据 交 换 , 它 的 protocol 文 档 可 以 参 考 :
http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt
在 API 中,换行符号统一为\r\n
◎Memcached 的内存管理方式
Memcached 有一个很有特色的内存管理方式,为了提高效率,它使用预申请和分组的方式
管理内存空间,而并不是每次需要写入数据的时候去 malloc,删除数据的时候 free 一个指
针。Memcached 使用 slab->chunk 的组织方式管理内存。
1.1 和 1.2 的 slabs.c 中的 slab 空间划分算法有一些不同,后面会分别介绍。
Slab 可以理解为一个内存块,一个 slab 是 memcached 一次申请内存的最小单位,在
memcached 中,一个 slab 的大小默认为 1048576 字节(1MB),所以 memcached 都是整 MB
的使用内存。每一个 slab 被划分为若干个 chunk,每个 chunk 里保存一个 item,每个 item
同时包含了 item 结构体、key 和 value(注意在 memcached 中的 value 是只有字符串的)。slab
按照自己的 id 分别组成链表,这些链表又按 id 挂在一个 slabclass 数组上,整个结构看起来
有点像二维数组。slabclass 的长度在 1.1 中是 21,在 1.2 中是 200。
slab 有一个初始 chunk 大小,1.1 中是 1 字节,1.2 中是 80 字节,1.2 中有一个 factor 值,默
认为 1.25
在 1.1 中,chunk 大小表示为初始大小*2^n,n 为 classid,即:id 为 0 的 slab,每 chunk 大
小 1 字节,id 为 1 的 slab,每 chunk 大小 2 字节,id 为 2 的 slab,每 chunk 大小 4 字节……
id 为 20 的 slab,每 chunk 大小为 1MB,就是说 id 为 20 的 slab 里只有一个 chunk:
CODE:
void slabs_init(size_t limit) {
int i;
int size=1;
龙兴平 (MSN: lxp8@sina.com)
第 7 页 共 83 页
2010-4-27
mem_limit = limit;
for(i=0; i<=POWER_LARGEST; i++, size*=2) {
slabclass[i].size = size;
slabclass[i].perslab = POWER_BLOCK / size;
slabclass[i].slots = 0;
slabclass[i].sl_curr = slabclass[i].sl_total = slabclass[i].slabs = 0;
slabclass[i].end_page_ptr = 0;
slabclass[i].end_page_free = 0;
slabclass[i].slab_list = 0;
slabclass[i].list_size = 0;
slabclass[i].killing = 0;
}
/* for the test suite: faking of how much we’ve already malloc’d */
{
char *t_initial_malloc = getenv(”T_MEMD_INITIAL_MALLOC”);
if (t_initial_malloc) {
mem_malloced = atol(getenv(”T_MEMD_INITIAL_MALLOC”));
}
}
/* pre-allocate slabs by default, unless the environment variable
for testing is set to something non-zero */
{
char *pre_alloc = getenv(”T_MEMD_SLABS_ALLOC”);
if (!pre_alloc || atoi(pre_alloc)) {
slabs_preallocate(limit / POWER_BLOCK);
}
}
}
在 1.2 中,chunk 大小表示为初始大小*f^n,f 为 factor,在 memcached.c 中定义,n 为 classid,
同时,201 个头不是全部都要初始化的,因为 factor 可变,初始化只循环到计算出的大小达
到 slab 大小的一半为止,而且它是从 id1 开始的,即:id 为 1 的 slab,每 chunk 大小 80 字
节,id 为 2 的 slab,每 chunk 大小 80*f,id 为 3 的 slab,每 chunk 大小 80*f^2,初始化大小
有 一 个 修 正 值 CHUNK_ALIGN_BYTES , 用 来 保 证 n-byte 排 列 ( 保 证 结 果 是
CHUNK_ALIGN_BYTES 的整倍数)。这样,在标准情况下,memcached1.2 会初始化到 id40,
这个 slab 中每个 chunk 大小为 504692,每个 slab 中有两个 chunk。最后,slab_init 函数会在
最后补足一个 id41,它是整块的,也就是这个 slab 中只有一个 1MB 大的 chunk:
CODE:
void slabs_init(size_t limit, double factor) {
int i = POWER_SMALLEST – 1;
unsigned int size = sizeof(item) + settings.chunk_size;
龙兴平 (MSN: lxp8@sina.com)
第 8 页 共 83 页
2010-4-27