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後端農民工必讀。