zoukankan      html  css  js  c++  java
  • Memcached网络模型

    之前用libevent开发了一个流媒体服务器。用线程池实现的。之后又看了memcached的网络相关实现,今天来整理一下memcached的实现流程。

    memcached不同于Redis的单进程单线程,是采用多线程的工作方式。有一个主线程,同时维护了一个线程池(工作线程)。worker thread工作线程和main thread主线程之间主要通过pipe来进行通信。因为用了libevent,所以感觉比Redis稍微庞大点,没有在生产环境对比过Redis和memcached,所以也不好说什么性能比对。

    主线程和工作线程都有一个event base,大体框架如下图:

                                                         093b7a63-e974-34c5-9b9c-04c68432d8d6

    整体框架图:

                                  9b09f028-310a-3ee3-9b12-78e5ab202105

    线程模型:

                                            22312037_1365061875gws7

    每个线程包括主线程都各自有独立的Libevent实例,Memcached的listen fd注册到主线程的Libevent实例上,由主线程来accept新的连接,接受新的连接后根据Round-robin算法选择工作线程,将新连接的socket fd封装为CQ_ITEM后push到所选工作线程的CQ队列中,然后主线程(notify_send_fd)通过管道发送字符“c”到工作线程(notify_receive_fd),而notify_receive_fd已经注册到工作线程的Libevent实例上了,这样工作线程就能收到通知“c”,然后从该工作线程的CQ队列中pop出CQ_ITEM进而取出新连接并将fd注册到工作线程的Libevent实例上,从而由工作线程来处理该连接的所有后续事件

    代码层实现图:

              22312037_1365061637v66N

                                 540df15e239ea56d310b4a167f8f908520150406073128

    源码解读:

    1. 关键数据结构

    /* An item in the connection queue. */主要用于存储用户socket连接的基本信息

    主线程会将用户的socket连接信息封装成CQ_ITEM,并放入工作线程的处理队列中。工作线程得到主线程的pipe通知后,就会将队列中的ITEM取出来,创建libevent的socket读事件。
    typedef struct conn_queue_item CQ_ITEM;
    struct conn_queue_item {
        int               sfd;//socket fd
        enum conn_states  init_state;//事件类型
        int               event_flags;//libevent的flag
        int               read_buffer_size;//读取buffer的大小
        enum network_transport     transport;
        CQ_ITEM          *next;//下一个CQ_ITEM
    };

    /**
    * The structure representing a connection into memcached.
    */
    typedef struct conn conn;
    struct conn {
        int    sfd;
        struct event event;
        short  ev_flags;
        short  which;   /** which events were just triggered */

        LIBEVENT_THREAD *thread; /* Pointer to the thread object serving this connection */
    };


    /* A connection queue. */
    typedef struct conn_queue CQ;
    struct conn_queue {
        CQ_ITEM *head;//指向队列的第一个节点
        CQ_ITEM *tail;//指向队列的最后一个节点
        pthread_mutex_t lock;//一个队列就对应一个锁
    };

    typedef struct {
        pthread_t thread_id;        /* unique ID of this thread */线程ID
        struct event_base *base;    /* libevent handle this thread uses */线程所使用的event_base
        struct event notify_event;  /* listen event for notify pipe */用于监听管道读事件的event
        int notify_receive_fd;      /* receiving end of notify pipe */管道的读端fd
        int notify_send_fd;         /* sending end of notify pipe */管道的写端fd
        struct thread_stats stats;  /* Stats generated by this thread */
        struct conn_queue *new_conn_queue; /* queue of new connections to handle */
        cache_t *suffix_cache;      /* suffix cache */
    } LIBEVENT_THREAD;

    2. CQ队列示意图

                                                20150114214203308

    CQ_ITEM的创建实现了一个内存池,具体可以参考函数static CQ_ITEM *cqi_new(void)和static void cqi_free(CQ_ITEM *item),解决频繁分配释放内存而产生的内存碎片问题。

    3.文字代码流程

    (1)在main函数中调用main_base = event_init()来初始化主线程Libevent实例。
    (2)在main函数中调用thread_init来初始化工作线程,并将主线程Libevent实例作为参数传入。
    (3)在thread_init函数中为指定数量的工作线程分配内存,为每个线程创建管道,并分别绑定到通知收和发的socket描述符上,调用函数setup_thread初始化线程信息,调用函数create_worker为每个线程注册回调函数。每个线程有一个CQ队列。关键代码:
            for (i = 0; i < nthreads; i++) {
                    int fds[2];
                    if (pipe(fds)) {
                            ...
                    }
                    threads[i].notify_receive_fd = fds[0];
                    threads[i].notify_send_fd = fds[1];
                   setup_thread(&threads[i]);
                   ...
            }
            /* Create threads after we've done all the libevent setup. */
            for (i = 0; i < nthreads; i++) {
                create_worker(worker_libevent, &threads[i]);
            }

    (4)在setup_thread函数中,为工作线程初始化Libevent实例,为主线程通知读(notify_receive_fd)注册回调函数thread_libevent_process,初始化cq队列,关键代码如下:
            static void setup_thread(LIBEVENT_THREAD *me) {
                    me->base = event_init();
                    ...
                    /* Listen for notifications from other threads */
                    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, 0) == -1) {
                            ...
                    }
                    me->new_conn_queue = malloc(sizeof(struct conn_queue));
                    ...
                    cq_init(me->new_conn_queue);
                    ...
            }

    (5)在thread_libevent_process函数中,读取主线程发送的通知接收消息,将主线程accept来的fd注册到工作线程的Libevent实例中,主线程accept来的fd从conn_queue队列获取,关键代码如下:
            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)
                            ...
                    switch (buf[0]) {
                    case 'c':
                    item = cq_pop(me->new_conn_queue);
                    if (NULL != item) {
                            conn *c = conn_new(item->sfd, item->init_state, item->event_flags,
                                                            item->read_buffer_size, item->transport, me->base);
                   ...
                    }
            }
    (6)在函数conn_new中,创建conn句柄,为句柄注册回调函数event_handler处理事件,将该句柄作为参数传入回调函数并设置到Libevent中,该函数的关键代码如下:
            conn *conn_new(const int sfd, enum conn_states init_state,
                            const int event_flags,
                            const int read_buffer_size, enum network_transport transport,
                            struct event_base *base) {
                conn *c = conn_from_freelist();
                if (NULL == c) {
                    if (!(c = (conn *)calloc(1, sizeof(conn)))) {
                            ...
                }
               ...
               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, 0) == -1) {
                      ...
               }
               ...
            }

    (7)在create_worker函数中,创建工作线程并注册回调函数,在工作线程的回调函数work_libevent中,开始Libevent主循环。

    (8)在main函数中,调用函数server_sockets,再调用函数server_socket,进而调用函数new_socket,在调用函数conn_new,创建并注册listen fd到主线程Libevent实例上,最后开始Libevent主循环即event_base_loop。在conn_new函数关键代码见步骤(6)
    (9)在event_handler函数中,调用函数drive_machine,在该函数中处理所有事件,其关键代码如下:
            static void drive_machine(conn *c) {
                    ...
                    while (!stop) {
                        switch(c->state) {
                            case conn_listening:
                                    addrlen = sizeof(addr);
                                    if ((sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen)) == -1) {
                                           ...                                     
                                    }
                                   ...
                                   if (settings.maxconns_fast &&
                                        stats.curr_conns + stats.reserved_fds >= settings.maxconns - 1) {
                                            ...
                                   } else {
                                        dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST,
                                                 DATA_BUFFER_SIZE, tcp_transport);
                                  }
                                stop = true;
                                break;
                                ...
                            }
                        }
                    return;
            }
            在处理事件时,如果是listening事件,则调用函数dispatch_conn_new(Memcached.c/3785行)将accept fd分配给工作线程。

    (10)在dispatch_conn_new函数中,根据round-robin算法将新连接push到所分配线程的CQ队列中,并通过管道发送通知消息“c,关键代码如下:
            void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags,
                                   int read_buffer_size, enum network_transport transport) {
                        CQ_ITEM *item = cqi_new();
                        char buf[1];
                        int tid = (last_thread + 1) % settings.num_threads;
                        LIBEVENT_THREAD *thread = threads + tid;
                        last_thread = tid;
                        ...
                        cq_push(thread->new_conn_queue, item);
                        ...
                        buf[0] = 'c';
                        if (write(thread->notify_send_fd, buf, 1) != 1) {
                            perror("Writing to thread notify pipe");
                        }
            }

    dispatch_conn_new函数只在主线程中调用,last_thread为静态变量,每次将该变量值+1,再模线程数来选择工作线程。

    4. conn状态机

                                                27767798_13534327399ZI6

    1.listening:这个状态是主线程的connection默认状态,它只有这一个状态,它做的工作就是把接到连接分发到worker子线程。

    2.conn_new_cmd:每个新连接的初始状态,这个状态会清空读写buf。

    3.conn_waiting:这个状态就是在event_base中设置读事件,然后状态机暂停,挂起当前connection(函数退出,回调函数的attachment会记录这个connection),等待有新的信息过来,然后通过回调函数的attachment重新找到这个connection,然后启动状态机。

    4.conn_read:该状态从sfd中读取客户端的指令信息。例如有用户提交数据过来的时候,工作线程监听到事件后,最终会走到这里。

    5.conn_parse_cmd:判断具体的指令,如果是update的指令,那么需要跳转到conn_nread中,因为需要在从网络中读取固定byte的数据,如果是查询之类的指令,就直接查询完成后,跳转到conn_mwrite中,返回数据

    6.conn_nread:从网络中读取指定大小的数据,这个数据就是更新到item的数据,然后将数据更新到hash和lru中去,然后跳转到conn_write

    7.conn_write:这个状态主要是调用out_string函数会跳转到这个状态,一般都是提示信息和返回的状态信息,然后输出这些数据,然后根据write_to_go的状态,继续跳转

    8.conn_mwrite:这个写是把connection中的msglist返回到客户端,这个msglist存的是item的数据,用于那种get等获得item的操作的返回数据。

    9.conn_swallow:对于那种update操作,如果分配item失败,显然后面的nread,是无效的,客户端是不知道的,这样客户端继续发送特定的数量的数据,就需要把读到的这些数据忽略掉,然后如果把后面指定的数据都忽略掉了(set的两部提交,数据部分忽略掉),那么connection跳转到conn_new_cmd,如果读nread的那些特定数量的数据没有读到,直接跳转到conn_closing。

    10.conn_closing:服务器端主动关闭连接,调用close函数关闭文件描述符,同时把conn结构体放到空闲队列中,供新的连接重用这写conn结构体。

    Memcached的数据结构那块源码还没有看过,下次也梳理下。看过Redis的,数据结构设计的还是十分精妙的。

    参考资料:

    http://www.lvtao.net/c/623.html?utm_source=tuicool

    http://www.linuxidc.com/Linux/2012-01/52515p3.htm

    http://blog.csdn.net/funkri/article/details/17022309

    http://blog.chinaunix.net/uid-16723279-id-3568441.html

    http://blog.csdn.net/luotuo44/article/details/42705475

    http://blog.chinaunix.net/uid-27767798-id-3415510.html

    感谢以上作者的无私分享。

  • 相关阅读:
    最长回文子串(马拉车)
    ubutun 启动/停止/重启MySQL数据库
    臭大佬_LZW
    Beauty Contest(求凸包最大直径)
    Smallest Bounding Rectangle(最小面积外接矩形)
    Triangle(求凸包最大内接三角形)
    P4196 [CQOI2006]凸多边形
    P4724 【模板】三维凸包(简洁)
    P2600 [ZJOI2008]瞭望塔【半平面交】
    退役记
  • 原文地址:https://www.cnblogs.com/chenxianpao/p/5572895.html
Copyright © 2011-2022 走看看