memcache源码分析之软件架构

学习开源框架的源码是学习软件开发的有效途径,可以了解到之前学习的linux进程,线程和网络知识是怎么运用的,以及巩固编程语言,最后掌握一门技术。之前在看leveldb源码时,学习到了好多知识,内存池的设计,缓冲池的设计以及C++如何开发一个开源框架;看redis时(有时间要补上redis源码总结),因为是我第一个看的开源框架,给我感受最深的是服务器端事件驱动,也就是mainae模块,可以由一个线程,监听所有的套接字描述符,实现并发。redis代码也很优美,看起来比较轻松。这次看的memcache,区别与redis,memcache使用的多线程模型,主线程accept客户端连接,工作线程处理客户端的请求。

接下来,先介绍下常见的服务器端软件架构,之后在介绍memcache软件架构。

常见的服务端软件架构


目前在服务器端常见的软件架构有单进程,多进程和多线程。具体展开细分,又可以分为以下10种:

  1. 单进程 即服务器端阻塞在accept等待客户端请求,来一个客户端请求,服务器端处理这个请求,这个请求处理之后,才能接下去处理其他客户端请求,这种设计也称为迭代式服务器。这种单进程模型不适合现在服务器设计了,因为现在的服务器连接数都已经上万,几十万,百万,可能还更多,单进程会导致很多连接延迟。
  2. 单进程的IO多路复用 即redis的服务器进程模型。这种模型进程阻塞在select,epoll等io多路系统调用上,然后在一个一个串行处理客户端请求。这种模型在每个客户端的请求都占用较短时间的情况下,还是很高效的。
  3. 一个客户一个子进程模型 服务器父进程阻塞在accept调用中,每接收一个客户端,就fork一个子进程处理这个客户端。
  4. 预先派生子进程,accept无上锁 即程序在一开始运行时,先fork出一定数量的子进程,然后每个子进程分别accept。子进程由于继承了监听的套接字,所以当有一个客户端连接时,每个子进程均可以被通知有客户端到达,接着调用accept接受连接,但是只有最快的那个子进程接收这个客户端,这种现象也叫作惊群。所以有下面两种加锁方式避免惊群。
  5. 预先派生子进程,accept使用文件锁 每个子进程在调用accept之前,必须先获取文件锁,然后才能调用accept,最后在把文件锁给释放了,然后才处理客户端请求。可以用fcntl锁住一个文件。
  6. 预先派生多个子进程,accept使用线程锁保护 当在进程间使用线程锁时,必须将线程锁存储在共享内存区,然后将线程锁的属性设置为PTHREAD_PROCESS_SHARED即可。
  7. 预先派生多个子进程,传递描述符 现在父进程和子进程之间建立通信管道或者域套接字,当主进程接受一个客户端连接时,用管道传递给子进程,子进程处理之后,再向父进程发送一个字符,表示子进程又可用。
  8. 一个客户,一个线程 这种主线程接收客户端请求,子线程处理客户请求。
  9. 预先创建多个线程,每个线程accept 这个和第三个类似,需要加线程锁锁住accept调用。
  10. 预先创建多个线程,主线程统一accept 主线程主要为accept客户端请求,并把接收到的描述符放进一个队列;然后子线程在分别从队列提取描述符处理。

在这么多服务器端软件设计模型中,我比较喜欢的是单进程IO多路复用(redis);预先派生多个子进程,进程池中的accept加锁(nginx);线程池,主线程统一accept,放进队列,工作线程提取任务(memcache)。nginx也是一款很优秀的开源软件,也在我学习计划中。

memcache在服务端的作用


memcache和redis都是缓存数据库,在大型网址中作为cache。当服务器程序要访问mysql时,首先访问缓存数据库,如果缓存数据库中有需要的数据,则之前从缓存数据库中返回;如果没有,则从mysql数据库中获取数据,并存入缓存数据库中,最后从缓存数据库返回给服务器程序。因为缓存数据库运行在内存中,所以获取数据的速度比Mysql获取高效。所以在大型网站的架构中,缓存数据库可以说是举足轻重。下面图说明了服务器访问关系性数据库和NoSQL的关系,图片来自网上: 服务器访问数据库

memcache线程模型


memcache使用了libevent开源事件驱动框架,libvent底层封装了select,epoll,/dev/poll,kqueue等各平台的IO多路复用,接口简单,效率高。总体框架如下图所示: memceche软件线程模型 memcache采用的是线程池模型,首先在进程运行时,产生一定数量的子线程,主线程和子线程利用管道进行通信。主线程调用libevent接口,监听客户端的请求,当接收到一个客户端请求之后,选择一个空闲的线程,将客户端的套接字放入队列中,并向这个子线程管道发送一个字符。这个被选中的子线程接收到字符之后,就从队列中获取客户端的描述符,然后调用libevent接口,监听这个客户端是否有业务需要执行。

所以总结为:主线和子线程各有一个libevent,主线程监听服务器端套接字,接收客户请求,子线程处理客户请求。

memcache内存池


当客户端与memcache服务器建立连接之后,接着并向服务器发送命令set,get等等,存储数据或者取数据。那么这时就设计到memcache内存池设计。memcache利用slab来管理内存分配。slab模型图如下: slab内存池模型 memcache分配了n个slabclass数组,每个数组负责固定大小的内存请求,相邻的slabclass负责的内存大小成等比数列。例如slabclass[0]负责88bytes,假设比例为1.25,则slabclas[1]负责110bytes。slabclass每次分配一个内存页,大小为1M,然后将这1M内存分为88bytes字节的chunk,用于每个item存储,(item为存储键值对的数据结构)。slab_list存储的是内存页列表,每个内存页为1M,由一定数量的chunk组成。slots为一个item指针列表,存储回收的内存item内存块指针。 chunk模型; 当需要存储一个键值对时,线程会将键值对封装成item结构,然后到slab中请求内存。slab分配给这个item的内存大小为大于sizeof(item)的最小内存chunk。如下图所示: slab给item分配内存

memcache作为一款缓存数据库,它维护着一个哈希表和为每一个slabclass分配了一个LRU队列。LRU队列用于存储管理数据,哈希表用于查询数据。这也是缓存设计经典数据结构组合:队列+哈希表。当某个item分配到一块chunk之后,这个chunk就从slab内存池脱离,然后绑定到这个LRU队列tail中。同时也插入到哈希表中,便于查询。memcache没有真正的删除键值对,而是当要删除元素时,先删除LRU队列的head指针,然后在删除哈希表中的指针,最后将存储这个item的chunk回收到slabclass的slots中,当重新利用这个chunk时,直接覆盖旧数据即可。

memcache还创建了一些爬虫线程用于平衡内存分配,LRU队列等等,等分析源码时,再细究。所以对于一款内存数据库而言,主要由网络,内存管理和线程组成。

以上就是memcache的整体框架,接下来,我对主要部分将进行源码分析。注:这篇文章的图片均来自网络。