memcached 原始碼閱讀筆記

發表於2013-12-23

閱讀 memcached 最好有 libevent 基礎,memcached 是基於 libevent 構建起來的。通由 libevent 提供的事件驅動機制觸發 memcached 中的 IO 事件。

個人認為,閱讀原始碼的起初最忌鑽牛角尖,如標頭檔案裡天花亂墜的結構體到底有什麼用。原始檔裡稀里嘩啦的函式是做什麼的。剛開始並沒必要事無鉅細弄清楚標頭檔案每個型別定義的具體用途;很可能那些是不緊要的工具函式,知道他的功能和用法就沒他事了。

來看 memcached 內部做了什麼事情。memcached 是用 c 語言實現,必須有一個入口函式main(),memcached 的生命從這裡開始。

初始化過程

建立並初始化 main_base,即主執行緒的事件中心,這是 libevent 裡面的概念,可以把它理解為事件分發中心。

建立並初始化 memcached 內部容器資料結構。

建立並初始化空閒連線結構體陣列。

建立並初始化執行緒結構陣列,指定每個執行緒的入口函式是worker_libevent(),並建立工作執行緒。從worder_libevent()的實現來看,工作執行緒都會呼叫event_base_loop()進入自己的事件迴圈。

根據 memcached 配置,開啟以下兩種服務模式中的一種:

以 UNIX 域套接字的方式接受客戶的請求
以 TCP/UDP 套接字的方式接受客戶的請求
memcached 有可配置的兩種模式: UNIX 域套接字和 TCP/UDP,允許客戶端以兩種方式向 memcached 發起請求。客戶端和伺服器在同一個主機上的情況下可以用 UNIX 域套接字,否則可以採用 TCP/UDP 的模式。兩種模式是不相容的。特別的,如果是 UNIX 域套接字或者 TCP 模式,需要建立監聽套接字,並在事件中心註冊了讀事件,回撥函式是event_handler(),我們會看到所有的連線都會被註冊回撥函式是event_handler()。

呼叫event_base_loop()開啟 libevent 的事件迴圈。到此,memcached 伺服器的工作正式進入了工作。如果遇到致命錯誤或者客戶明令結束 memcached,那麼才會進入接下來的清理工作。

 

UNIX 域套接字和 UDP/TCP 工作模式

在初始化過程中介紹了這兩種模式,memcached 這麼做為的是讓其能更加可配置。TCP/UDP 自不用說,UNIX 域套接字有獨特的優勢:

在同一臺主機上進行通訊時,是不同主機間通訊的兩倍
UNIX 域套介面可以在同一臺主機上,不同程式之間傳遞套接字描述符
UNIX 域套接字可以向伺服器提供客戶的憑證(使用者id或者使用者組id)
其他關於 UNIX 域套接字優缺點的請參看: https://pangea.stanford.edu/computing/UNIX/overview/advantages.php

 

工作執行緒管理和執行緒調配方式

在thread_init(),setup_thread()函式的實現中,memcached 的意圖是很清楚的。每個執行緒都有自己獨有的連線佇列,即 CQ,注意這個連線佇列中的物件並不是一個或者多個 memcached 命令,它對應一個客戶! 一旦一個客戶交給了一個執行緒,它的餘生就屬於這個執行緒了! 執行緒只要被喚醒就立即進入工作狀態,將自己 CQ 佇列的任務所有完完成。當然,每一個工作執行緒都有自己的 libevent 事件中心。

很關鍵的線索是thread_init()的實現中,每個工作執行緒都建立了讀寫管道,所能給我們的提示是: 只要利用 libevent 在工作執行緒的事件中心註冊讀管道的讀事件,就可以按需喚醒執行緒,完成工作,很有意思,而setup_thread()的工作正是讀管道的讀事件被註冊到執行緒的事件中心,回撥函式是thread_libevent_process().thread_libevent_process()的工作就是從工作執行緒自己的 CQ 佇列中取出任務執行,而往工作執行緒工作佇列中新增任務的是dispatch_conn_new(),此函式一般由主執行緒呼叫。下面是主執行緒和工作執行緒的工作流程:

120131223103045

前幾天在微博上,看到 @高階小混混 的微博,轉發了:

“多工並行處理的兩種方式,一種是將所有的任務用佇列儲存起來,每個工作者依次去拿一個來處理,直到做完所有的>任務為止。另一種是將任務平均分給工作者,先做完任務的工作者就去別的工作者那裡拿一些任務來做,同樣直到所有任務做完為止。兩種方式的結果如何?根據自己的場景寫碼驗證。”

memcached 所採用的模式就是這裡所說的第二種! memcached 的執行緒分配模式是:一個主執行緒和多個工作執行緒。主執行緒負責初始化和將接收的請求分派給工作執行緒,工作執行緒負責接收客戶的命令請求和回覆客戶。

 

儲存容器

memcached 是做快取用的,內部肯定有一個容器。回到main()中,呼叫assoc_init()初始化了容器–hashtable,採用頭插法插入新資料,因為頭插法是最快的。memcached 只做了一級的索引,即 hash; 接下來的就靠 memcmp() 在連結串列中找資料所在的位置。memcached 容器管理的介面主要在 item.h .c 中.

220131223103240

 

 

連線管理

每個連線都會建立一個連線結構體與之對應。main()中會呼叫conn_init()建立連線結構體陣列。連線結構體 struct conn 記錄了連線套接字,讀取的資料,將要寫入的資料,libevent event 結構體以及所屬的執行緒資訊。

當有新的連線時,主執行緒會被喚醒,主執行緒選定一個工作執行緒 thread0,在 thread0 的寫管道中寫入資料,特別的如果是接受新的連線而不是接受新的資料,寫入管道的資料是字元 ‘c’。工作執行緒因管道中有資料可讀被喚醒,thread_libevent_process()被呼叫,新連線套接字被註冊了event_handler()回撥函式,這些工作在conn_new()中完成。因此,客戶端有命令請求的時候(譬如發起 get key 命令),工作執行緒都會被觸發呼叫event_handler()。

當出現致命錯誤或者客戶命令結束服務(quit 命令),關於此連線的結構體內部的資料會被釋放(譬如曾經讀取的資料),但結構體本身不釋放,等待下一次使用。如果有需要,連線結構體陣列會指數自增。

 

一個請求的工作流程

memcached 服務一個客戶的時候,是怎麼一個過程,試著去除錯模擬一下。當一個客戶向 memcached 發起請求時,主執行緒會被喚醒,接受請求。接下來的工作在連線管理中有說到。

客戶已經與 memcached 伺服器建立了連線,客戶在終端(黑框框)敲擊 get key + Enter鍵,一個請求包就發出去了。從連線管理中已經瞭解到所有連線套接字都會被註冊回撥函式為event_handler(),因此event_handler()會被觸發呼叫。

event_handler()呼叫了drive_machine().drive_machine()是請求處理的開端,特別的當有新的連線時,listen socket 也是有請求的,所以建立新的連線也會呼叫drive_machine(),這在連線管理有提到過。下面是drive_machine()函式的骨架:

通過修改連線結構體狀態 struct conn.state 執行相應的操作,從而完成一個請求,完成後 stop 會被設定為 true,一個命令只有執行結束(無論結果如何)才會跳出這個迴圈。我們看到 struct conn 有好多種狀態,一個正常執行的命令狀態的轉換是:

這個過程任何一個環節出了問題都會導致狀態轉變為 conn_close。帶著剛開始的問題把從客戶連線到一個命令執行結束的過程是怎麼樣的:

客戶connect()後,memcached 伺服器主執行緒被喚醒,接下來的呼叫鏈是event_handler()->drive_machine()被呼叫,此時主執行緒對應 conn 狀態為 conn_listining,接受請求

dispatch_conn_new()的工作是往工作執行緒工作佇列中新增任務(前面已經提到過),所以其中一個沉睡的工作執行緒會被喚醒,thread_libevent_process()會被工作執行緒呼叫,注意這些機制都是由 libevent 提供的。

thread_libevent_process()呼叫conn_new()新建 struct conn 結構體,且狀態為 conn_new_cmd,其對應的就是剛才accept()的連線套接字.conn_new()最關鍵的任務是將剛才接受的套接字在 libevent 中註冊一個事件,回撥函式是event_handler()。迴圈繼續,狀態 conn_new_cmd 下的操作只是只是將 conn 的狀態轉換為 conn_waiting;

迴圈繼續,conn_waiting 狀態下的操作只是將 conn 狀態轉換為 conn_read,迴圈退出。

此後,如果客戶端不請求服務,那麼主執行緒和工作執行緒都會沉睡,注意這些機制都是由 libevent 提供的。

客戶敲擊命令「get key」後,工作執行緒會被喚醒,event_handler()被呼叫了。看! 又被呼叫了.event_handler()->drive_machine(),此時 conn 的狀態為 conn_read。conn_read 下的操作就是讀資料了,如果讀取成功,conn 狀態被轉換為 conn_parse_cmd。

迴圈繼續,conn_parse_cmd 狀態下的操作就是嘗試解析命令: 可能是較為簡單的命令,就直接回復,狀態轉換為 conn_close,迴圈接下去就結束了; 涉及存取操作的請求會導致 conn_parse_cmd 狀態轉換為 conn_nread。

迴圈繼續,conn_nread 狀態下的操作是真正執行存取命令的地方。裡面的操作無非是在記憶體尋找資料項,返回資料。所以接下來的狀態 conn_mwrite,它的操作是為客戶端回覆資料。

狀態又回到了 conn_new_cmd 迎接新的請求,直到客戶命令結束服務或者發生致命錯誤。大概就是這麼個過程。

 

memcached 的分散式

memcached 的伺服器沒有向其他 memcached 伺服器收發資料的功能,意即就算部署多個 memcached 伺服器,他們之間也沒有任何的通訊。memcached 所謂的分散式部署也是並非平時所說的分散式。所說的「分散式」是通過建立多個 memcached 伺服器節點,在客戶端新增快取請求分發器來實現的。memcached 的更多的時候限制是來自網路 I/O,所以應該儘量減少網路 I/O。

320131223103255

我在 github 上分享了 memcached 的原始碼剖析註釋: 這裡

相關文章