memcached Master-Worker 模型分析__網路編程

來源:互聯網
上載者:User

memcached,相信我們搞linux後端的農民工都知道。這裡簡單的分析一下memcached是如何處理大量並發的串連的。

如題,memcached是個單進程程式,單進程多線程的程式(linuxer可能會會心一笑,這不就是多進程嘛)。memcached底層是用的libevent來管理事件的,下面我們就來看看這個libevent的經典應用是如何運轉的。其實一開始memcached是個正宗的單進程程式,其實使用了非同步技術後基本能把cpu和網卡的效能發揮到極限了(這種情況下硬是多線程反而會使程式效能下降),只不過後來隨著多核cpu的普及,為了榨光cpu的效能,引入多線程也是順勢而為。

memcached的源碼結構非常簡單,其中線程相關的代碼基本都在Thread.c中。簡單的說,memcached的眾多線程就是個Master-Worker的模型,其中主線程負責接收串連,然後將串連分給各個worker線程,在各個worker線程中完成命令的接收,處理和返回結果。

OK,讓我們從main函數開始,一步一步來。

main函數中,線程相關的代碼基本就下面幾行:

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");        return1;    }    /* 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這個函數之前,先看幾個結構體:
//worker線程結構體typedef struct{    pthread_t thread_id;        /* 線程ID */    structevent_base *base;    /* 此線程的libevent控制代碼 */    structevent notify_event;  /* 通知事件,主線程通過這個事件通知worker線程有新串連 */    intnotify_receive_fd;     /* 通知事件關聯的讀fd,這和下面的notify_send_fd是一對管道,具體使用後面講 */    intnotify_send_fd;        /* 通知事件關聯的寫fd,後面講 */    structthread_stats stats;  /* 線程相關統計相信 */    structconn_queue *new_conn_queue; /* 由主線程分配過來還沒來得及處理的串連(用戶端)的隊列 */    cache_t *suffix_cache;      /* suffix cache */} LIBEVENT_THREAD; //這是主線程的結構體,就比較簡單了//這個結構體的執行個體只有一個全域的dispatcher_threadtypedef struct{    pthread_t thread_id;        /* 主線程ID */    structevent_base *base;    /* libevent控制代碼 */} LIBEVENT_DISPATCHER_THREAD;
下面進入線程初始化函數:
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++) {        intfds[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所有網路事件的處理中樞,我們來看看:

static void drive_machine(conn *c) {    boolstop = false;    intsfd, flags = 1;    socklen_t addrlen;    structsockaddr_storage addr;    intnreqs = settings.reqs_per_event;    intres;     while(!stop) {         switch(c->state) {        //這個監聽狀態只有主線程的監聽fd才會有,而主線程也就基本就這麼一個狀態        caseconn_listening:            //到這,說明有新串連來了            //accept新串連            addrlen = sizeof(addr);            if((sfd = accept(c->sfd, (structsockaddr *)&addr, &addrlen)) == -1) {                if(errno== EAGAIN || errno== EWOULDBLOCK) {                    /* these are transient, so don't log anything */                    stop = true;                }elseif(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線程的一些事件,此處略        caseconn_waiting:            //...         caseconn_read:            //...    }     return;}

接著看dispatch_conn_new這個函數:

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輪詢得到這個串連的目標線程    inttid = (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上,來看看:

static void thread_libevent_process(int fd,short which,void *arg) {    LIBEVENT_THREAD *me = arg;    CQ_ITEM *item;    charbuf[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後端農民工必讀。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.