阅读 memcached 最好有 libevent 基础, memcached 是基于 libevent 构建起来的. 通由 libevent 提供的事件驱动机制触发 memcached 中的 IO 事件.

个人认为, 阅读源码的起初最忌钻牛角尖, 如头文件里天花乱坠的结构体到底有什么用. 源文件里稀里哗啦的函数是做什么的. 刚开始并没必要事无巨细弄清楚头文件每个类型定义的具体用途; 很可能那些是不紧要的工具函数, 知道他的功能和用法就没他事了.

来看 memcached 内部做了什么事情. memcached 是用 c 语言实现, 必须有一个入口函数main(), memcached 的生命从这里开始.

初始化过程

建立并初始化 main_base, 即主线程的事件中心, 这是 libevent 里面的概念, 可以把它理解为事件分发中心.

建立并初始化 memcached 内部容器数据结构.

建立并初始化空闲连接结构体数组.

建立并初始化线程结构数组, 指定每个线程的入口函数是worker_libevent(), 并创建工作线程. 从worder_libevent()的实现来看, 工作线程都会调用event_base_loop()进入自己的事件循环.

根据 memcached 配置, 开启以下两种服务模式中的一种:

  1. 以 UNIX 域套接字的方式接受客户的请求
  2. 以 TCP/UDP 套接字的方式接受客户的请求

memcached 有可配置的两种模式: UNIX 域套接字和 TCP/UDP, 允许客户端以两种方式向 memcached 发起请求. 客户端和服务器在同一个主机上的情况下可以用 UNIX 域套接字, 否则可以采用 TCP/UDP 的模式. 两种模式是不兼容的. 特别的, 如果是 UNIX 域套接字或者 TCP 模式, 需要建立监听套接字, 并在事件中心注册了读事件, 回调函数是event_handler(), 我们会看到所有的连接都会被注册回调函数是event_handler().

调用event_base_loop()开启 libevent 的事件循环. 到此, memcached 服务器的工作正式进入了工作. 如果遇到致命错误或者客户明令结束 memcached, 那么才会进入接下来的清理工作.

UNIX 域套接字和 UDP/TCP 工作模式

初始化过程中介绍了这两种模式, memcached 这么做为的是让其能更加可配置. TCP/UDP 自不用说, UNIX 域套接字有独特的优势:

  1. 在同一台主机上进行通信时,是不同主机间通信的两倍
  2. UNIX 域套接口可以在同一台主机上,不同进程之间传递套接字描述符
  3. UNIX 域套接字可以向服务器提供客户的凭证(用户id或者用户组id)

其他关于 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-&gt;which = which; /* sanity */
if (fd != c-&gt;sfd) {
if (settings.verbose &gt; 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-&gt;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-&gt;conn_waiting-&gt;conn_read-&gt;conn_parse_cmd-&gt;conn_nread-&gt;conn_mwrite-&gt;conn_close
</code>

这个过程任何一个环节出了问题都会导致状态转变为 conn_close. 带着刚开始的问题把从客户连接到一个命令执行结束的过程是怎么样的:

  1. 客户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);

  2. dispatch_conn_new()的工作是往工作线程工作队列中添加任务(前面已经提到过), 所以其中一个沉睡的工作线程会被唤醒,thread_libevent_process()会被工作线程调用, 注意这些机制都是由 libevent 提供的.
  3. thread_libevent_process()调用conn_new()新建 struct conn 结构体, 且状态为 conn_new_cmd, 其对应的就是刚才accept()的连接套接字.conn_new()最关键的任务是将刚才接受的套接字在 libevent 中注册一个事件, 回调函数是event_handler(). 循环继续, 状态 conn_new_cmd 下的操作只是只是将 conn 的状态转换为 conn_waiting;
  4. 循环继续, conn_waiting 状态下的操作只是将 conn 状态转换为 conn_read, 循环退出.
  5. 此后, 如果客户端不请求服务, 那么主线程和工作线程都会沉睡, 注意这些机制都是由 libevent 提供的.
  6. 客户敲击命令「get key」后, 工作线程会被唤醒,event_handler()被调用了. 看! 又被调用了.event_handler()->drive_machine()此时 conn 的状态为 conn_read. conn_read 下的操作就是读数据了, 如果读取成功, conn 状态被转换为 conn_parse_cmd.
  7. 循环继续, conn_parse_cmd 状态下的操作就是尝试解析命令: 可能是较为简单的命令, 就直接回复, 状态转换为 conn_close, 循环接下去就结束了; 涉及存取操作的请求会导致 conn_parse_cmd 状态转换为 conn_nread.
  8. 循环继续, conn_nread 状态下的操作是真正执行存取命令的地方. 里面的操作无非是在内存寻找数据项, 返回数据. 所以接下来的状态 conn_mwrite, 它的操作是为客户端回复数据.
  9. 状态又回到了 conn_new_cmd 迎接新的请求, 直到客户命令结束服务或者发生致命错误. 大概就是这么个过程.

memcached 的分布式

memcached 的服务器没有向其他 memcached 服务器收发数据的功能, 意即就算部署多个 memcached 服务器, 他们之间也没有任何的通信. memcached 所谓的分布式部署也是并非平时所说的分布式. 所说的「分布式」是通过创建多个 memcached 服务器节点, 在客户端添加缓存请求分发器来实现的. memcached 的更多的时候限制是来自网络 I/O, 所以应该尽量减少网络 I/O.

我在 github 上分享了 memcached 的源码剖析注释: 这里

欢迎讨论: @郑思愿daoluan

memcached 源码阅读笔记的更多相关文章

  1. CI框架源码阅读笔记5 基准测试 BenchMark.php

    上一篇博客(CI框架源码阅读笔记4 引导文件CodeIgniter.php)中,我们已经看到:CI中核心流程的核心功能都是由不同的组件来完成的.这些组件类似于一个一个单独的模块,不同的模块完成不同的功 ...

  2. CI框架源码阅读笔记4 引导文件CodeIgniter.php

    到了这里,终于进入CI框架的核心了.既然是“引导”文件,那么就是对用户的请求.参数等做相应的导向,让用户请求和数据流按照正确的线路各就各位.例如,用户的请求url: http://you.host.c ...

  3. CI框架源码阅读笔记3 全局函数Common.php

    从本篇开始,将深入CI框架的内部,一步步去探索这个框架的实现.结构和设计. Common.php文件定义了一系列的全局函数(一般来说,全局函数具有最高的加载优先权,因此大多数的框架中BootStrap ...

  4. CI框架源码阅读笔记2 一切的入口 index.php

    上一节(CI框架源码阅读笔记1 - 环境准备.基本术语和框架流程)中,我们提到了CI框架的基本流程,这里再次贴出流程图,以备参考: 作为CI框架的入口文件,源码阅读,自然由此开始.在源码阅读的过程中, ...

  5. 源码阅读笔记 - 1 MSVC2015中的std::sort

    大约寒假开始的时候我就已经把std::sort的源码阅读完毕并理解其中的做法了,到了寒假结尾,姑且把它写出来 这是我的第一篇源码阅读笔记,以后会发更多的,包括算法和库实现,源码会按照我自己的代码风格格 ...

  6. Three.js源码阅读笔记-5

    Core::Ray 该类用来表示空间中的“射线”,主要用来进行碰撞检测. THREE.Ray = function ( origin, direction ) { this.origin = ( or ...

  7. PHP源码阅读笔记一(explode和implode函数分析)

    PHP源码阅读笔记一一.explode和implode函数array explode ( string separator, string string [, int limit] )此函数返回由字符 ...

  8. AQS源码阅读笔记(一)

    AQS源码阅读笔记 先看下这个类张非常重要的一个静态内部类Node.如下: static final class Node { //表示当前节点以共享模式等待锁 static final Node S ...

  9. libevent源码阅读笔记(一):libevent对epoll的封装

    title: libevent源码阅读笔记(一):libevent对epoll的封装 最近开始阅读网络库libevent的源码,阅读源码之前,大致看了张亮写的几篇博文(libevent源码深度剖析 h ...

随机推荐

  1. [原] XAF 如何将数据库中Byte array图片显示出来

    问题比较简单,直接上代码. private Image _Cover; [Size(SizeAttribute.Unlimited), ValueConverter(typeof(ImageValue ...

  2. 日期运算 jsf日期组建

    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); Date date; try { d ...

  3. poj 3613 经过k条边最短路 floyd+矩阵快速幂

    http://poj.org/problem?id=3613 s->t上经过k条边的最短路 先把1000范围的点离散化到200中,然后使用最短路可以使用floyd,由于求的是经过k条路的最短路, ...

  4. innodb_ft_max_token_size取值范围

    根据问档中所说,innodb_ft_max_token_size取值范围为10-252,而实际上但我们在配置文件设置innodb_ft_max_token_size=252时,error log会出现 ...

  5. openSUSE 13.1 Milestone 4 发布

    openSUSE 13.1 发布第四个里程碑版本,下载地址: openSUSE-Factory-KDE-Live-Build0652-x86_64.iso (949MB, MD5, torrent) ...

  6. Dynamic CRM 2013学习笔记(三十一)自定义用excel批量导入实体数据

    有一个实体的子表数据量太大,于是客户想用execel来导入实体数据.首先想到的是用系统自带的Import Data,客户嫌太麻烦,比如lookup字段要做map等. 下面是具体的实现步骤: 一.定义e ...

  7. Fragment之间的通信

    在本节中,你会学到 1.定义接口 2.实现接口 3.将消息传递给fragment 为了重用Fragment UI 组件,在设计中你应该通过定义每一个fragemnt自己的layout和行为,让frag ...

  8. CSS设计资料

    CSS实现垂直居中的5种方法 网页阶级配色:http://tools.jb51.net/tools/peise.htm

  9. [异常] MyEclipse Deploy点不开 An internal error occurred during: "Launching MVC on Tomcat 6.x". java.lang.NullPointerException

    >_<" 今天做struts时,工程写好了,可是点击MyEclipse上面的Delopy不会跳出常规的窗口,导致各种坑~ >_<" 如果直接运行还出现下面的 ...

  10. 【原】关于使用jieba分词+PyInstaller进行打包时出现的一些问题的解决方法

    错误现象: 最近在做一个小项目,在Python中使用了jieba分词,感觉非常简洁方便.在Python端进行调试的时候没有任何问题,使用PyInstaller打包成exe文件后,就会报错: 错误原因分 ...