写在前面:解读过程中使用的术语或变量名称,尽量和源码中的保持一致。这样,便于大家学习参考。若是某些名称和源码中的存在冲突,则以源码为准。该软件版本是:Memcached-1.4.5 在这篇博文中,我将带领大家探究memcached软件的内存管理技术。涉及到的数据结构分别有primary_hashtable, slabclass, heads (Type is item *heads[LARGEST_ID]), tails (Type is item *tails[LARGEST_ID])等。其中,LARGEST_ID的值是POWER_LARGEST(=200)。
需要说明的是这里我们只只做ascii_prot相关技术的解读,binary_prot相关技术可参照ascii_prot。
- 缓存空间的创建
slabclass是用来管理缓存空间的数据结构。在软件开始运行时,需要初始化用于维护其结构特性的一些参数,其中主要有size, perslab, list_size, slab_list等。它们的具体含义详见《memcached源码级执行流程解读》一文。
对于每一个slabclass[.list_size]槽位中空间的分配,主要是在do_slabs_newslabs()中完成的。当由于list_size的数值过小,而无法满足新空间的分配时,需要调用grow_slab_list()函数,通过realloc()函数将list_size扩大为原来的2倍。而后在通过memory_allocate()函数为slabclass[.list_size].slab_list[.slabs]分配(内容)空间。实质上,slabclass[.list_size].slab_list[.slabs]中的(内容)空间才是真正的缓存空间,它内部缓存块的数量有.perslab标识,缓存快的大小由.size标识。
- 申请缓存
当服务器收到客户端发送过来的请求后,服务器会依据命令的情况决定是否需要申请一个item作为接受后续数据(data)的缓存空间。这里我们假设服务器需要申请该空间。
服务器在conn_parse_cmd状态中将分析客户端发送过来的命令,并从中提取出key, nkey(key的长度), exptime(超时时间),data_len(数据块长度)。得到这些信息后,服务器将调用item_alloc()函数获得一个空闲的item。注意该函数是多线程可访问的,需要锁的保护。在这里个函数里,又调用了do_item_alloc()函数。在函数do_item_alloc()函数中,首先获取能够存放该数据块(长度为data_len)的最小缓存块所在的slabclass槽位ID,然后调用slabs_alloc()函数获得实际的缓存块。
slabs_alloc()函数也是多线程可访问的,因此也需要锁的保护。在该函数中,服务器调用了do_slabs_alloc()函数,它是最终操作缓存块分配的底层函数。在这里,它根据之前已经获得ID,取出待操作的slabclass槽位(p=&slabclass[id])。由于p->end_page_ptr是指向末尾的可使用的缓存空间的首地址指针,但是可以为NULL。若p->end_page_ptr为NULL,则表明此时缓存中没有可用的缓存块了。出现这种情况可能是内存使用量超过了list_size或者系统内存被耗尽了。但若是使用量达到了list_size,而没有出现系统内存耗尽的时候,可以通过do_slabs_newslab()函数为slabclass[id]扩容,这样就可以继续申请缓存块了。这是申请到的内存块的首地址就是p->end_page_ptr,长度为p->size。
slabclass[id]中有一个成员变量:slots,是一个二维指针。它主要是用来回收使用过的被废弃的缓存块,并使其能重新被利用。当服务器需要申请新的缓存块时,就可以通过检查p->sl_curr的值,判读垃圾收集器slots中是否有可重新被利用的缓存块。若有的话,则可以通过p->slots[p->sl_curr]操作将缓存块取出,以重新用于数据的缓存。
- 缓存块的使用
好了,现在我们已经得到了空间的可使用的缓存块(由item结构来管理)。那么接下来就是往item的缓冲区中读取数据,该过程略。
待读完数据后,服务器将在conn_nread状态中调用complete_nread()函数处理接收到的数据。在这个函数中,服务器complete_nread_ascii()函数调用store_item()函数把数据存储起来。
store_item()函数也是一个多线程可访问的,同样也需要锁的保护。在这里,服务器通过调用do_store_item()函数并根据客户端的命令来操作数据。这里假设是"set"命令。那么,在do_store_item()函数中将会调用do_item_link()函数将item挂接在primary_hashtable上。该挂接工作是在assoc_insert()函数中进行的。
讲到assoc_insert()函数,不得不提一下它完成的另一项工作:哈希数组空间扩展(expanding)。当当前item的数量超过了哈希数组空间的1.5倍的时候,就需要启动空间扩展策略了。这种机制保证了冲突链的平均有效长度不超过2个节点。哈希数组空间的扩展工作是在assoc_expanding()函数中完成的。在该函数中,通过calloc()函数申请一个是原先容量两倍大的哈希数组空间。并将原先的哈希数组空间标志为old_hashtable,接下来就通过pthread_cond_signal(&maintenance_cond)将睡眠在maintenance_cond上的维护线程唤醒。维护线程的工作工程在《memcached源码级执行流程解读》一文中已有详细介绍。
待item被成功的挂接到primary_hashtable上之后,还需要做的一项工作是:调用item_link_q()函数将item挂接在heads上。heads是一个和slabclass具有相同槽位数量的数组队列管理结构。每个槽位的第一个队列节点是头结点,队列的尾节点由tails(和heads的结构完全相同)指示。这里不对heads和tails作太多的介绍,但是需要突出讲的是它们的作用:就是为了缓解primary_hashtable的访问压力,从而提高memcached整体的服务性能。memcached可以使用它来标记超时节点,统计系统内部的各种状态数据等等。
- 缓存块的释放
由于item是一个智能指针结构,那么对他的释放必须是在memcached对它的引用计数变为零的时候才可以进行。当然,在对它释放前需要先将其从primary_hashtable和heads中分离出来,完成这项工作的函数分别是assoc_delete()和item_unlink_q()。
而后就可以调用item_free()函数了。在item_free()函数中首先通过ITEM_ntotal()宏获得item所在缓存块的总长度(包括头部和数据),而后调用slabs_free()函数完成缓存块的释放工作。注意,slabs_free()函数也是一个多线程可访问的函数,在其内部的操作需要锁的保护。在这里,服务器又调用了do_slabs_free()函数。do_slabs_free()函数的工作很简单,就是负责把ptr(缓存块的首地址)放入到p->slots[p->sl_curr++]中。这里需要注意的是,p是指向slabclass[item->slabs_clsid]的指针。另外,p->sl_curr的值有可能会达到或超过p->sl_total的值。当出现这种情况的时候,就需要通过realloc()函数对p->slots进行扩容了。
以上操作完成后,缓存块就被放入到垃圾回收站中了。
以上讲解的就是memcached内部的缓存管理技术。由于操作细节太多且过于繁琐,这里在讲解时进行了一些简化,但是在对关键技术点的讲解时,是很详尽且很到位的。希望以上的分析解读对大家能有所帮助^_^
祝好!
阅读(2880) | 评论(0) | 转发(1) |