阅读memcached***有libevent基础,memcached是基于libevent构建的。通过libevent提供的事件驱动机制触发memcached中的IO事件。个人认为阅读源码的时候最好避免一开始就挖牛角,比如头文件中的炒作结构有什么用。源文件中草率函数的作用是什么。一开始,不必弄清楚头文件中每个类型定义的具体用途;那些很可能是不重要的效用函数,知道他的作用和用法就可以了。让我们看看memcached内部发生了什么。Memcached是用C语言实现的,必须有一个入口函数main(),memcached的生命从这里开始。初始化过程建立并初始化main_base,它是主线程的事件中心。这是libevent中的一个概念,可以理解为事件分发中心。创建并初始化memcached内部容器数据结构。创建并初始化一个自由连接结构数组。创建并初始化一个线程结构体数组,指定每个线程的入口函数为worker_libevent(),创建工作线程。从worder_libevent()的实现来看,工作线程会调用event_base_loop()进入自己的事件循环。根据memcached配置,启用以下两种服务模式之一:作为UNIX域套接字接受客户请求和作为TCP/UDP套接字接受客户请求。memcached有两种可配置的模式:UNIXDomainsocket和TCP/UDP,允许客户端以两种方式向memcached发起请求。如果客户端和服务器在同一台主机上,可以使用UNIX域套接字;否则,可以使用TCP/UDP。这两种模式是不兼容的。特别是,如果是UNIX域socket或者TCP方式,需要创建一个监听socket,并在事件中心注册一个read事件。回调函数是event_handler(),我们会看到所有的连接都会被注册。回调函数是event_handler()。调用event_base_loop()启动libevent的事件循环。至此,memcached服务器的工作正式进入工作。如果遇到致命错误或客户端显式终止memcached,则进入下一步清理工作。UNIXDomainSockets和UDP/TCP工作模式在初始化时引入,memcached这样做是为了使其更易于配置。TCP/UDP不用多说,UNIX域套接字有独特的优势:在同一台主机上通信时,是不同主机间通信的两倍。UNIX域套接字可以在同一主机上的不同进程之间传输套接字描述符UNIX域套接字可以向服务器提供客户端的凭据(用户标识或用户组标识)。UNIXdomainsocket的其他优缺点可以参考:https://pangea.stanford.edu/computing/UNIX/overview/advantages.phpWorker线程管理和线程分配方法在thread_init()和setup_thread()功能,memcached的意图非常明确。每个线程都有自己唯一的连接队列,即CQ。注意这个连接队列中的对象不是一个或多个memcached命令,它对应的是一个客户!一个客户一旦交给一个线程,剩下的生命就属于这个线程了!只要线程被唤醒,就会立即进入工作状态,完成自己CQ队列中的所有任务。当然,每个工作线程都有自己的libevent事件中心。关键线索是在thread_init()的实现中,每个工作线程都创建了一个读写管道。可以给我们的提示是:只要使用libevent在工作线程的事件中心注册读管道的读事件,就可以按唤醒线程完成工作,这很有趣,而且setup_thread()的工作是读取管道的读事件注册到线程的事件中心,回调函数为thread_libevent_process()。thread_libevent_process()的工作是从worker线程自身的CQ中读取任务取出队列执行,将任务加入到worker线程的工作队列中是dispatch_conn_new(),一般由主线程。下面是主线程和工作线程的工作流程:前几天在微博上看到@高兴小惊惊的微博,转发:“多任务并行处理有两种方式,一种是使用alltaskswithqueue存储,每个worker轮流拿一个处理,直到所有>tasks完成。一些任务要做,直到所有任务都完成。这两种方法的结果是什么?根据自己的场景写代码验证。》memcached采用的模式就是这里说的第二种!memcached的线程分配模式是:一个主线程,多个工作线程。主线程负责初始化,将接收到的请求分派给工作线程,工作线程负责接收客户端请求,命令请求和回复客户。#p#存储容器memcached用于缓存,里面必须有一个容器。回到main(),调用assoc_init()初始化container-hashtable,并通过headerinsertion的方式插入新的数据,因为headerinsertion的方式是最快的,memcached只做一级索引,也就是hash;接下来就是靠memcmp()找到数据在其中的位置链表.memcached容器管理的接口主要在item.h.c中.连接管理每个连接都会建立一个与之对应的连接结构.main()会调用conn_init()创建一个n个连接结构数组。连接结构structconn记录了连接套接字和要读取的数据。写入的数据,libevent事件结构及其所属的线程信息。当有新连接时,主线程会被唤醒,主线程选择一个工作线程thread0,在thread0写管道中写入数据,尤其是如果是接受新连接而不是接受新数据。写入管道的数据是字符'c'。工作线程被唤醒是因为管道中有数据需要读取,thread_libevent_process()被调用,新的连接socket被注册到event_handler()回调函数中,这些工作都是在conn_new()中完成的。因此,当客户端有命令请求时(比如发起getkey命令),就会触发工作线程调用event_handler()。当发生致命错误或者客户端命令结束服务(quit命令)时,这个连接的结构体里面的数据会被释放(比如已经读取的数据),但是结构体本身不会被释放,等待下次使用。如果有必要的话,连接结构数组会呈指数级增长一个请求的工作流程memcached服务一个客户端的时候,流程是什么,试着调试模拟一下。当客户端向memcached发起请求时,主线程会被唤醒并接受请求。接下来的工作就是中提到的连接管理。客户端已经与memcached服务器建立连接,客户端在终端(黑框)上敲get键+回车键,发送请求包。从连接管理中得知,所有的连接套接字都会注册一个回调函数为event_handler(),所以event_handler()会被触发调用。voidevent_handler(constintfd,constshortwhich,void*arg){conn*c;c=(conn*)arg;assert(c!=NULL);c->whichwhich=which;/*sanity*/if(fd!=c->sfd){if(settings.verbose>0)fprintf(stderr,"Catastrophic:eventfddoesn'tmatchconnfd!\n");conn_close(c);return;}drive_machine(c);/*waitfornextevent*/return;}event_handler()调用drive_machine()。drive_machine()是请求处理的开始,尤其是有新连接的时候,listensocket也有请求,所以新连接的建立也会调用drive_machine(),这是在提到的连接管理中可用。以下是drive_machine()函数的框架://请求的开头。event_handler()将在有新连接时调用此函数。staticvoiddrive_machine(conn*c){boolstop=false;intsfd,flags=1;socklen_taddrlen;structsockaddr_storageaddr;intnreqs=settings.reqs_per_event;intres;constchar*str;assert(c!=NULL);while(!stop){//while可以保证一个命令被异常执行或中断(比如IO操作次数超过一定限制)switch(c->state){//connecting,notyetacceptcaseconn_listening://等待新的命令请求caseconn_waiting://read取数据caseconn_read://尝试解析命令caseconn_parse_cmd://新命令请求,只负责改变conn的状态caseconn_new_cmd://命令实际执行的位置caseconn_nread://读取所有数据,丢弃!!!一般错误的case转换到这个状态caseconn_swallow://数据回复caseconn_write:caseconn_mwrite://连接结束。一般当出现错误或者客户显示服务结束时,会切换回这个状态一个request,完成后stop会被设置为true,一个command只有在执行的时候才会跳出这个循环(不管结果如何)。我们看到structconn有很多状态。一个正常执行的命令状态的转换是:conn_new_cmd->conn_waiting->conn_read->conn_parse_cmd->conn_nread->conn_mwrite->conn_close这个过程中出现任何问题都会导致状态变为conn_close。从客户端连接到一个命令执行结束的过程和开头的问题:客户端connect()之后,memcached服务器的主线程被唤醒,接下来的调用链是event_handler()——>drive_machine()被执行调用,此时主线程对应的conn状态为conn_listining,接受请求dispatch_conn_new(sfd,conn_new_cmd,EV_READ|EV_PERSIST,DATA_BUFFER_SIZE,tcp_transport);dispatch_conn_new()的工作是将任务添加到工作线程工作队列中(如前所述),因此其中一个休眠的工作线程将被唤醒,thread_libevent_process()将被工作线程调用。请注意,这些机制由libevent提供。thread_libevent_process()调用conn_new()创建了一个新的structconn结构体,state为conn_new_cmd,对应刚才accept()的connectionsocket。conn_new()最关键的任务就是把刚刚接受的socket在libevent中注册一个事件,回调函数是event_handler()。循环往复,conn_new_cmd状态下的操作只是将conn的状态转换为conn_waiting;循环继续,conn_waiting状态下的操作只是将conn的状态转换为conn_read,循环退出。之后,如果客户端没有请求服务,主线程和工作线程都会休眠。请注意,这些机制由libevent提供。客户端点击“获取密钥”命令后,工作线程将被唤醒,并调用event_handler()。看!.event_handler()->drive_machine()再次被调用,conn的状态为conn_read。conn_read下的操作是读取数据。如果读取成功,则将conn状态转换为conn_parse_cmd。循环继续,conn_parse_cmd状态下的操作是尝试解析命令:可能是比较简单的命令,直接回复即可,状态变为conn_close,接下来循环结束;涉及访问操作的请求将导致conn_parse_cmd状态更改为conn_nread。如此循环下去,conn_nread状态下的操作才是真正执行access命令的地方。里面的操作无非就是在内存中查找数据项,返回数据。所以下一个状态conn_mwrite,它的操作就是给client回复数据。状态回到conn_new_cmd接受新的请求,直到客户端命令结束服务或发生致命错误。大概就是这个过程。#p#memcached分布式memcached服务器不具备向其他memcached服务器发送和接收数据的功能,也就是说即使部署了多台memcached服务器,它们之间也没有通信。memcached所谓的分布式部署,通常不叫分布式。所谓“分布式”就是通过创建多个memcachedserver节点,在client端添加一个缓存请求分发器来实现的。Memcached更经常受到网络I/O的限制,因此应尽量减少网络I/O。我在github上分享了memcached源码分析笔记:这里是原文链接:https://github.com/daoluan/decode-memcached翻译链接:http://blog.jobbole.com/53861/
