接下来,看看memcached的内存分配方法。
按照memcached的set方法来说明,memcached在保存一条数据时做了哪一些操作,其内存是如何操作的。
- 先列出telnet指令,包括三个操作,连接memcached,保存一个数据,mykey=”abc”;最后使用stats slabs输出其slab信息。接下来,分析这三操作来看看,memcached内部是如何工作的,内存是怎么管理的。
<span style="font-size:18px;">$ <strong>telnet 127.0.0.1 11211</strong> Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. <strong>set mykey 0 3600 3</strong> abc STORED <strong>stats slabs</strong> STAT 1:chunk_size 80 STAT 1:chunks_per_page 13107 STAT 1:total_pages 1 STAT 1:total_chunks 13107 STAT 1:used_chunks 1 STAT 1:free_chunks 0 STAT 1:free_chunks_end 13106 STAT 1:mem_requested 57 STAT 1:get_hits 1 STAT 1:cmd_set 1 STAT 1:delete_hits 0 STAT 1:incr_hits 0 STAT 1:decr_hits 0 STAT 1:cas_hits 0 STAT 1:cas_badval 0 STAT active_slabs 1 STAT total_malloced 1048560 END </span>
- telnet连接操作。 在客户端进行连接之前tenlet连接前,服务器已经在监听相应端口了,而且创建了一个线程池,某些线程处于conn_listening状态,等待处理客户端连接。
客户端发起telnet 127.0.0.1 11211请求后,memcached将从连接池中拿出一个连接,或者新建一个连接,来响应请求。连接成功后,其线程状态变为conn_new_cmd。等待客户端输入命令 - set操作。
- 客户端输入set操作:
<span style="font-size:18px;">set mykey 0 3600<span style="color:#ff0000;"><strong> 3</strong> </span></span>
- 敲击确定之后,本行信息将发送给服务端,服务器触发conn_read状态。在读取这一行数据完毕之后,进入conn_parse_cmd状态。
- 服务器在conn_parse_cmd状态机中调用try_read_command()方法尝试对这一行进行此法分析。在process_command函数中,有详细的此法分析等流程。他检测到,本命令为set操作,且需要保存的长度为3.
- 虽然目前尚不知道需要保存的数据的值,但数据长度已经知道了,因此,调用item_alloc进行内存分配。
- 先加cache_lock锁。该锁是set/add/replace操作的互斥锁。即保证在同一个时刻,只能有一个写操作。
- 并调用do_item_alloc,给需要保存的数据申请内存。申请内存的计算公式:
<span style="font-size:18px;">/** * 计算存储数据需要的内存大小,包括头信息等。 * * key - The key * nkey - key的长度 * flags - key标志位 * nbytes - 包括\r\n在内的数据长度 * suffix - 一段由(flags, size)拼接起来的字符串, * nsuffix - suffix的长度. * * 返回值的长度(原注释中写的是头的长度,但根据源码分析得到,这个大小是包含数据在内的。) */ ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix); = sizeof(item) + (nkey + 1) + *nsuffix + nbytes; *nsuffix = strlen(" %d %d\r\n", flags, nbytes - 2 ) </span>
通过两个例子来说明:
1)建立一个key为a,保存一个1位长度的值,它将申请占用43个字节。<span style="font-size:18px;">set a 0 3600 1 nkey的长度为1,flags为0,nbyte长度为3 = 1+2(有\r\n), item结构体的长度为32(item定义可以参考memcached.h文件) ntotal = item_make_header(1 + 1, 0, 3, suffix, &nsuffix) = 32 + (1+1) + 6【strlen(" 0 1\r\n")】 + 3 = 43 </span>
2)建立一个key为a1234567890,保存一个80位长度的值。它将申请占用159个字节。
<span style="font-size:18px;">set a1234567890 0 3600 105 key的长度为11,flags为0,nbyte长度为105+2(有\r\n), item结构体的长度为32 ntotal = 32 + (11+1) + 8【strlen(" 0 105\r\n")】 + 107 = 159; </span>
- 获得需要的内存大小之后,通过slabs_clsid函数去找能够容下这个长度的slab id。
- 在这个slab尾部中快速的查找,看看是否有剩余的空间,或者尾部的50个item中有没有否过期的数据。如果找到,则使用这个item的内存空间。
- 如果在3.9中没有找到可以替换的item,则do_slabs_alloc,向系统申请一个新的slab,每个slab大小为1M。
- 若该slab尾部或slab用完了,则申请一个新的slab。slab大小为chunk size * perslab, 大约占用1M内存。若申请成功,则使用该slab的第一块空间
- 若内存申请不成功,比如memcached已经超过了设置的最大内存了。则在当前的slab的后50条数据中,使用一定的算法(最近最少使用算法)淘汰一个数据,并占用其空间片。至此,do_item_alloc方法结束。解锁cache_lock。
- 空间申请成功之后,将本item的值存入。memcpy(ITEM_data(new_it), buf, res);
- 因此将状态切换至conn_nread。等待用户再输入三个字符。
- 客户端输入”abc”换行,服务器conn_nread接收到值后,逐级调用函数complete_nread() -> complete_nread_ascii(),在检查完其输入(abc)长度是否与要求的长度(6)是否吻合之后,进入store_item(it, comm, c)函数存储该数据。其中参数it存储了需要保存的信息,comm围需要执行的命令,即set,c为当前连接信息。
- 进入do_store_item(),使用do_item_get()检查这个key是否存在,如果存在,记录该老数据的地址为old_it。最后保存数据,并将其建立hash关联。如果有老数据,则删除
- 至此,全部保存工作完成。将保存状态输出给客户端,并重置其状态位为conn_new_cmd
- 客户端输入set操作:
在整个过程中,发现到两个问题。
1. 在memcached内部,在做set,add, replace操作时会加cache_lock,这个所的级别很高,将影响整个memcache在同一个时刻只能写一个些操作。这个可以保证他的高性能么?还是我的理解有误?很怀疑这一个结论。
2. 在set过程中,memcached是为新值分配空间,再删除老空间。而不是直接使用老空间的内存。这使得保存逻辑更为简单,但很多情况下,一个key所对应的值长度是比较固定的,特别是在存储数字的时候。如果修改这里的逻辑,先测试老空间的大小能够容纳新空间,并尝试使用老空间的值。这样可以减少后期的内存分配申请流程,特别是过期及淘汰检查等等。