memcached 源碼閱讀筆記

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

我的認爲, 閱讀源碼的起初最忌鑽牛角尖, 如頭文件裏天花亂墜的結構體到底有什麼用. 源文件裏稀里嘩啦的函數是作什麼的. 剛開始並不必事無鉅細弄清楚頭文件每一個類型定義的具體用途; 極可能那些是不緊要的工具函數, 知道他的功能和用法就沒他事了.git

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

初始化過程

創建並初始化 main_base, 即主線程的事件中心, 這是 libevent 裏面的概念, 能夠把它理解爲事件分發中心.數組

創建並初始化 memcached 內部容器數據結構.緩存

創建並初始化空閒鏈接結構體數組.服務器

創建並初始化線程結構數組, 指定每一個線程的入口函數是worker_libevent(), 並建立工做線程. 從worder_libevent()的實現來看, 工做線程都會調用event_base_loop()進入本身的事件循環.網絡

根據 memcached 配置, 開啓如下兩種服務模式中的一種:數據結構

  1. 以 UNIX 域套接字的方式接受客戶的請求
  2. 以 TCP/UDP 套接字的方式接受客戶的請求

memcached 有可配置的兩種模式: UNIX 域套接字和 TCP/UDP, 容許客戶端以兩種方式向 memcached 發起請求. 客戶端和服務器在同一個主機上的狀況下能夠用 UNIX 域套接字, 不然能夠採用 TCP/UDP 的模式. 兩種模式是不兼容的. 特別的, 若是是 UNIX 域套接字或者 TCP 模式, 須要創建監聽套接字, 並在事件中心註冊了讀事件, 回調函數是event_handler(), 咱們會看到全部的鏈接都會被註冊回調函數是event_handler().socket

調用event_base_loop()開啓 libevent 的事件循環. 到此, memcached 服務器的工做正式進入了工做. 若是遇到致命錯誤或者客戶明令結束 memcached, 那麼纔會進入接下來的清理工做.tcp

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

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

  1. 在同一臺主機上進行通訊時,是不一樣主機間通訊的兩倍
  2. UNIX 域套接口能夠在同一臺主機上,不一樣進程之間傳遞套接字描述符
  3. 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(), 此函數通常由主線程調用. 下面是主線程和工做線程的工做流程:

how_threads_work

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

@高端小混混

多任務並行處理的兩種方式,一種是將全部的任務用隊列存儲起來,每一個工做者依次去拿一個來處理,直到作完全部的>任務爲止。另外一種是將任務平均分給工做者,先作完任務的工做者就去別的工做者那裏拿一些任務來作,一樣直到全部任務作完爲止。兩種方式的結果如何?根據本身的場景寫碼驗證。

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

存儲容器

memcached 是作緩存用的, 內部確定有一個容器. 回到main()中, 調用assoc_init()初始化了容器--hashtable, 採用頭插法插入新數據, 由於頭插法是最快的. memcached 只作了一級的索引, 即 hash; 接下來的就靠 memcmp() 在鏈表中找數據所在的位置. memcached 容器管理的接口主要在 item.h .c 中.

hashtable

鏈接管理

每一個鏈接都會創建一個鏈接結構體與之對應. 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 + 回車鍵, 一個請求包就發出去了. 從鏈接管理中已經瞭解到全部鏈接套接字都會被註冊回調函數爲event_handler(), 所以event_handler()會被觸發調用.

<code>void event_handler(const int fd, const short which, void *arg) {
    conn *c;

    c = (conn *)arg;
    assert(c != NULL);

    c-&gt;which = which;

    /* sanity */
    if (fd != c-&gt;sfd) {
        if (settings.verbose &gt; 0)
            fprintf(stderr, "Catastrophic: event fd doesn't match conn fd!\n");
        conn_close(c);
        return;
    }

    drive_machine(c);

    /* wait for next event */
    return;
}
</code>

event_handler()調用了drive_machine().drive_machine()是請求處理的開端, 特別的當有新的鏈接時, listen socket 也是有請求的, 因此創建新的鏈接也會調用drive_machine(), 這在鏈接管理有提到過. 下面是drive_machine()函數的骨架:

<code>// 請求的開端. 當有新的鏈接的時候 event_handler() 會調用此函數.
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 能保證一個命令被執行完成或者異常中斷(譬如 IO 操做次數超出了必定的限制)

        switch(c-&gt;state) {
        // 正在鏈接, 尚未 accept
        case conn_listening:

        // 等待新的命令請求
        case conn_waiting:

        // 讀取數據
        case conn_read:

        // 嘗試解析命令
        case conn_parse_cmd :

        // 新的命令請求, 只是負責轉變 conn 的狀態
        case conn_new_cmd:

        // 真正執行命令的地方
        case conn_nread:

        // 讀取全部的數據, 拋棄!!! 通常出錯的狀況下會轉換到此狀態
        case conn_swallow:

        // 數據回覆
        case conn_write:

        case conn_mwrite:

        // 鏈接結束. 通常出錯或者客戶顯示結束服務的狀況下回轉換到此狀態
        case conn_closing:
        }
    }
    return;
}
</code>

經過修改鏈接結構體狀態 struct conn.state 執行相應的操做, 從而完成一個請求, 完成後 stop 會被設置爲 true, 一個命令只有執行結束(不管結果如何)纔會跳出這個循環. 咱們看到 struct conn 有好多種狀態, 一個正常執行的命令狀態的轉換是:

<code> conn_new_cmd-&gt;conn_waiting-&gt;conn_read-&gt;conn_parse_cmd-&gt;conn_nread-&gt;conn_mwrite-&gt;conn_close
</code>

這個過程任何一個環節出了問題都會致使狀態轉變爲 conn_close. 帶着剛開始的問題把從客戶鏈接到一個命令執行結束的過程是怎麼樣的:

  1. 客戶connect()後, memcached 服務器主線程被喚醒, 接下來的調用鏈是event_handler()->drive_machine()被調用,此時主線程對應 conn 狀態爲 conn_listining,接受請求

    dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST,DATA_BUFFER_SIZE, tcp_transport);

  2. dispatch_conn_new()的工做是往工做線程工做隊列中添加任務(前面已經提到過), 因此其中一個沉睡的工做線程會被喚醒,thread_libevent_process()會被工做線程調用, 注意這些機制都是由 libevent 提供的.
  3. thread_libevent_process()調用conn_new()新建 struct conn 結構體, 且狀態爲 conn_new_cmd, 其對應的就是剛纔accept()的鏈接套接字.conn_new()最關鍵的任務是將剛纔接受的套接字在 libevent 中註冊一個事件, 回調函數是event_handler(). 循環繼續, 狀態 conn_new_cmd 下的操做只是只是將 conn 的狀態轉換爲 conn_waiting;
  4. 循環繼續, conn_waiting 狀態下的操做只是將 conn 狀態轉換爲 conn_read, 循環退出.
  5. 此後, 若是客戶端不請求服務, 那麼主線程和工做線程都會沉睡, 注意這些機制都是由 libevent 提供的.
  6. 客戶敲擊命令「get key」後, 工做線程會被喚醒,event_handler()被調用了. 看! 又被調用了.event_handler()->drive_machine()此時 conn 的狀態爲 conn_read. conn_read 下的操做就是讀數據了, 若是讀取成功, conn 狀態被轉換爲 conn_parse_cmd.
  7. 循環繼續, conn_parse_cmd 狀態下的操做就是嘗試解析命令: 多是較爲簡單的命令, 就直接回復, 狀態轉換爲 conn_close, 循環接下去就結束了; 涉及存取操做的請求會致使 conn_parse_cmd 狀態轉換爲 conn_nread.
  8. 循環繼續, conn_nread 狀態下的操做是真正執行存取命令的地方. 裏面的操做無非是在內存尋找數據項, 返回數據. 因此接下來的狀態 conn_mwrite, 它的操做是爲客戶端回覆數據.
  9. 狀態又回到了 conn_new_cmd 迎接新的請求, 直到客戶命令結束服務或者發生致命錯誤. 大概就是這麼個過程.

memcached 的分佈式

memcached 的服務器沒有向其餘 memcached 服務器收發數據的功能, 意即就算部署多個 memcached 服務器, 他們之間也沒有任何的通訊. memcached 所謂的分佈式部署也是並不是平時所說的分佈式. 所說的「分佈式」是經過建立多個 memcached 服務器節點, 在客戶端添加緩存請求分發器來實現的. memcached 的更多的時候限制是來自網絡 I/O, 因此應該儘可能減小網絡 I/O.

distributed-memcached

我在 github 上分享了 memcached 的源碼剖析註釋: 這裏

歡迎討論: @鄭思願daoluan

相關文章
相關標籤/搜索