新聞中心
閱讀 memcached ***有 libevent 基礎(chǔ),memcached 是基于libevent 構(gòu)建起來(lái)的。通由 libevent 提供的事件驅(qū)動(dòng)機(jī)制觸發(fā) memcached 中的 IO 事件。

個(gè)人認(rèn)為,閱讀源碼的起初最忌鉆牛角尖,如頭文件里天花亂墜的結(jié)構(gòu)體到底有什么用。源文件里稀里嘩啦的函數(shù)是做什么的。剛開(kāi)始并沒(méi)必要事無(wú)巨細(xì)弄清楚頭文件每個(gè)類(lèi)型定義的具體用途;很可能那些是不緊要的工具函數(shù),知道他的功能和用法就沒(méi)他事了。
來(lái)看 memcached 內(nèi)部做了什么事情。memcached 是用 c 語(yǔ)言實(shí)現(xiàn),必須有一個(gè)入口函數(shù)main(),memcached 的生命從這里開(kāi)始。
初始化過(guò)程
建立并初始化 main_base,即主線(xiàn)程的事件中心,這是 libevent 里面的概念,可以把它理解為事件分發(fā)中心。
建立并初始化 memcached 內(nèi)部容器數(shù)據(jù)結(jié)構(gòu)。
建立并初始化空閑連接結(jié)構(gòu)體數(shù)組。
建立并初始化線(xiàn)程結(jié)構(gòu)數(shù)組,指定每個(gè)線(xiàn)程的入口函數(shù)是worker_libevent(),并創(chuàng)建工作線(xiàn)程。從worder_libevent()的實(shí)現(xiàn)來(lái)看,工作線(xiàn)程都會(huì)調(diào)用event_base_loop()進(jìn)入自己的事件循環(huán)。
根據(jù) memcached 配置,開(kāi)啟以下兩種服務(wù)模式中的一種:
以 UNIX 域套接字的方式接受客戶(hù)的請(qǐng)求
以 TCP/UDP 套接字的方式接受客戶(hù)的請(qǐng)求
memcached 有可配置的兩種模式: UNIX 域套接字和 TCP/UDP,允許客戶(hù)端以?xún)煞N方式向 memcached 發(fā)起請(qǐng)求??蛻?hù)端和服務(wù)器在同一個(gè)主機(jī)上的情況下可以用 UNIX 域套接字,否則可以采用 TCP/UDP 的模式。兩種模式是不兼容的。特別的,如果是 UNIX 域套接字或者 TCP 模式,需要建立監(jiān)聽(tīng)套接字,并在事件中心注冊(cè)了讀事件,回調(diào)函數(shù)是event_handler(),我們會(huì)看到所有的連接都會(huì)被注冊(cè)回調(diào)函數(shù)是 event_handler()。
調(diào)用event_base_loop()開(kāi)啟 libevent 的事件循環(huán)。到此,memcached 服務(wù)器的工作正式進(jìn)入了工作。如果遇到致命錯(cuò)誤或者客戶(hù)明令結(jié)束 memcached,那么才會(huì)進(jìn)入接下來(lái)的清理工作。
UNIX 域套接字和 UDP/TCP 工作模式
在初始化過(guò)程中介紹了這兩種模式,memcached 這么做為的是讓其能更加可配置。TCP/UDP 自不用說(shuō),UNIX 域套接字有獨(dú)特的優(yōu)勢(shì):
在同一臺(tái)主機(jī)上進(jìn)行通信時(shí),是不同主機(jī)間通信的兩倍
UNIX 域套接口可以在同一臺(tái)主機(jī)上,不同進(jìn)程之間傳遞套接字描述符
UNIX 域套接字可以向服務(wù)器提供客戶(hù)的憑證(用戶(hù)id或者用戶(hù)組id)
其他關(guān)于 UNIX 域套接字優(yōu)缺點(diǎn)的請(qǐng)參看: https://pangea.stanford.edu/computing/UNIX/overview/advantages.php
工作線(xiàn)程管理和線(xiàn)程調(diào)配方式
在thread_init(),setup_thread()函數(shù)的實(shí)現(xiàn)中,memcached 的意圖是很清楚的。每個(gè)線(xiàn)程都有自己獨(dú)有的連接隊(duì)列,即 CQ,注意這個(gè)連接隊(duì)列中的對(duì)象并不是一個(gè)或者多個(gè) memcached 命令,它對(duì)應(yīng)一個(gè)客戶(hù)! 一旦一個(gè)客戶(hù)交給了一個(gè)線(xiàn)程,它的余生就屬于這個(gè)線(xiàn)程了! 線(xiàn)程只要被喚醒就立即進(jìn)入工作狀態(tài),將自己 CQ 隊(duì)列的任務(wù)所有完完成。當(dāng)然,每一個(gè)工作線(xiàn)程都有自己的 libevent 事件中心。
很關(guān)鍵的線(xiàn)索是thread_init()的實(shí)現(xiàn)中,每個(gè)工作線(xiàn)程都創(chuàng)建了讀寫(xiě)管道,所能給我們的提示是: 只要利用 libevent 在工作線(xiàn)程的事件中心注冊(cè)讀管道的讀事件,就可以按需喚醒線(xiàn)程,完成工作,很有意思,而setup_thread()的工作正是讀管道的讀事件被注冊(cè)到線(xiàn) 程的事件中心,回調(diào)函數(shù)是thread_libevent_process().thread_libevent_process()的工作就是從工作線(xiàn) 程自己的 CQ 隊(duì)列中取出任務(wù)執(zhí)行,而往工作線(xiàn)程工作隊(duì)列中添加任務(wù)的是dispatch_conn_new(),此函數(shù)一般由主線(xiàn)程調(diào)用。下面是主線(xiàn)程和工作線(xiàn)程的工 作流程:
前幾天在微博上,看到 @高端小混混 的微博,轉(zhuǎn)發(fā)了:
“多任務(wù)并行處理的兩種方式,一種是將所有的任務(wù)用隊(duì)列存儲(chǔ)起來(lái),每個(gè)工作者依次去 拿一個(gè)來(lái)處理,直到做完所有的>任務(wù)為止。另一種是將任務(wù)平均分給工作者,先做完任務(wù)的工作者就去別的工作者那里拿一些任務(wù)來(lái)做,同樣直到所有任務(wù) 做完為止。兩種方式的結(jié)果如何?根據(jù)自己的場(chǎng)景寫(xiě)碼驗(yàn)證?!?/p>
memcached 所采用的模式就是這里所說(shuō)的第二種! memcached 的線(xiàn)程分配模式是:一個(gè)主線(xiàn)程和多個(gè)工作線(xiàn)程。主線(xiàn)程負(fù)責(zé)初始化和將接收的請(qǐng)求分派給工作線(xiàn)程,工作線(xiàn)程負(fù)責(zé)接收客戶(hù)的命令請(qǐng)求和回復(fù)客戶(hù)。
#p#
存儲(chǔ)容器
memcached 是做緩存用的,內(nèi)部肯定有一個(gè)容器?;氐絤ain()中,調(diào)用assoc_init()初始化了容器–hashtable,采用頭插法插入新數(shù)據(jù),因?yàn)轭^ 插法是最快的。memcached 只做了一級(jí)的索引,即 hash; 接下來(lái)的就靠 memcmp() 在鏈表中找數(shù)據(jù)所在的位置。memcached 容器管理的接口主要在 item.h .c 中.
連接管理
每個(gè)連接都會(huì)建立一個(gè)連接結(jié)構(gòu)體與之對(duì)應(yīng)。main()中會(huì)調(diào)用conn_init()建立連接結(jié)構(gòu)體數(shù)組。連接結(jié)構(gòu)體 struct conn 記錄了連接套接字,讀取的數(shù)據(jù),將要寫(xiě)入的數(shù)據(jù),libevent event 結(jié)構(gòu)體以及所屬的線(xiàn)程信息。
當(dāng)有新的連接時(shí),主線(xiàn)程會(huì)被喚醒,主線(xiàn)程選定一個(gè)工作線(xiàn)程 thread0,在 thread0 的寫(xiě)管道中寫(xiě)入數(shù)據(jù),特別的如果是接受新的連接而不是接受新的數(shù)據(jù),寫(xiě)入管道的數(shù)據(jù)是字符 ‘c’。工作線(xiàn)程因管道中有數(shù)據(jù)可讀被喚醒,thread_libevent_process()被調(diào)用,新連接套接字被注冊(cè)了 event_handler()回調(diào)函數(shù),這些工作在conn_new()中完成。因此,客戶(hù)端有命令請(qǐng)求的時(shí)候(譬如發(fā)起 get key 命令),工作線(xiàn)程都會(huì)被觸發(fā)調(diào)用event_handler()。
當(dāng)出現(xiàn)致命錯(cuò)誤或者客戶(hù)命令結(jié)束服務(wù)(quit 命令),關(guān)于此連接的結(jié)構(gòu)體內(nèi)部的數(shù)據(jù)會(huì)被釋放(譬如曾經(jīng)讀取的數(shù)據(jù)),但結(jié)構(gòu)體本身不釋放,等待下一次使用。如果有需要,連接結(jié)構(gòu)體數(shù)組會(huì)指數(shù)自增。
一個(gè)請(qǐng)求的工作流程
memcached 服務(wù)一個(gè)客戶(hù)的時(shí)候,是怎么一個(gè)過(guò)程,試著去調(diào)試模擬一下。當(dāng)一個(gè)客戶(hù)向 memcached 發(fā)起請(qǐng)求時(shí),主線(xiàn)程會(huì)被喚醒,接受請(qǐng)求。接下來(lái)的工作在連接管理中有說(shuō)到。
客戶(hù)已經(jīng)與 memcached 服務(wù)器建立了連接,客戶(hù)在終端(黑框框)敲擊 get key + 回車(chē)鍵,一個(gè)請(qǐng)求包就發(fā)出去了。從連接管理中已經(jīng)了解到所有連接套接字都會(huì)被注冊(cè)回調(diào)函數(shù)為event_handler(),因此 event_handler()會(huì)被觸發(fā)調(diào)用。
- void event_handler(const int fd,const short which,void *arg) {
- conn *c;
- c = (conn *)arg;
- assert(c != NULL);
- c->whichwhich = which;
- /* sanity */
- if (fd != c->sfd) {
- if (settings.verbose > 0)
- fprintf(stderr,"Catastrophic: event fd doesn't match conn fd!\n");
- conn_close(c);
- return;
- }
- drive_machine(c);
- /* wait for next event */
- return;
- }
event_handler()調(diào)用了drive_machine().drive_machine()是請(qǐng)求處理的開(kāi)端,特別的當(dāng)有新的連接 時(shí),listen socket 也是有請(qǐng)求的,所以建立新的連接也會(huì)調(diào)用drive_machine(),這在連接管理有提到過(guò)。下面是drive_machine()函數(shù)的骨架:
- // 請(qǐng)求的開(kāi)端。當(dāng)有新的連接的時(shí)候 event_handler() 會(huì)調(diào)用此函數(shù)。
- 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;
- const char *str;
- assert(c != NULL);
- while (!stop) {
- // while 能保證一個(gè)命令被執(zhí)行完成或者異常中斷(譬如 IO 操作次數(shù)超出了一定的限制)
- switch(c->state) {
- // 正在連接,還沒(méi)有 accept
- case conn_listening:
- // 等待新的命令請(qǐng)求
- case conn_waiting:
- // 讀取數(shù)據(jù)
- case conn_read:
- // 嘗試解析命令
- case conn_parse_cmd :
- // 新的命令請(qǐng)求,只是負(fù)責(zé)轉(zhuǎn)變 conn 的狀態(tài)
- case conn_new_cmd:
- // 真正執(zhí)行命令的地方
- case conn_nread:
- // 讀取所有的數(shù)據(jù),拋棄!!! 一般出錯(cuò)的情況下會(huì)轉(zhuǎn)換到此狀態(tài)
- case conn_swallow:
- // 數(shù)據(jù)回復(fù)
- case conn_write:
- case conn_mwrite:
- // 連接結(jié)束。一般出錯(cuò)或者客戶(hù)顯示結(jié)束服務(wù)的情況下回轉(zhuǎn)換到此狀態(tài)
- case conn_closing:
- }
- }
- return;
- }
通過(guò)修改連接結(jié)構(gòu)體狀態(tài) struct conn.state 執(zhí)行相應(yīng)的操作,從而完成一個(gè)請(qǐng)求,完成后 stop 會(huì)被設(shè)置為 true,一個(gè)命令只有執(zhí)行結(jié)束(無(wú)論結(jié)果如何)才會(huì)跳出這個(gè)循環(huán)。我們看到 struct conn 有好多種狀態(tài),一個(gè)正常執(zhí)行的命令狀態(tài)的轉(zhuǎn)換是:
- conn_new_cmd->conn_waiting->conn_read->conn_parse_cmd->conn_nread->conn_mwrite->conn_close
這個(gè)過(guò)程任何一個(gè)環(huán)節(jié)出了問(wèn)題都會(huì)導(dǎo)致?tīng)顟B(tài)轉(zhuǎn)變?yōu)?conn_close。帶著剛開(kāi)始的問(wèn)題把從客戶(hù)連接到一個(gè)命令執(zhí)行結(jié)束的過(guò)程是怎么樣的:
客戶(hù)connect()后,memcached 服務(wù)器主線(xiàn)程被喚醒,接下來(lái)的調(diào)用鏈?zhǔn)莈vent_handler()->drive_machine()被調(diào)用,此時(shí)主線(xiàn)程對(duì)應(yīng) conn 狀態(tài)為 conn_listining,接受請(qǐng)求
- dispatch_conn_new(sfd,conn_new_cmd,EV_READ | EV_PERSIST,DATA_BUFFER_SIZE,tcp_transport);
dispatch_conn_new()的工作是往工作線(xiàn)程工作隊(duì)列中添加任務(wù)(前面已經(jīng)提到過(guò)),所以其中一個(gè)沉睡的工作線(xiàn)程會(huì)被喚醒,thread_libevent_process()會(huì)被工作線(xiàn)程調(diào)用,注意這些機(jī)制都是由 libevent 提供的。
thread_libevent_process()調(diào)用conn_new()新建 struct conn 結(jié)構(gòu)體,且狀態(tài)為 conn_new_cmd,其對(duì)應(yīng)的就是剛才accept()的連接套接字.conn_new()最關(guān)鍵的任務(wù)是將剛才接受的套接字在 libevent 中注冊(cè)一個(gè)事件,回調(diào)函數(shù)是event_handler()。循環(huán)繼續(xù),狀態(tài) conn_new_cmd 下的操作只是只是將 conn 的狀態(tài)轉(zhuǎn)換為 conn_waiting;
循環(huán)繼續(xù),conn_waiting 狀態(tài)下的操作只是將 conn 狀態(tài)轉(zhuǎn)換為 conn_read,循環(huán)退出。
此后,如果客戶(hù)端不請(qǐng)求服務(wù),那么主線(xiàn)程和工作線(xiàn)程都會(huì)沉睡,注意這些機(jī)制都是由 libevent 提供的。
客戶(hù)敲擊命令「get key」后,工作線(xiàn)程會(huì)被喚醒,event_handler()被調(diào)用了??? 又被調(diào)用了.event_handler()->drive_machine(),此時(shí) conn 的狀態(tài)為 conn_read。conn_read 下的操作就是讀數(shù)據(jù)了,如果讀取成功,conn 狀態(tài)被轉(zhuǎn)換為 conn_parse_cmd。
循環(huán)繼續(xù),conn_parse_cmd 狀態(tài)下的操作就是嘗試解析命令: 可能是較為簡(jiǎn)單的命令,就直接回復(fù),狀態(tài)轉(zhuǎn)換為 conn_close,循環(huán)接下去就結(jié)束了; 涉及存取操作的請(qǐng)求會(huì)導(dǎo)致 conn_parse_cmd 狀態(tài)轉(zhuǎn)換為 conn_nread。
循環(huán)繼續(xù),conn_nread 狀態(tài)下的操作是真正執(zhí)行存取命令的地方。里面的操作無(wú)非是在內(nèi)存尋找數(shù)據(jù)項(xiàng),返回?cái)?shù)據(jù)。所以接下來(lái)的狀態(tài) conn_mwrite,它的操作是為客戶(hù)端回復(fù)數(shù)據(jù)。
狀態(tài)又回到了 conn_new_cmd 迎接新的請(qǐng)求,直到客戶(hù)命令結(jié)束服務(wù)或者發(fā)生致命錯(cuò)誤。大概就是這么個(gè)過(guò)程。
#p#
memcached 的分布式
memcached 的服務(wù)器沒(méi)有向其他 memcached 服務(wù)器收發(fā)數(shù)據(jù)的功能,意即就算部署多個(gè) memcached 服務(wù)器,他們之間也沒(méi)有任何的通信。memcached 所謂的分布式部署也是并非平時(shí)所說(shuō)的分布式。所說(shuō)的「分布式」是通過(guò)創(chuàng)建多個(gè) memcached 服務(wù)器節(jié)點(diǎn),在客戶(hù)端添加緩存請(qǐng)求分發(fā)器來(lái)實(shí)現(xiàn)的。memcached 的更多的時(shí)候限制是來(lái)自網(wǎng)絡(luò) I/O,所以應(yīng)該盡量減少網(wǎng)絡(luò) I/O。
我在 github 上分享了 memcached 的源碼剖析注釋: 這里
本文題目:memcached源碼閱讀筆記
本文來(lái)源:http://www.5511xx.com/article/cddjhoo.html


咨詢(xún)
建站咨詢(xún)
