memcached是一个典型的单进程系统。虽然是单进程,但是memcached内部通过多线程实现了master-worker模型,这也是服务端最常见的一种并发模型。实际上,除了master线程和worker线程之外,memcached还有一些其他的辅助线程(比如logger线程),但是与本文主题无关,所以这里不做描述。

master-worker线程模型

memcached有1条主线程,以及4条woker线程。可以通过启动参数-t来指定worker线程的数量,如果不指定,默认情况下就是4。简单来说,主线程负责监听请求,分发给worker线程。而worker线程负责接收具体的请求命令并且作出处理。

示意图如下:

这张图大致画出了主从线程之间的关系。

主线程监听到有新的连接之后,会做出一次选择,我们称之为dispatch,其实就是确定此连接后续会由哪个worker线程处理。一旦确定worker线程,接下来主线程会借助管道通知该worker线程。在每条worker线程的内部,都有一个连接队列。worker线程收到通知之后,会从连接队列中取出一个连接,用于后续接受具体的命令,以及做出响应。

OK,上面只是一个大概的描述,准确的细节,我们还是得通过分析源代码才能得知。

worker线程的创建

来看一下worker线程的创建过程,在main函数中有:

/* start up worker threads if MT mode */
memcached_thread_init(settings.num_threads);

settings.num_threads控制着worker线程的个数。前文提到可以通过-t参数改变,因为在main函数刚开始分析启动参数的一段中有:

case 't':
settings.num_threads = atoi(optarg);
if (settings.num_threads <= ) {
fprintf(stderr, "Number of threads must be greater than 0\n");
return ;
}
/* There're other problems when you get above 64 threads.
* In the future we should portably detect # of cores for the
* default.
*/
if (settings.num_threads > ) {
fprintf(stderr, "WARNING: Setting a high number of worker"
" threads is not recommended.\n"
" Set this value to the number of cores in"
" your machine or less.\n");
}
break;

可以看到,最好不要设置超过64条线程,线程一旦太多,频繁的切换也需要开销,另外就是memcached大量使用互斥锁,可能会使得没有抢到锁的线程处于等待状态。不过笔者并没有测试过线程数量递增与性能的损失比例。

接下来看memcached_thread_init函数:

void memcached_thread_init(int nthreads) {
// 锁的初始化
... // 初始化init_lock,init_cond
pthread_mutex_init(&init_lock, NULL);
pthread_cond_init(&init_cond, NULL); // 锁的初始化
... // 分配LIBEVENT_THREAD的空间
threads = calloc(nthreads, sizeof(LIBEVENT_THREAD));
if (! threads) {
perror("Can't allocate thread descriptors");
exit();
} // 初始化各线程
for (i = ; i < nthreads; i++) {
int fds[];
if (pipe(fds)) { // 这里创建了管道!!!
perror("Can't create notify pipe");
exit();
} threads[i].notify_receive_fd = fds[];
threads[i].notify_send_fd = fds[]; setup_thread(&threads[i]);
/* Reserve three fds for the libevent base, and two for the pipe */
stats_state.reserved_fds += ;
} // 启动各线程
for (i = ; i < nthreads; i++) {
create_worker(worker_libevent, &threads[i]);
} // 等待各条线程初始化完成之后,再继续执行
pthread_mutex_lock(&init_lock);
wait_for_thread_registration(nthreads);
pthread_mutex_unlock(&init_lock);
}

memcached_thread_init的刚开始,是初始化一系列的锁。关于memcached中锁的运用,可以单独开一章来讲述,源码中几乎处处存在锁。这里特地列出了init_lock和init_cond,因为在memcached_thread_init的最后会涉及到。

LIBEVENT_THREAD

初始化各种锁之后,会为4条worker线程分配内存空间。我们可以看到,线程相关的数据被封装在LIBEVENT_THREAD结构体当中。

typedef struct {
pthread_t thread_id; // 线程ID
struct event_base *base; // 一个libevent的实例
struct event notify_event; // 监听的事件
int notify_receive_fd; // 管道的recv端
int notify_send_fd; // 管道的send端
struct thread_stats stats; // 线程的一些状态统计
struct conn_queue *new_conn_queue; // 连接队列
cache_t *suffix_cache; /* suffix cache */
logger *l; // logger
} LIBEVENT_THREAD;

在为LIBEVENT_THREAD分配好内存之后,紧接着就开始在for循环中依次初始化各条线程。

这里首先做的事情,是建立主线程和worker线程之间的管道。

// 建立管道
int fds[];
if (pipe(fds)) {
perror("Can't create notify pipe");
exit();
} // 设置管道两端的fd
threads[i].notify_receive_fd = fds[];
threads[i].notify_send_fd = fds[];

setup_thread

前文提到了,当marster接收新连接之后,会利用管道通知worker。我们继续看setup_thread函数:

static void setup_thread(LIBEVENT_THREAD *me) {

    // 初始化worker线程的libevent实例
me->base = event_init();
if (! me->base) {
fprintf(stderr, "Can't allocate event base\n");
exit();
} // 监听管道的recv端,一旦获取到marster线程的通知,会触发thread_libevent_process函数
event_set(&me->notify_event, me->notify_receive_fd,
EV_READ | EV_PERSIST, thread_libevent_process, me);
event_base_set(me->base, &me->notify_event); if (event_add(&me->notify_event, ) == -) {
fprintf(stderr, "Can't monitor libevent notify pipe\n");
exit();
} // 初始化worker线程的conn_queue,即连接队列
me->new_conn_queue = malloc(sizeof(struct conn_queue));
if (me->new_conn_queue == NULL) {
perror("Failed to allocate memory for connection queue");
exit(EXIT_FAILURE);
}
cq_init(me->new_conn_queue); if (pthread_mutex_init(&me->stats.mutex, NULL) != ) {
perror("Failed to initialize mutex");
exit(EXIT_FAILURE);
} // 初始化worker线程的suffix_cache
me->suffix_cache = cache_create("suffix", SUFFIX_SIZE, sizeof(char*),
NULL, NULL);
if (me->suffix_cache == NULL) {
fprintf(stderr, "Failed to create suffix cache\n");
exit(EXIT_FAILURE);
}
}

至此,LIBEVENT_THREAD中,一些字段初始化工作都已准备就绪。

create_worker

接着看第二个for循环中的create_worker函数,就是利用pthread_create,真正的创建线程。

static void create_worker(void *(*func)(void *), void *arg) {
pthread_attr_t attr;
int ret; pthread_attr_init(&attr);

// 利用ptread_create创建线程,线程执行函数为worker_libevent
if ((ret = pthread_create(&((LIBEVENT_THREAD*)arg)->thread_id, &attr, func, arg)) != ) {
fprintf(stderr, "Can't create thread: %s\n",
strerror(ret));
exit();
}
}

值得一提的是,线程被创建时指定的执行函数为worker_libevent。这意味着,4条worker线程被pthread真正创建之后,会进入到worker_libevent。来看看worker_libevent:

static void *worker_libevent(void *arg) {
LIBEVENT_THREAD *me = arg; // 初始化logger
me->l = logger_create();
if (me->l == NULL) {
abort();
} // 利用条件变量与主线程同步
register_thread_initialized(); // 一旦执行下面一句,worker线程就开始挂起,直到接收主线程通知
event_base_loop(me->base, );
return NULL;
}

其中的register_thread_initialized要与memcached_thread_init函数最后三句结合一起来看:

pthread_mutex_lock(&init_lock);
wait_for_thread_registration(nthreads);
pthread_mutex_unlock(&init_lock);

这是利用条件变量进行线程同步的一个很经典的做法。

wait_for_thread_registration表示,主线程挂起在条件变量上,只要init_count<4则主线程一直会挂起。而子线程的register_thread_initialized表示,子线程一旦init完成,就将init_count++,并且告知主线程,主线程随后继续判断init_count是否<4。最终,只有当4条worker线程都init结束之后,主线程才会结束挂起,继续向下执行。

至此,worker线程初始化的相关源码都已经阅读完了。memcached的源码相对其他一些开源项目,其实还是很清晰明了的。

marster线程的监听

主线程初始化完worker线程之后,会进一步处理网络连接。通过代码可以看到,memcached可以支持三种协议:uds(unix domain socket),tcp,udp。

// 如果指定了uds路径,则创建unix domain socket
if (settings.socketpath != NULL) {
errno = ;
if (server_socket_unix(settings.socketpath, settings.access)) {
vperror("failed to listen on UNIX socket: %s", settings.socketpath);
exit(EX_OSERR);
}
} // 否则,创建tcp和udp的socket
if (settings.socketpath == NULL) {
... // tcp
errno = ;
if (settings.port && server_sockets(settings.port, tcp_transport,
portnumber_file)) {
vperror("failed to listen on TCP port %d", settings.port);
exit(EX_OSERR);
} // udp
errno = ;
if (settings.udpport && server_sockets(settings.udpport, udp_transport,
portnumber_file)) {
vperror("failed to listen on UDP port %d", settings.udpport);
exit(EX_OSERR);
} ...
}

memcached的启动参数-p和-U分别控制了setting.port和setting.udpport。

举例,如果想只支持tcp协议的连接,监听端口号为8888,不支持udp连接,我们可以按如下方式启动:

memcached -p8888 -U0

OK,接下来就是具体来看socket的处理了。

server_socket

先画一个简单的调用关系图:

server_sockets函数稍微有些复杂,但是其本质上是调用server_socket函数来创建一个个socket。在server_socket中,具体完成了初始化socket,绑定IP和端口,添加监听的事件等工作。

我们直接来看server_socket:

static int server_socket(const char *interface,
int port,
enum network_transport transport,
FILE *portnumber_file) {
int sfd;
struct linger ling = {, };
struct addrinfo *ai;
struct addrinfo *next;
struct addrinfo hints = { .ai_flags = AI_PASSIVE,
.ai_family = AF_UNSPEC };
char port_buf[NI_MAXSERV];
int error;
int success = ;
int flags = ; // 区分udp还是tcp
hints.ai_socktype = IS_UDP(transport) ? SOCK_DGRAM : SOCK_STREAM; if (port == -) {
port = ;
}
snprintf(port_buf, sizeof(port_buf), "%d", port); // 调用getaddrinfo来返回ai链表
error = getaddrinfo(interface, port_buf, &hints, &ai);
if (error != ) {
if (error != EAI_SYSTEM)
fprintf(stderr, "getaddrinfo(): %s\n", gai_strerror(error));
else
perror("getaddrinfo()");
return ;
} // 遍历ai链表,针对每个地址创建socket
for (next= ai; next; next= next->ai_next) {
conn *listen_conn_add; // 调用socket()接口来实际创建套接字,并且将套接字设置成“非阻塞”
if ((sfd = new_socket(next)) == -) {
/* getaddrinfo can return "junk" addresses,
* we make sure at least one works before erroring.
*/
if (errno == EMFILE) {
/* ...unless we're out of fds */
perror("server_socket");
exit(EX_OSERR);
}
continue;
} ... // 设置套接字的一些其他选项
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, (void *)&flags, sizeof(flags));
if (IS_UDP(transport)) {
maximize_sndbuf(sfd);
} else {
error = setsockopt(sfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&flags, sizeof(flags));
if (error != )
perror("setsockopt"); error = setsockopt(sfd, SOL_SOCKET, SO_LINGER, (void *)&ling, sizeof(ling));
if (error != )
perror("setsockopt"); error = setsockopt(sfd, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags));
if (error != )
perror("setsockopt");
} // 调用bind
if (bind(sfd, next->ai_addr, next->ai_addrlen) == -) {
if (errno != EADDRINUSE) {
perror("bind()");
close(sfd);
freeaddrinfo(ai);
return ;
}
close(sfd);
continue;
} else {
success++; // 调用listen
if (!IS_UDP(transport) && listen(sfd, settings.backlog) == -) {
perror("listen()");
close(sfd);
freeaddrinfo(ai);
return ;
}
...
} if (IS_UDP(transport)) {
...
} else {
// 初始化conn,设置对sfd的监听事件
if (!(listen_conn_add = conn_new(sfd, conn_listening,
EV_READ | EV_PERSIST, ,
transport, main_base))) {
fprintf(stderr, "failed to create listening connection\n");
exit(EXIT_FAILURE);
}
// 加入conn链表的头部
listen_conn_add->next = listen_conn;
listen_conn = listen_conn_add;
}
} freeaddrinfo(ai); /* Return zero iff we detected no errors in starting up connections */
return success == ;
}

这段代码虽然比较长,但实际上并不复杂。归根结底,依然是我们熟悉的服务端编程,socket,bind,listen...等等。其中比较关键的一点是利用conn_new函数来创建conn,以及设置监听事件。conn在memcached中是一个很重要的数据结构,后文会具体分析。来看下设置监听事件:

event_set(&c->event, sfd, event_flags, event_handler, (void *)c);
event_base_set(base, &c->event);
c->ev_flags = event_flags; if (event_add(&c->event, ) == -) {
perror("event_add");
return NULL;
}

这段代码可以看出,一旦前面socket创建的fd有监听到请求,则会触发调用event_handler函数。event_handler是一个非常核心的函数,它里面维护了一个状态机,根据请求的状态分别进行不同的处理。event_handler的具体实现我们也放到后面再看。

当主线程完成server_sockets处理之后,在main函数中,会调用event_base_loop:

/* enter the event loop */
if (event_base_loop(main_base, ) != ) {
retval = EXIT_FAILURE;
}

至此,主线程会进入所谓的event loop,每当有client发起连接,主线程便会执行上述的event_handler。

关于conns

前文提到在sever_socket中利用conn_new函数来创建conn连接。conn结构体在memcache中扮演着比较重要的角色。无论是主线程的监听,还是子线程与各个client进行交互处理,用到的都是这些conn。

master线程只绑定了一个event事件,client发起连接时被触发。对应的event_handler函数真正去处理的,便是传入的conn对象。

每个woker线程会绑定多个event事件,具体要视一个worker线程处理多少client的访问有关。但是每个worker线程,只有一个事件是与master线程有关的。那就是master接收到client发起的连接之后,会利用pipe通知一个worker,这个过程我们称之为“分发”。woker线程收到通知后,触发该事件,对应的函数为thread_libevent_process(参考setup_thread一小节)函数。thread_libevent_process中对于新的连接到来,也会立即用conn_new函数生成一个conn。至此,这个client与woker的所有交互,都是利用该conn完成。

画几个简单的示意图:

A,有新的连接

B,分发给worker1之后

C,又有新的连接

D,分发给worker2之后

E,建立了若干连接之后

memcached源码剖析4:并发模型的更多相关文章

  1. Memcached源码分析之线程模型

    作者:Calix 一)模型分析 memcached到底是如何处理我们的网络连接的? memcached通过epoll(使用libevent,下面具体再讲)实现异步的服务器,但仍然使用多线程,主要有两种 ...

  2. memcached源码剖析5:并发模型

    网络连接建立与分发 前面分析了worker线程的初始化,以及主线程创建socket并监听的过程.本节会分析连接如何建立与分发. 初始状态 A,可以摸清楚master线程的大致逻辑: 1)初始化各个wo ...

  3. memcached源码剖析——流程图

    参考: http://blog.csdn.net/column/details/memcached-src.html http://calixwu.com/2014/11/memcached-yuan ...

  4. Memcached源码分析之从SET命令开始说起

    作者:Calix 如果直接把memcached的源码从main函数开始说,恐怕会有点头大,所以这里以一句经典的“SET”命令简单地开个头,算是回忆一下memcached的作用,后面的结构篇中关于命令解 ...

  5. Memcached源码分析

    作者:Calix,转载请注明出处:http://calixwu.com 最近研究了一下memcached的源码,在这里系统总结了一下笔记和理解,写了几 篇源码分析和大家分享,整个系列分为“结构篇”和“ ...

  6. 并发编程之 ConcurrentLinkedQueue 源码剖析

    前言 今天我们继续分析 java 并发包的源码,今天的主角是谁呢?ConcurrentLinkedQueue,上次我们分析了并发下 ArrayList 的替代 CopyOnWriteArrayList ...

  7. 并发编程之 CopyOnWriteArrayList 源码剖析

    前言 ArrayList 是一个不安全的容器,在多线程调用 add 方法的时候会出现 ArrayIndexOutOfBoundsException 异常,而 Vector 虽然安全,但由于其 add ...

  8. petite-vue源码剖析-沙箱模型

    在解析v-if和v-for等指令时我们会看到通过evaluate执行指令值中的JavaScript表达式,而且能够读取当前作用域上的属性.而evaluate的实现如下: const evalCache ...

  9. 并发编程之 ThreadLocal 源码剖析

    前言 首先看看 JDK 文档的描述: 该类提供了线程局部 (thread-local) 变量.这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局 ...

随机推荐

  1. vue+webpack项目中使用dev-server搭建虚拟服务器,请求json文件数据,实现前后台分离开发

    在项目开发中,前后台分离,做了假数据,项目使用vue2.0重构,后台也推到重来了,为了不耽误开发进程,我做了虚拟的数据请求,使用vue-cli脚手架搭建的项目文件中dev-server搭建虚拟api请 ...

  2. slot的使用

    1.slot https://www.w3cplus.com/vue/vue-slot.html 2.ref https://www.cnblogs.com/xumqfaith/p/7743387.h ...

  3. [javaSE] IO流(FIle对象递归文件列表)

    获取File对象,new出来,构造参数:String目录名 调用File对象的list()方法,获取String[]数组文件名称 循环数组,列出所有文件包含隐藏文件 递归列出所有的数据 定义一个静态方 ...

  4. poj 1811 Prime Test 大数素数测试+大数因子分解

    Prime Test Time Limit: 6000MS   Memory Limit: 65536K Total Submissions: 27129   Accepted: 6713 Case ...

  5. sql 模糊搜素拼接

    if($irb_order!=''){ $condition .= " AND d.irb_order like '%".$irb_order."%'"; } ...

  6. JS获取今天年月日

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  7. Mysql分布式部署高可用集群方案

    HAproxy+Mycat +MySQL主从集群高可用方案 1.         HAproxy高可用方案: haproxy+keepalived,利用keepalived的VIP浮动能力,(多台ha ...

  8. drupal7 获取当前使用的主题的名称

    直接引用全局变量就行: 参考: 代码测试: global $theme, $theme_key; echo $theme; echo '<br>'; echo $theme_key; 结果 ...

  9. 支持n多视频网站的下载,推荐 you-get

    其实刚开始是想下载youtube视频, 网上找了下 其中之一:http://jingyan.baidu.com/article/39810a23d2deb2b637fda66c.html 访问一个在线 ...

  10. python 类方法的互相调用及self的含义

    每次调用内部的方法时,方法前面加 self.   class MyClass: def __init__(self): pass def func1(self): # do something pri ...