memcache 多线程 并发模型

memcached,相信我们搞linux后端的农民工都知道!这里简单的分析一下memcached是如何处理大量并发的连接的。

如题,memcached是个单进程程序,单进程多线程的程序(linuxer可能会会心一笑,这不就是多进程嘛)。memcached底层是用的libevent来管理事件的,下面我们就来看看这个libevent的经典应用是如何运转的。其实一开始memcached是个正宗的单进程程序,其实使用了异步技术后基本能把cpu和网卡的性能发挥到极限了(这种情况下硬是多线程反而会使程序性能下降),只不过后来随着多核cpu的普及,为了榨光cpu的性能,引入多线程也是顺势而为。

memcached的源码结构非常简单,其中线程相关的代码基本都在Thread.c中。简单的说,memcached的众多线程就是个Master-Worker的模型,其中主线程负责接收连接,然后将连接分给各个worker线程,在各个worker线程中完成命令的接收,处理和返回结果。上个图:

OK,让我们从main函数开始,一步一步来。

main函数中,线程相关的代码基本就下面几行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
case 't' :
     //此处处理-t参数,设置线程数。
     //注意下面的WARNING,线程数超过了cpu核的个数其实没有意义了,只会有负作用
     settings.num_threads = atoi (optarg);
     if (settings.num_threads <= 0) {
         fprintf (stderr, "Number of threads must be greater than 0\n" );
         return 1;
     }
     /* 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 > 64) {
         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 ;
 
//此处调用线程初始化函数,main_base是主线程的libevent句柄,
//由于libevent不支持多线程共享句柄,所以每个线程都有一个libevent句柄
/* start up worker threads if MT mode */
thread_init(settings.num_threads, main_base);

下面,进入线程的初始化环节,在看thread_init这个函数之前,先看几个结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//worker线程结构体
typedef struct {
     pthread_t thread_id;        /* 线程ID */
     struct event_base *base;    /* 此线程的libevent句柄 */
     struct event notify_event;  /* 通知事件,主线程通过这个事件通知worker线程有新连接 */
     int notify_receive_fd;      /* 通知事件关联的读fd,这和下面的notify_send_fd是一对管道,具体使用后面讲 */
     int notify_send_fd;         /* 通知事件关联的写fd,后面讲 */
     struct thread_stats stats;  /* 线程相关统计相信 */
     struct conn_queue *new_conn_queue; /* 由主线程分配过来还没来得及处理的连接(客户端)的队列 */
     cache_t *suffix_cache;      /* suffix cache */
} LIBEVENT_THREAD;
 
//这是主线程的结构体,就比较简单了
//这个结构体的实例只有一个全局的dispatcher_thread
typedef struct {
     pthread_t thread_id;        /* 主线程ID */
     struct event_base *base;    /* libevent句柄 */
} LIBEVENT_DISPATCHER_THREAD;

下面进入线程初始化函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void thread_init( int nthreads, struct event_base *main_base) {
     int         i;
 
     //先是初始化一堆锁
 
     //这是主锁,用来同步key-value缓存的存取
     pthread_mutex_init(&cache_lock, NULL);
 
     //这是缓存状态锁,用来同步memcached的一些统计数据的存取
     pthread_mutex_init(&stats_lock, NULL);
 
     //这个锁是用来同步init_count(已初始化完的线程数)变量的存取
     pthread_mutex_init(&init_lock, NULL);
 
     //这是用来通知所有线程都初始化完成的条件变量
     pthread_cond_init(&init_cond, NULL);
 
     //这个锁是用来同步空闲连接链表的存取
     pthread_mutex_init(&cqi_freelist_lock, NULL);
     cqi_freelist = NULL;
 
     //分配worker线程结构体内存
     threads = calloc (nthreads, sizeof (LIBEVENT_THREAD));
     if (! threads) {
         perror ( "Can't allocate thread descriptors" );
         exit (1);
     }
 
     //把主线程先设置好
     dispatcher_thread.base = main_base;
     dispatcher_thread.thread_id = pthread_self();
 
     //设置所有worker线程与主线程之间的管道
     for (i = 0; i < nthreads; i++) {
         int fds[2];
         if (pipe(fds)) {
             perror ( "Can't create notify pipe" );
             exit (1);
         }
 
         threads[i].notify_receive_fd = fds[0];
         threads[i].notify_send_fd = fds[1];
 
         //这个函数进行worker线程的初始化工作
         //比如libevent句柄,连接队列等的初始化
         setup_thread(&threads[i]);
     }
 
     //这里就是真正调用pthread_create创建线程的地方了
     for (i = 0; i < nthreads; i++) {
         create_worker(worker_libevent, &threads[i]);
     }
 
     //主线程等所有的worker线程都跑起来了之后再跑后面的代码(接受连接)
     pthread_mutex_lock(&init_lock);
     while (init_count < nthreads) {
         pthread_cond_wait(&init_cond, &init_lock);
     }
     pthread_mutex_unlock(&init_lock);
}

好了,初始化完成,各个线程(包括主线程)都跑了起来,下面我们看看具体的连接是怎么处理的。

先看主线程,在thread_init返回(所有线程初始化完成)之后,main函数做了一些其他的初始化之后就调用了event_base_loop(main_base, 0);这个函数开始处理网络事件,接受连接了。在此之前,main函数在绑定监听端口的时候就已经把监听socket的事件加到了main_base中了(参看server_socket函数,不多说)。监听事件的回调函数是memcached中所有网络事件公用的回调函数event_handler,而这个event_handler也是基本什么都不干,直接又调用drive_machine,这个函数是由一个大大是switch组成的大状态机。这里就是memcached所有网络事件的处理中枢,我们来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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;
 
     while (!stop) {
 
         switch (c->state) {
         //这个监听状态只有主线程的监听fd才会有,而主线程也就基本就这么一个状态
         case conn_listening:
             //到这,说明有新连接来了
             //accept新连接
             addrlen = sizeof (addr);
             if ((sfd = accept(c->sfd, ( struct sockaddr *)&addr, &addrlen)) == -1) {
                 if ( errno == EAGAIN || errno == EWOULDBLOCK) {
                     /* these are transient, so don't log anything */
                     stop = true ;
                 } else if ( errno == EMFILE) {
                     if (settings.verbose > 0)
                         fprintf (stderr, "Too many open connections\n" );
                     accept_new_conns( false );
                     stop = true ;
                 } else {
                     perror ( "accept()" );
                     stop = true ;
                 }
                 break ;
             }
 
             //设置套接字非阻塞
             if ((flags = fcntl(sfd, F_GETFL, 0)) < 0 ||
                 fcntl(sfd, F_SETFL, flags | O_NONBLOCK) < 0) {
                 perror ( "setting O_NONBLOCK" );
                 close(sfd);
                 break ;
             }
 
             //将新连接分给worker线程
             dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST,
                                      DATA_BUFFER_SIZE, tcp_transport);
             stop = true ;
             break ;
 
             //下面是worker线程的一些事件,此处略
         case conn_waiting:
             //...
 
         case conn_read:
             //...
     }
 
     return ;
}

接着看dispatch_conn_new这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void dispatch_conn_new( int sfd, enum conn_states init_state, int event_flags,
                        int read_buffer_size, enum network_transport transport) {
     //分配一个连接队列item,此item将会由主线程塞到worker线程的连接队列中
     CQ_ITEM *item = cqi_new();
 
     //RR轮询得到这个连接的目标线程
     int tid = (last_thread + 1) % settings.num_threads;
     LIBEVENT_THREAD * thread = threads + tid;
     last_thread = tid;
 
     //初始化item
     item->sfd = sfd;
     item->init_state = init_state;
     item->event_flags = event_flags;
     item->read_buffer_size = read_buffer_size;
     item->transport = transport;
 
     //将item塞到worker线程的队列中
     cq_push( thread ->new_conn_queue, item);
 
     MEMCACHED_CONN_DISPATCH(sfd, thread ->thread_id);
     //向worker线程的通知写fd中写一个字节,如此notify_receive_fd就会有一个字节可读
     //这样worker线程的notify_event就会收到一个可读的事件
     //memcached就是这样来达到线程间异步通知的目的,很tricky
     if (write( thread ->notify_send_fd, "" , 1) != 1) {
         perror ( "Writing to thread notify pipe" );
     }
}

好了,自此主线程处理连接的逻辑基本就没了,下面看看worker线程的相关代码。worker线程初始化完成后将notify_event的libevent事件的回调注册到了thread_libevent_process上,来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static void thread_libevent_process( int fd, short which, void *arg) {
     LIBEVENT_THREAD *me = arg;
     CQ_ITEM *item;
     char buf[1];
 
     //将主线程写入的一个字节读掉,一个字节代表一个连接
     if (read(fd, buf, 1) != 1)
         if (settings.verbose > 0)
             fprintf (stderr, "Can't read from libevent pipe\n" );
 
     //将主线程塞到队列中的连接pop出来
     item = cq_pop(me->new_conn_queue);
 
     if (NULL != item) {
         //初始化新连接,注册事件监听,回调到前面提到的event_handler上
         conn *c = conn_new(item->sfd, item->init_state, item->event_flags,
                            item->read_buffer_size, item->transport, me->base);
         if (c == NULL) {
             if (IS_UDP(item->transport)) {
                 fprintf (stderr, "Can't listen for events on UDP socket\n" );
                 exit (1);
             } else {
                 if (settings.verbose > 0) {
                     fprintf (stderr, "Can't listen for events on fd %d\n" ,
                         item->sfd);
                 }
                 close(item->sfd);
             }
         } else {
             c-> thread = me;
         }
         //回收item
         cqi_free(item);
     }
}

好了,这样worker线程就多了一个连接了,后面worker线程就是不断的监听notify事件添加连接和客户端连接socket的网络IO事件处理业务了。

memcached的这套多线程libevent机制几乎成了高性能服务器的一本教材。linux后端农民工必读。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值