切換語言為:簡體
Redis 6.0 以後為什麼使用了多執行緒?

Redis 6.0 以後為什麼使用了多執行緒?

  • 爱糖宝
  • 2024-11-18
  • 2026
  • 0
  • 0

Redis 所謂的單執行緒並不是所有工作都是隻有一個執行緒在執行,而是指 Redis 的網路 IO 和鍵值對讀寫是由一個執行緒來完成的,Redis 在處理客戶端的請求時包括獲取 (socket 讀)、解析、執行、內容返回 (socket 寫) 等都由一個順序序列的主執行緒處理。

這就是所謂的“單執行緒”。這也是 Redis 對外提供鍵值儲存服務的主要流程。
由於 Redis 在處理命令的時候是單執行緒作業的,所以會有一個 Socket 佇列,每一個到達的服務端命令來了之後都不會馬上被執行,而是進入佇列,然後被執行緒的事件分發器逐個執行。如下圖:

Redis 6.0 以後為什麼使用了多執行緒?

至於 Redis 的其他功能, 比如持久化、非同步刪除、叢集資料同步等等,其實是由額外的執行緒執行的。 可以這麼說,Redis 工作執行緒是單執行緒的。但是在 4.0 之後,對於整個 Redis 服務來說,還是多執行緒運作的。

6.0 之前為什麼要使用單執行緒

Redis 6.0 以後為什麼使用了多執行緒?

  • 在使用 Redis 時,Redis 主要受限是在記憶體和網路上,CPU 幾乎沒有效能瓶頸的問題。

  • 以 Linux 系統為例子,在 Linux 系統上 Redis 透過 pipelining 可以處理 100w 個請求每秒,而應用程式的計算複雜度主要是 O(N) 或 O(log(N)) ,不會消耗太多 CPU。

  • 使用了單執行緒後,提高了可維護性。多執行緒模型在某些方面表現優異,卻增加了程式執行順序的不確定性,並且帶來了併發讀寫的一系列問題,增加了系統複雜度。同時因為執行緒切換、加解鎖,甚至死鎖,造成一定的效能損耗。

  • Redis 透過 AE 事件模型以及 IO 多路複用等技術,擁有超高的處理效能,因此沒有使用多執行緒的必要

6.0 之後的多執行緒主要解決什麼問題

近年來底層網路硬體效能越來越好,Redis 的效能瓶頸逐漸體現在網路 I/O 的讀寫上,單個執行緒處理網路 I/O 讀寫的速度跟不上底層網路硬體執行的速度。

Redis 在處理網路資料時,呼叫 epoll 的過程是阻塞的,這個過程會阻塞執行緒。如果併發量很高,達到萬級別的 QPS,就會形成瓶頸,影響整體吞吐能力

Redis 6.0 以後為什麼使用了多執行緒?

既然讀寫網路的 read/write 系統呼叫佔用了 Redis 執行期間大部分 CPU 時間,那麼要想真正做到提速,必須改善網路 IO 效能。我們可以從這兩個方面來最佳化:

  • 提高網路 IO 效能,典型實現方式比如使用 DPDK 來替代核心網路棧的方式

  • 使用多執行緒,這樣可以充分利用多核 CPU,同類實現案例比如 Memcached。

協議棧最佳化的這種方式跟 Redis 關係不大,所以最便捷高效的方式就是支援多執行緒。總結起來,redis 支援多執行緒就是以下兩個原因:

  • 可以充分利用伺服器 CPU 的多核資源,而主執行緒明顯只能利用一個

  • 多執行緒任務可以分攤 Redis 同步 IO 讀寫負荷,降低耗時

6.0 版本最佳化之後,主執行緒和多執行緒網路 IO 的執行流程如下:

Redis 6.0 以後為什麼使用了多執行緒?

具體步驟如下:

  • 主執行緒建立連線,並接受資料,並將獲取的 socket 資料放入等待佇列;

  • 透過輪詢的方式將 socket 讀取出來並分配給 IO 執行緒;

  • 之後主執行緒保持阻塞,一直等到 IO 執行緒完成 socket 讀取和解析;

  • I/O 執行緒讀取和解析完成之後,返回給主執行緒 ,主執行緒開始執行 Redis 命令;

  • 執行完 Redis 命令後,主執行緒阻塞,直到 IO 執行緒完成 結果回寫到 socket 的工作;

  • 主執行緒清空已完成的佇列,等待客戶端新的請求。

本質上是將主執行緒 IO 讀寫的這個操作 獨立出來,單獨交給一個 I/O 執行緒組處理。
這樣多個 socket 讀寫可以並行執行,整體效率也就提高了。同時注意 Redis 命令還是主執行緒序列執行。

利用多核來分擔 I/O 讀寫負荷。在事件處理執行緒每次獲取到可讀事件時,會將所有就緒的讀事件分配給 I/O 執行緒,並進行等待,在所有 I/O 執行緒完成讀操作後,事件處理執行緒開始執行任務處理,在處理結束後,同樣將寫事件分配給 I/O 執行緒,等待所有 I/O 執行緒完成寫操作。

int handleClientsWithPendingReadsUsingThreads(void) {
    ...
    /* Distribute the clients across N different lists. */
    listIter li;
    listNode *ln;
    listRewind(server.clients_pending_read,&li);
    int item_id = 0;
    // 將等待處理的客戶端分配給 I/O 執行緒
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        int target_id = item_id % server.io_threads_num;
        listAddNodeTail(io_threads_list[target_id],c);
        item_id++;
    }
    ...
    /* Wait for all the other threads to end their work. */
    // 輪訓等待所有 I/O 執行緒處理完
    while(1) {
        unsigned long pending = 0;
        for (int j = 1; j < server.io_threads_num; j++)
            pending += io_threads_pending[j];
        if (pending == 0) break;
    }
    ...
    return processed;
}

本質上是利用多核的多執行緒讓多個 IO 的讀寫加速。

侷限性

6.0 版本的多執行緒並非徹底的多執行緒,I/O 執行緒只能同時執行讀或者同時執行寫操作,期間事件處理執行緒一直處於等待狀態,並非流水線模型,有很多輪訓等待開銷。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.