阅读 memcached 最好有 libevent 基础, memcached 是基于 libevent 构建起来的. 通由 libevent 提供的事件驱动机制触发 memcached 中的 IO 事件.php
我的认为, 阅读源码的起初最忌钻牛角尖, 如头文件里天花乱坠的结构体到底有什么用. 源文件里稀里哗啦的函数是作什么的. 刚开始并不必事无巨细弄清楚头文件每一个类型定义的具体用途; 极可能那些是不紧要的工具函数, 知道他的功能和用法就没他事了.git
来看 memcached 内部作了什么事情. memcached 是用 c 语言实现, 必须有一个入口函数main()
, memcached 的生命从这里开始.github
创建并初始化 main_base, 即主线程的事件中心, 这是 libevent 里面的概念, 能够把它理解为事件分发中心.数组
创建并初始化 memcached 内部容器数据结构.缓存
创建并初始化空闲链接结构体数组.服务器
创建并初始化线程结构数组, 指定每一个线程的入口函数是worker_libevent()
, 并建立工做线程. 从worder_libevent()
的实现来看, 工做线程都会调用event_base_loop()
进入本身的事件循环.网络
根据 memcached 配置, 开启如下两种服务模式中的一种:数据结构
memcached 有可配置的两种模式: UNIX 域套接字和 TCP/UDP, 容许客户端以两种方式向 memcached 发起请求. 客户端和服务器在同一个主机上的状况下能够用 UNIX 域套接字, 不然能够采用 TCP/UDP 的模式. 两种模式是不兼容的. 特别的, 若是是 UNIX 域套接字或者 TCP 模式, 须要创建监听套接字, 并在事件中心注册了读事件, 回调函数是event_handler()
, 咱们会看到全部的链接都会被注册回调函数是event_handler()
.socket
调用event_base_loop()
开启 libevent 的事件循环. 到此, memcached 服务器的工做正式进入了工做. 若是遇到致命错误或者客户明令结束 memcached, 那么才会进入接下来的清理工做.tcp
在初始化过程中介绍了这两种模式, memcached 这么作为的是让其能更加可配置. TCP/UDP 自不用说, UNIX 域套接字有独特的优点:
其余关于 UNIX 域套接字优缺点的请参看:https://pangea.stanford.edu/computing/UNIX/overview/advantages.php
在thread_init()
,setup_thread()
函数的实现中, memcached 的意图是很清楚的. 每一个线程都有本身独有的链接队列, 即 CQ, 注意这个链接队列中的对象并非一个或者多个 memcached 命令, 它对应一个客户! 一旦一个客户交给了一个线程, 它的余生就属于这个线程了! 线程只要被唤醒就当即进入工做状态, 将本身 CQ 队列的任务全部完完成. 固然, 每个工做线程都有本身的 libevent 事件中心.
很关键的线索是thread_init()
的实现中, 每一个工做线程都建立了读写管道, 所能给咱们的提示是: 只要利用 libevent 在工做线程的事件中心注册读管道的读事件, 就能够按需唤醒线程, 完成工做, 颇有意思, 而setup_thread()
的工做正是读管道的读事件被注册到线程的事件中心, 回调函数是thread_libevent_process()
.thread_libevent_process()
的工做就是从工做线程本身的 CQ 队列中取出任务执行, 而往工做线程工做队列中添加任务的是dispatch_conn_new()
, 此函数通常由主线程调用. 下面是主线程和工做线程的工做流程:
前几天在微博上, 看到 @高端小混混 的微博, 转发了:
@高端小混混
多任务并行处理的两种方式,一种是将全部的任务用队列存储起来,每一个工做者依次去拿一个来处理,直到作完全部的>任务为止。另外一种是将任务平均分给工做者,先作完任务的工做者就去别的工做者那里拿一些任务来作,一样直到全部任务作完为止。两种方式的结果如何?根据本身的场景写码验证。
memcached 所采用的模式就是这里所说的第二种! memcached 的线程分配模式是:一个主线程和多个工做线程。主线程负责初始化和将接收的请求分派给工做线程,工做线程负责接收客户的命令请求和回复客户。
memcached 是作缓存用的, 内部确定有一个容器. 回到main()
中, 调用assoc_init()
初始化了容器--hashtable, 采用头插法插入新数据, 由于头插法是最快的. memcached 只作了一级的索引, 即 hash; 接下来的就靠 memcmp() 在链表中找数据所在的位置. memcached 容器管理的接口主要在 item.h .c 中.
每一个链接都会创建一个链接结构体与之对应. main()
中会调用conn_init()
创建链接结构体数组. 链接结构体 struct conn 记录了链接套接字, 读取的数据, 将要写入的数据, libevent event 结构体以及所属的线程信息.
当有新的链接时, 主线程会被唤醒, 主线程选定一个工做线程 thread0, 在 thread0 的写管道中写入数据, 特别的若是是接受新的链接而不是接受新的数据, 写入管道的数据是字符 'c'. 工做线程因管道中有数据可读被唤醒,thread_libevent_process()
被调用, 新链接套接字被注册了event_handler()
回调函数, 这些工做在conn_new()
中完成. 所以, 客户端有命令请求的时候(譬如发起 get key 命令), 工做线程都会被触发调用event_handler()
.
当出现致命错误或者客户命令结束服务(quit 命令), 关于此链接的结构体内部的数据会被释放(譬如曾经读取的数据), 但结构体自己不释放, 等待下一次使用. 若是有须要, 链接结构体数组会指数自增.
memcached 服务一个客户的时候, 是怎么一个过程, 试着去调试模拟一下. 当一个客户向 memcached 发起请求时, 主线程会被唤醒, 接受请求. 接下来的工做在链接管理中有说到.
客户已经与 memcached 服务器创建了链接, 客户在终端(黑框框)敲击 get key + 回车键, 一个请求包就发出去了. 从链接管理中已经了解到全部链接套接字都会被注册回调函数为event_handler()
, 所以event_handler()
会被触发调用.
<code>void event_handler(const int fd, const short which, void *arg) { conn *c; c = (conn *)arg; assert(c != NULL); c->which = which; /* sanity */ if (fd != c->sfd) { if (settings.verbose > 0) fprintf(stderr, "Catastrophic: event fd doesn't match conn fd!\n"); conn_close(c); return; } drive_machine(c); /* wait for next event */ return; } </code>
event_handler()
调用了drive_machine()
.drive_machine()
是请求处理的开端, 特别的当有新的链接时, listen socket 也是有请求的, 因此创建新的链接也会调用drive_machine()
, 这在链接管理有提到过. 下面是drive_machine()
函数的骨架:
<code>// 请求的开端. 当有新的链接的时候 event_handler() 会调用此函数. static void drive_machine(conn *c) { bool stop = false; int sfd, flags = 1; socklen_t addrlen; struct sockaddr_storage addr; int nreqs = settings.reqs_per_event; int res; const char *str; assert(c != NULL); while (!stop) { // while 能保证一个命令被执行完成或者异常中断(譬如 IO 操做次数超出了必定的限制) switch(c->state) { // 正在链接, 尚未 accept case conn_listening: // 等待新的命令请求 case conn_waiting: // 读取数据 case conn_read: // 尝试解析命令 case conn_parse_cmd : // 新的命令请求, 只是负责转变 conn 的状态 case conn_new_cmd: // 真正执行命令的地方 case conn_nread: // 读取全部的数据, 抛弃!!! 通常出错的状况下会转换到此状态 case conn_swallow: // 数据回复 case conn_write: case conn_mwrite: // 链接结束. 通常出错或者客户显示结束服务的状况下回转换到此状态 case conn_closing: } } return; } </code>
经过修改链接结构体状态 struct conn.state 执行相应的操做, 从而完成一个请求, 完成后 stop 会被设置为 true, 一个命令只有执行结束(不管结果如何)才会跳出这个循环. 咱们看到 struct conn 有好多种状态, 一个正常执行的命令状态的转换是:
<code> conn_new_cmd->conn_waiting->conn_read->conn_parse_cmd->conn_nread->conn_mwrite->conn_close </code>
这个过程任何一个环节出了问题都会致使状态转变为 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()
新建 struct conn 结构体, 且状态为 conn_new_cmd, 其对应的就是刚才accept()
的链接套接字.conn_new()
最关键的任务是将刚才接受的套接字在 libevent 中注册一个事件, 回调函数是event_handler()
. 循环继续, 状态 conn_new_cmd 下的操做只是只是将 conn 的状态转换为 conn_waiting;event_handler()
被调用了. 看! 又被调用了.event_handler()->drive_machine()
, 此时 conn 的状态为 conn_read. conn_read 下的操做就是读数据了, 若是读取成功, conn 状态被转换为 conn_parse_cmd.memcached 的服务器没有向其余 memcached 服务器收发数据的功能, 意即就算部署多个 memcached 服务器, 他们之间也没有任何的通讯. memcached 所谓的分布式部署也是并不是平时所说的分布式. 所说的「分布式」是经过建立多个 memcached 服务器节点, 在客户端添加缓存请求分发器来实现的. memcached 的更多的时候限制是来自网络 I/O, 因此应该尽可能减小网络 I/O.
我在 github 上分享了 memcached 的源码剖析注释: 这里
欢迎讨论: @郑思愿daoluan