memcached源码剖析4:并发模型
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:并发模型的更多相关文章
- Memcached源码分析之线程模型
作者:Calix 一)模型分析 memcached到底是如何处理我们的网络连接的? memcached通过epoll(使用libevent,下面具体再讲)实现异步的服务器,但仍然使用多线程,主要有两种 ...
- memcached源码剖析5:并发模型
网络连接建立与分发 前面分析了worker线程的初始化,以及主线程创建socket并监听的过程.本节会分析连接如何建立与分发. 初始状态 A,可以摸清楚master线程的大致逻辑: 1)初始化各个wo ...
- memcached源码剖析——流程图
参考: http://blog.csdn.net/column/details/memcached-src.html http://calixwu.com/2014/11/memcached-yuan ...
- Memcached源码分析之从SET命令开始说起
作者:Calix 如果直接把memcached的源码从main函数开始说,恐怕会有点头大,所以这里以一句经典的“SET”命令简单地开个头,算是回忆一下memcached的作用,后面的结构篇中关于命令解 ...
- Memcached源码分析
作者:Calix,转载请注明出处:http://calixwu.com 最近研究了一下memcached的源码,在这里系统总结了一下笔记和理解,写了几 篇源码分析和大家分享,整个系列分为“结构篇”和“ ...
- 并发编程之 ConcurrentLinkedQueue 源码剖析
前言 今天我们继续分析 java 并发包的源码,今天的主角是谁呢?ConcurrentLinkedQueue,上次我们分析了并发下 ArrayList 的替代 CopyOnWriteArrayList ...
- 并发编程之 CopyOnWriteArrayList 源码剖析
前言 ArrayList 是一个不安全的容器,在多线程调用 add 方法的时候会出现 ArrayIndexOutOfBoundsException 异常,而 Vector 虽然安全,但由于其 add ...
- petite-vue源码剖析-沙箱模型
在解析v-if和v-for等指令时我们会看到通过evaluate执行指令值中的JavaScript表达式,而且能够读取当前作用域上的属性.而evaluate的实现如下: const evalCache ...
- 并发编程之 ThreadLocal 源码剖析
前言 首先看看 JDK 文档的描述: 该类提供了线程局部 (thread-local) 变量.这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局 ...
随机推荐
- vue+webpack项目中使用dev-server搭建虚拟服务器,请求json文件数据,实现前后台分离开发
在项目开发中,前后台分离,做了假数据,项目使用vue2.0重构,后台也推到重来了,为了不耽误开发进程,我做了虚拟的数据请求,使用vue-cli脚手架搭建的项目文件中dev-server搭建虚拟api请 ...
- slot的使用
1.slot https://www.w3cplus.com/vue/vue-slot.html 2.ref https://www.cnblogs.com/xumqfaith/p/7743387.h ...
- [javaSE] IO流(FIle对象递归文件列表)
获取File对象,new出来,构造参数:String目录名 调用File对象的list()方法,获取String[]数组文件名称 循环数组,列出所有文件包含隐藏文件 递归列出所有的数据 定义一个静态方 ...
- poj 1811 Prime Test 大数素数测试+大数因子分解
Prime Test Time Limit: 6000MS Memory Limit: 65536K Total Submissions: 27129 Accepted: 6713 Case ...
- sql 模糊搜素拼接
if($irb_order!=''){ $condition .= " AND d.irb_order like '%".$irb_order."%'"; } ...
- JS获取今天年月日
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- Mysql分布式部署高可用集群方案
HAproxy+Mycat +MySQL主从集群高可用方案 1. HAproxy高可用方案: haproxy+keepalived,利用keepalived的VIP浮动能力,(多台ha ...
- drupal7 获取当前使用的主题的名称
直接引用全局变量就行: 参考: 代码测试: global $theme, $theme_key; echo $theme; echo '<br>'; echo $theme_key; 结果 ...
- 支持n多视频网站的下载,推荐 you-get
其实刚开始是想下载youtube视频, 网上找了下 其中之一:http://jingyan.baidu.com/article/39810a23d2deb2b637fda66c.html 访问一个在线 ...
- python 类方法的互相调用及self的含义
每次调用内部的方法时,方法前面加 self. class MyClass: def __init__(self): pass def func1(self): # do something pri ...