1、介绍
(1)说明
Memcached是分布式的,基于网络连接(当然它也可以使用localhost)方式完成服务,本身它是一个独立于应用的程序。Memcached使用libevent库实现网络连接服务,理论上可以处理无限多的连接,但是它和Apache不同,它更多的时候是面向稳定的持续连接的,所以它实际的并发能力是有限制的。在保守情况下memcached的最大同时连接数为200,这和Linux线程能力有关系,这个数值是可以调整的。
memcachd有自己的内存分配算法和管理方式,通常情况下,每个memcached进程可以管理2GB的内存空间,如果需要更多的空间,可以增加进程数。
(2)本地缓存与memcached
Memcached是“分布式”的内存对象缓存系统,那么就是说,那些不需要“分布”的,不需要共享的,或者干脆规模小到只有一台服务器的应用,memcached不会带来任何好处,相反还会拖慢系统效率,因为网络连接同样需要资源,即使是UNIX本地连接也一样。
在之前的测试数据中显示,memcached本地读写速度要比直接PHP内存数组慢几十倍,而APC、共享内存方式都和直接数组差不多。可见,如果只是本地级缓存,使用memcached是非常不划算的。
(3)作为cache
Memcached在很多时候都是作为数据库前端cache使用的。因为它比数据库少了很多SQL解析、磁盘操作等开销,而且它是使用内存来管理数据的,所以它可以提供比直接读取数据库更好的性能,在大型系统中,访问同样的数据是很频繁的,memcached可以大大降低数据库压力,使系统执行效率提升。
memcached使用内存管理数据,所以它是易失的,当服务器重启,或者memcached进程中止,数据便会丢失,所以memcached不能用来持久保存数据。
应用上,memcached可作为服务器之间数据共享的存储媒介,例如在SSO系统中保存系统单点登陆状态的数据就可以保存在memcached中,被多个应用共享。
(4)性能
memcached使用内存并不会得到成百上千的读写速度提高(跟硬盘相比)。它和使用磁盘的数据库系统相比,好处在于没有过多的io开销,进程本身也不占用多少cpu资源,利于处理非常大的数据交换量。
实际瓶颈在于网络连接,所以经常会出现两条千兆网络带宽都满负荷了。
2、工作方式
(1)守护进程
Memcached是传统的网络服务程序,如果启动的时候使用了-d参数,它会以守护进程的方式执行。
创建守护进程,代码如下(daemon.c,版本1.2.1):
fork 子进程,退出父进程,重定向 到空设备文件(STDIN 、 STDOUT 、 STDERR )
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int daemon(int nochdir, int noclose)
{
int fd;
switch (fork()) {
case -1://fork错误
return (-1);
case 0: //子进程
break;
default://父进程
_exit(0);
}
//调用setsid函数的进程成为新的会话的领头进程,并与其父进程的会话组和进程组脱离。
//由于会话对控制终端的独占性,进程同时与控制终端脱离
if (setsid() == -1)
return (-1);
if (!nochdir)//设置当前目录到根目录
(void)chdir("/");
if (!noclose && (fd = open("/dev/null", O_RDWR, 0)) != -1) {//获取空设备文件句柄
(void)dup2(fd, STDIN_FILENO);//重定向STDIN到空设备文件
(void)dup2(fd, STDOUT_FILENO);//重定向STDOUT到空设备文件
(void)dup2(fd, STDERR_FILENO);//重定向STDERR到空设备文件
if (fd > STDERR_FILENO)
(void)close(fd);
}
return (0);
}
(2)启动过程
main 函数中执行步骤(memcached.c):
1>调用 settings_init() 设定初始化参数2 >从启动命令中读取参数来设置 setting 值
3 >设定 LIMIT 参数
4 >开始网络 socket 监听(如果非 socketpath 存在)(1.2 之后支持 UDP 方式)
5 >检查用户身份( Memcached 不允许 root 身份启动)
6 >如果有 socketpath 存在,开启 UNIX 本地连接(Sock 管道)
7 >如果以 -d 方式启动,创建守护进程(如上调用 daemon 函数)
8 >初始化 item 、 event 、状态信息、 hash 、连接、 slab
9 >如设置 managed 生效,创建 bucket 数组
10> 检查是否需要锁定内存页
11 >初始化信号、连接、删除队列
12 >如果 daemon 方式,处理进程 ID
13 >event 开始,启动过程结束, main 函数进入循环。
启动方式
# /usr/local/bin/memcached -d -m 10 -u root -l 192.168.0.200 -p 12000 -c 256 -P /tmp/memcached.pid
-d选项是启动一个守护进程,
-m是分配给Memcache使用的内存数量,单位是MB,这里是10MB,
-u是运行Memcache的用户,这里是root,
-l是监听的服务器IP地址,如果有多个地址的话,这里指定了服务器的IP地址192.168.0.200,
-p是设置Memcache监听的端口,这里设置了12000,最好是1024以上的端口,
-c选项是最大运行的并发连接数,默认是1024,这里设置了256,按照服务器的负载量来设定,
-P是设置保存Memcache的进程id文件,这里是保存在 /tmp/memcached.pid,
结束方式
# kill `cat /tmp/memcached.pid`
(3)自定义协议
Memcached 使用一套自定义的协议通信在API中,换行符号统一为\r\n
3、内存管理方式
1.Chunk为固定大小的内存空间,默认为96Byte。
2.page对应实际的物理空间,1个page为1M。
3.同样大小的chunk又称为slab。
Memcached启动的时候根据-n和-f参数,产生若干slab。具体应用中Memcache每次申请1page,并将这1M空间分割成若干个chunk,这些chunk有着同样的大小,属于同一个slab。
(1)内存设计
1)添加内存块Chunk通过memcache添加item的时候:
1. Memcache计算item的大小(key+value+flags),选取合适的slab(刚好能放下该item的slab)
2. 如果这个item对应的slab未出现过,则申请1个page(注意,这1M空间不论是否达到memcached使用内存都可以申请成功)并加该item存入slab中的chunk
3. 如果item对应的slab出现过,则在该slab中优先选择expired(free_chunks)和delete(在1.2.2中delete的chunk存在着不能被重复利用的问题)的chunk进行存储,其次将选择未使用过的chunk(free_chunks_end)进行存储。
4. 如果item对应的slab出现过,但是对应的slab已经存储满了,那么会申请一个新的page,这个page被分为对应大小的chunk,继续存储。
5. 如果item对应的slab出现过,但是对应的slab已经存储满了并且memcache也达到了最大内存使用。将使用lru算法,清除item(可能将未过期的item清除)
2)删除内存块Chunk
1. Delete操作只是将该chunk置为删除状态,这样在下次使用将优先利用这样的chunk。
3)内存失效
注意:
1. memcache已经分配的内存不会再主动清理。
2. memcache分配给某个slab的内存页不能再分配给其他slab。
3. flush_all不能重置memcache分配内存页的格局,只是给所有的item置为过期。
4. memcache最大存储的item(key+value)大小限制为1M,这由page大小1M限制
5.由于memcache的分布式是客户端程序通过hash算法得到的key取模来实现,不同的语言可能会采用不同的hash算法,同样的客户端程序也有可能使用相异的方法,因此在多语言、多模块共用同一组memcached服务时,一定要注意在客户端选择相同的hash算法
6.启动memcached时可以通过-M参数禁止LRU替换,在内存用尽时add和set会返回失败
7.memcached启动时指定的是数据存储量,没有包括本身占用的内存、以及为了保存数据而设置的管理空间。因此它占用的内存量会多于启动时指定的内存分配量,这点需要注意。
8.memcache存储的时候对key的长度有限制,php和C的最大长度都是250
(2)内存块
1)内存申请
memcached一次申请内存的最小单位为一个slab,一个slab的大小默认为1048576字节(1MB)。
每一个slab被划分为若干个chunk,每个chunk里保存一个item,每个item同时包含了item结构体、key和value(注意在memcached中的value是只有字符串的)。
slab按照自己的id分别组成链表,链表按id挂在一个slabclass数组上(slabclass的长度在1.1中是21,在1.2中是200)。
2)内存块大小
slab有一个初始块 ,1.1中是1字节,1.2中是80字节,1.2中有一个factor值,默认为1.25。
3)内存块初始化
在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。
void slabs_init(size_t limit) {
int i;
int size=1;
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,每个slab中有两个chunk,其中每个chunk大小为504692,最后再补足一个id41,它是整块的,也就是这个slab中只有一个1MB大的chunk。
初始化如下:
void slabs_init(size_t limit, double factor) {
int i = POWER_SMALLEST - 1;
unsigned int size = sizeof(item) + settings.chunk_size;
/* Factor of 2.0 means use the default memcached behavior */
if (factor == 2.0 && size < 128)
size = 128;
mem_limit = limit;
memset(slabclass, 0, sizeof(slabclass));
while (++i < POWER_LARGEST && size <= POWER_BLOCK / 2) {
/* Make sure items are always n-byte aligned */
if (size % CHUNK_ALIGN_BYTES)
size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);
slabclass[i].size = size;
slabclass[i].perslab = POWER_BLOCK / slabclass[i].size;
size *= factor;
if (settings.verbose > 1) {
fprintf(stderr, "slab class %3d: chunk size %6d perslab %5d\n",
i, slabclass[i].size, slabclass[i].perslab);
}
}
power_largest = i;
slabclass[power_largest].size = POWER_BLOCK;
slabclass[power_largest].perslab = 1;
/* 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"));
}
}
#ifndef DONT_PREALLOC_SLABS
{
char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC");
if (!pre_alloc || atoi(pre_alloc)) {
slabs_preallocate(limit / POWER_BLOCK);
}
}
#endif
}
由上可以看出,memcached的内存分配是有冗余的,当一个slab不能被它所拥有的chunk大小整除时,slab尾部剩余的空间就被丢弃了,如id40中,两个chunk占用了1009384字节,这个slab一共有1MB,那么就有39192字节被浪费了。
Memcached使用这种方式来分配内存,是为了可以快速的通过item长度定位出slab的classid,有一点类似hash,因为item的长度是可以计算的,比如一个item的长度是300字节,在1.2中就可以得到它应该保存在id7的slab中,因为按照上面的计算方法,id6的chunk大小是252字节,id7的chunk大小是316字节,id8的chunk大小是396字节,表示所有252到316字节的item都应该保存在id7中。同理,在1.1中,也可以计算得到它出于256和512之间,应该放在chunk_size为512的id9中(32位系统)。
Memcached初始化的时候,会初始化slab(在main函数中调用了的函数slabs_init())。
函数slabs_init()中的所有已经定义过的slabclass中,每一个id创建一个slab。
1.2在默认的环境中启动进程后要分配41MB的slab空间,因为有可能一个id根本没有被使用过,但是它也默认申请了一个slab,每个slab会用掉1MB内存
当一个slab用光后,又有新的item要插入这个id,那么它就会重新申请新的slab,申请新的slab时,对应id的slab链表就要增长,这个链表是成倍增长的,在函数grow_slab_list函数中,这个链的长度从1变成2,从2变成4,从4变成8……:在定位item时,使用slabs_clsid函数,传入参数为item大小,返回值为classid。
item总是小于或等于chunk大小的,当item小于chunk大小时,会发生了空间浪费。
(3)内存管理结构
Memcached的数据保存基于一个hash表(primary_hashtable),其成员为数据项item。
数据项的地址是slab中的chunk偏移,其定位是依靠对key做hash的结果。(在assoc.c和items.c中定义了hash和item操作)
item是数据项(items.c),包含部分:
key:键
nkey:键长
flags:用户定义的flag(其实这个flag在memcached中没有启用)
nbytes:值长(包括换行符号\r\n)
suffix:后缀Buffer
nsuffix:后缀长
item长度为:键长+值长+后缀长+item结构大小(32字节),根据item长度来计算slab的classid。
其哈希表使用NewHash算法,效率很高。(1.1和1.2的NewHash有一些不同,主要的实现方式还是一样的,1.2的hash函数是经过整理优化的。NewHash的原型参考:http://burtleburtle.net/bob/hash/evahash.html)
hashtable表长为hashsize(HASHPOWER),就是4MB(hashsize是一个宏,表示1右移n位),1.2中是变量16,即hashtable表长65536,1.1中HASHPOWER常量为20:
typedef unsigned long int ub4; /* unsigned 4-byte quantities */
typedef unsigned char ub1; /* unsigned 1-byte quantities */
#define hashsize(n) ((ub4)1<<(n))
#define hashmask(n) (hashsize(n)-1)
hash表操作:
assoc_init(),assoc_find()、assoc_expand()、assoc_move_next_bucket()、assoc_insert()、assoc_delete()。
其中assoc_find()是根据key和key长寻找对应的item地址的函数,返回的是item结构指针(其地址在slab中的某个chunk上)。
数据项操作:item_init()初始化了heads、tails、sizes三个数组为0,三个数组的大小都为常量LARGEST_ID(默认为255,这个值需要配合factor来修改)
item_assoc()会先尝试从slab中获取一块空闲的chunk,如果没有可用的chunk,会在链表中扫描50次,以得到一个被LRU踢掉的item,将它unlink,然后将需要插入的item插入链表中。
(4)内存回收
使用引用计数(item的refcount成员)。item被unlink之后只是从链表上摘掉,不是立刻就被free的,只是将它放到删除队列中(item_unlink_q()函数)。item对应的操作,包括remove、update、replace,当然最重要的就是alloc操作。
item还有一个特性就是它有过期时间,这是memcached的一个很有用的特性,很多应用都是依赖于memcached的item过期,比如session存储、操作锁等。item_flush_expired()函数就是扫描表中的item,对过期的item执行unlink操作,当然这只是一个回收动作,实际上在get的时候还要进行时间判断:
/* expires items that are more recent than the oldest_live setting. */
void item_flush_expired() {
int i;
item *iter, *next;
if (! settings.oldest_live)
return;
for (i = 0; i < LARGEST_ID; i++) {
/* The LRU is sorted in decreasing time order, and an item's timestamp
* is never newer than its last access time, so we only need to walk
* back until we hit an item older than the oldest_live time.
* The oldest_live checking will auto-expire the remaining items.
*/
for (iter = heads[i]; iter != NULL; iter = next) {
if (iter->time >= settings.oldest_live) {
next = iter->next;
if ((iter->it_flags & ITEM_SLABBED) == 0) {
item_unlink(iter);
}
} else {
/* We've hit the first old item. Continue to the next queue. */
break;
}
}
}
}
/* wrapper around assoc_find which does the lazy expiration/deletion logic */
item *get_item_notedeleted(char *key, size_t nkey, int *delete_locked) {
item *it = assoc_find(key, nkey);
if (delete_locked) *delete_locked = 0;
if (it && (it->it_flags & ITEM_DELETED)) {
/* it's flagged as delete-locked. let's see if that condition
is past due, and the 5-second delete_timer just hasn't
gotten to it yet... */
if (! item_delete_lock_over(it)) {
if (delete_locked) *delete_locked = 1;
it = 0;
}
}
if (it && settings.oldest_live && settings.oldest_live <= current_time &&
it->time <= settings.oldest_live) {
item_unlink(it);
it = 0;
}
if (it && it->exptime && it->exptime <= current_time) {
item_unlink(it);
it = 0;
}
return it;
}
4、特点和限制
(1) 工作参数
1.最大30天的过期时间: 常量REALTIME_MAXDELTA 60*60*24*302.最大同时连接数: conn_init()中的freetotal(=200)
3.最大键长: 常量KEY_MAX_LENGTH 250
4.factor将影响chunk的步进大小: settings.factor(=1.25)
5.最大软连接: settings.maxconns(=1024)6.块的大小: settings.chunk_size(=48)
一个保守估计的key+value长度,用来生成id1中的chunk长度(1.2)。id1的chunk长度等于这个数值加上item结构体的长度(32),即默认的80字节。
7.classid
常量POWER_SMALLEST 1
最小classid(1.2)常量POWER_LARGEST 200
最大classid(1.2)
8)默认slab大小: 常量POWER_BLOCK 1048576,其中会常量CHUNK_ALIGN_BYTES (sizeof(void *))保证chunk大小是这个数值的整数倍,字节对齐,(void *的长度在不同系统上不一样,在标准32位系统上是4)
9)队列刷新间隔: 常量ITEM_UPDATE_INTERVAL 60,常量LARGEST_ID 255
10)最大item链表数: 这个值不能比最大的classid小
11)hashtable的大小: 变量hashpower决定(在1.1中是常量HASHPOWER)
(2)特点总结
1)在 Memcached中可以保存的item数据量是没有限制的,只要内存足够。2)假设NewHash算法碰撞均匀,查找item的循环次数是item总数除以hashtable大小(由hashpower决定),是线性的。
3)Memcached限制了可以接受的最大item是1MB,大于1MB的数据不予理会。
4)Memcached的空间利用率和数据特性有很大的关系,又与DONT_PREALLOC_SLABS常量有关。 在最差情况下,有198个slab会被浪费(所有item都集中在一个slab中,199个id全部分配满)
5)Memcached单进程最大使用内存为2G,要使用更多内存,可以分多个端口开启多个Memcached进程,最大30天的数据过期时间,设置为永久的也会在这个时间过期
6)常量REALTIME_MAXDELTA 60*60*24*30控制最大键长为250字节,大于该长度无法存储
7)常量KEY_MAX_LENGTH 250控制单个item最大数据是1MB,超过1MB数据不予存储
8)常量POWER_BLOCK 1048576进行控制,它是默认的slab大小 最大同时连接数是200,通过 conn_init()中的freetotal进行控制,最大软连接数是1024,通过settings.maxconns=1024 进行控制跟空间占用相关的参数:settings.factor=1.25, settings.chunk_size=48, 影响slab的数据占用和步进方式memcached是一种无阻塞的socket通信方式服务,基于libevent库,由于无阻塞通信,对内存读写速度非常之快。
9)memcached是键值一一对应,key默认最大不能超过128个字 节,value默认大小是1M,也就是一个slabs,如果要存2M的值(连续的),不能用两个slabs,因为两个slabs不是连续的,无法在内存中 存储。
5、定长优化
Memcached本身是为变长数据设计的,根据数据特性,可以说它是“面向大众”的设计,但是很多时候,我们的数据并不是这样的“普遍”,典型的情况中,一种是非均匀分布,即数据长度集中在几个区域内(如保存用户 Session);或者是等长数据(如定长键值,定长数据,多见于访问、在线统计或执行锁)。解决定长数据,首先需要解决的是slab的分配问题,第一个需要确认的是我们不需要那么多不同chunk长度的slab,为了最大限度地利用资源,最好chunk和item等长。
item的长度是字符串长度加上item头结构的长度32字节。
以下假设item长度是200字节。
在原始版本中,chunk长度和classid是有对应关系的,现在如果把所有的chunk都定为200个字节,那么这个关系就不存在了,我们需要重新确定这二者的关系。
方法一:整个存储结构只使用一个固定的id,即只使用199个槽中的1个,在这种条件下,就一定要定义DONT_PREALLOC_SLABS来避免另外的预分配浪费。
方法二:不使用长度来做键,可以使用key的NewHash结果或者直接根据key来做hash(定长数据的key也一定等长)。
第一种方法的不足之处在于只使用一个id,在数据量非常大的情况下,slab链会很长(因为所有数据都挤在一条链上了),遍历起来的代价比较高。
前面介绍了三种空间冗余,设置chunk长度等于item长度,解决了第一种空间浪费问题,不预申请空间解决了第二种空间浪费问题,那么对于第一种问题(slab内剩余)如何解决呢,这就需要修改POWER_BLOCK常量,使得每一个slab大小正好等于chunk长度的整数倍,这样一个slab就可以正好划分成n个chunk。这个数值应该比较接近1MB,过大的话同样会造成冗余,过小的话会造成次数过多的alloc,根据chunk长度为200,选择1000000作为POWER_BLOCK的值,这样一个slab就是100万字节,不是1048576。三个冗余问题都解决了,空间利用率会大大提升。
unsigned int slabs_clsid(size_t size) {
return 1;
}
修改slabs_init函数,去掉循环创建所有classid属性的部分,直接添加slabclass[1]:
slabclass[1].size = 200; //每chunk 200字节
slabclass[1].perslab = 5000; //1000000/200