自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

小紅書抗住高并發(fā)的背后:Redis 7.0 性能必殺技之 I/O 多線程模型

開發(fā) 前端
Redis 的多線程網(wǎng)絡(luò)模型實際上并不是一個標準的 Multi-Reactors/Master-Workers 模型,I/O 線程任務(wù)僅僅是通過 socket 讀取客戶端請求命令并解析,以及把指令執(zhí)行結(jié)果回寫給 socket ,沒有真正去執(zhí)行命令。

今天,咱們就詳細的聊下 I/O 多線程模型帶來的效果到底是黛玉騎鬼火,該強強,該弱弱;還是猶如光明頂身懷絕技的的張無忌,招招都是必殺技。

單線程模型真的只有一個線程么?

謝霸哥:“碼哥, Redis 6.0 之前單線程指的是 Redis 只有一個線程干活么?”

非也,我們通常說的單線程模型指的是 Redis 在處理客戶端的請求時,包括獲取 (socket 讀)、解析、執(zhí)行、內(nèi)容返回 (socket 寫) 等都由一個順序串行的主線程處理。

而其他的清理過期鍵值對數(shù)據(jù)、釋放無用連接、內(nèi)存淘汰策略執(zhí)行、BGSAVE 生成 RDB 內(nèi)存快照文件、AOF rewrite 等都是其他線程處理。

命令執(zhí)行階段,每一條命令并不會立馬被執(zhí)行,而是進入一個一個 socket 隊列,當(dāng) socket 事件就緒則交給事件分發(fā)器分發(fā)到對應(yīng)的事件處理器處理,單線程模型的命令處理如下圖所示。

圖片圖片

線程模型的演化

謝霸哥:“為什么 Redis6.0 之前是單線程模型?”

以下是官方關(guān)于為什么 6.0 之前一直使用單線程模型的回答。

  • Redis 的性能瓶頸主要在于內(nèi)存和網(wǎng)絡(luò) I/O,CPU 不會是性能瓶頸所在。
  • Redis 通過使用 pipelining 每秒可以處理 100 萬個請求,應(yīng)用程序的所時候用的大多數(shù)命令時間復(fù)雜度主要使用 O(N) 或 O(log(N)) 的,它幾乎不會占用太多 CPU。
  • 單線程模型的代碼可維護性高。多線程模型雖然在某些方面表現(xiàn)優(yōu)異,但是它卻引入了程序執(zhí)行順序的不確定性,帶來了并發(fā)讀寫的一系列問題,增加了系統(tǒng)復(fù)雜度、同時可能存在線程切換、甚至加鎖解鎖、死鎖造成的性能損耗。

Redis 通過基于 I/O 多路復(fù)用實現(xiàn)的 AE 事件驅(qū)動框架將 I/O 事件和事件事件融合在一起,實現(xiàn)高性能網(wǎng)絡(luò)處理能力,再加上基于內(nèi)存的數(shù)據(jù)處理,沒有引入多線程的必要。

而且單線程機制讓 Redis 內(nèi)部實現(xiàn)的復(fù)雜度大大降低,Hash 的惰性 Rehash、Lpush 等等線程不安全的命令都可以無鎖進行。

謝霸哥:“既然單線程這么好,為什么 6.0 版本引入多線程模型?”

因為隨著底層網(wǎng)絡(luò)硬件性能提升,Redis 的性能瓶頸逐漸體現(xiàn)在網(wǎng)絡(luò) I/O 的讀寫上,單個線程處理網(wǎng)絡(luò)讀寫的速度跟不上底層網(wǎng)絡(luò)硬件執(zhí)行的速度。

因為讀寫網(wǎng)絡(luò)的 read/write 系統(tǒng)調(diào)用占用了 Redis 執(zhí)行期間大部分 CPU 時間。所以 Redis 采用多個 I/O 線程來處理網(wǎng)絡(luò)請求,提高網(wǎng)絡(luò)請求處理的并行度。

需要注意的是,Redis 多 IO 線程模型只用來處理網(wǎng)絡(luò)讀寫請求,對于 Redis 的讀寫命令,依然是單線程處理。

這是因為,網(wǎng)絡(luò) I/O 讀寫是瓶頸,可通過多線程并行處理可提高性能。而繼續(xù)使用單線程執(zhí)行讀寫命令,不需要為了保證 Lua 腳本、事務(wù)、等開發(fā)多線程安全機制,實現(xiàn)更簡單。

謝霸哥:“碼哥,你真是斑馬的腦袋,說的頭頭是道?!?/p>

我謝謝您嘞,主線程與 I/O 多線程共同協(xié)作處理命令的架構(gòu)圖如下所示。

圖片圖片

I/O 多線程模型解讀

謝霸哥:“如何開啟多線程呢?”

Redis 6.0 的多線程默認是禁用的,如需開啟需要修改 redis.conf 配置文件的配置io-threads-do-reads yes。

開啟多線程后,還要設(shè)置線程數(shù)才能生效,同樣是修改 redis.conf配置文件。

io-threads 4

謝霸哥:“碼老師,線程數(shù)是不是越多越好?”

當(dāng)然不是,關(guān)于線程數(shù)的設(shè)置,官方有一個建議:線程數(shù)的數(shù)量最好小于 CPU 核心數(shù),起碼預(yù)留一個空閑核處理,因為 Redis 是主線程處理指令,如果系統(tǒng)出現(xiàn)頻繁上下文切換,效率會降低。

比如 4 核的機器建議設(shè)置為 2 或 3 個線程,8 核的機器建議設(shè)置為 6 個線程,線程數(shù)一定要小于機器核數(shù)。

謝霸哥:“碼老師真厲害,就好像賣盆的進村一套一套的。我什么時候也能像你這樣連貫又有邏輯的掌握 Redis?!?/p>

認真讀 Redis 高手心法,長線放風(fēng)箏慢慢來。

謝霸哥:“主線程與 I/O 線程是如何實現(xiàn)協(xié)作呢?”

圖片圖片

主要流程。

  1. 主線程負責(zé)接收建立連接請求,通過輪詢將可讀 socket 分配給 I/O 線程綁定的等待隊列。
  2. 主線程阻塞等待,直到 I/O 線程完成 socket 讀取和解析。
  3. 主線程執(zhí)行 I/O 線程讀取和解析出來的 Redis 請求命令。
  4. 主線程阻塞等待 I/O 線程將指令執(zhí)行結(jié)果回寫回 socket完畢。
  5. 主線程清空等待隊列,等待下一次客戶端后續(xù)的請求。

思路:將主線程 IO 讀寫任務(wù)拆分出來給一組獨立的線程處理,使得多個 socket 讀寫可以并行化,但是 Redis 命令還是主線程串行執(zhí)行。

大家注意第三和第五步,主線程并不是掛起線程讓出 CPU 分片時間。而是通過 for 循環(huán)進行忙等,不斷的檢測所有 I/O 線程處理任務(wù)是否已經(jīng)完成,完成再執(zhí)行下一步。

源碼解析

看完流程圖以及主要步驟,接著跟著源碼走一個。通過 4.3 章節(jié)的學(xué)習(xí),我們知道 Redis 是通過 server.c的main函數(shù)啟動的,經(jīng)過一系列的初始化操作后,調(diào)用 aeMain(server.el);啟動事件驅(qū)動框架,也就是整個 Redis 的核心。

初始化線程

I/O 多線程模型的開端也是由 server.c的main方法中的 InitServerLast來初始化,該方法內(nèi)部會調(diào)用 networking.c 的 initThreadedIO來執(zhí)行實際 I/O 線程初始化工作。

/* networking.c */
void initThreadedIO(void) {
   // 設(shè)置成 0 表示激活 I/O 多線程模型
    server.io_threads_active = 0;
    /* I/O 線程處于空閑狀態(tài) */
    io_threads_op = IO_THREADS_OP_IDLE;

    /* 如果 redis.conf 的 io-threads 配置為 1 表示使用單線程模型,直接退出 */
    if (server.io_threads_num == 1) return;

    // 線程數(shù)超過最大值 128,退出程序
    if (server.io_threads_num > IO_THREADS_MAX_NUM) {
        ....省略
        exit(1);
    }

    for (int i = 0; i < server.io_threads_num; i++) {
        /*  io_threads_list 鏈表,用于存儲該線程要執(zhí)行的 I/O 操作。*/
        io_threads_list[i] = listCreate();
        // 0 號線程不創(chuàng)建,0 號就是主線程,主線程也會處理任務(wù)邏輯。
        if (i == 0) continue;

        // 創(chuàng)建線程,主線程先對子線程上鎖,掛起子線程,不讓其進入工作模式
        pthread_t tid;
        pthread_mutex_init(&io_threads_mutex[i],NULL);
        setIOPendingCount(i, 0);
        // 掛起子線程,先不進入工作模式,等待主線程發(fā)出干活信號再執(zhí)行任務(wù)。
        pthread_mutex_lock(&io_threads_mutex[i]);
        // 創(chuàng)建線程,指定I/O線程的入口函數(shù) IOThreadMain
        if (pthread_create(&tid,NULL,IOThreadMain,(void*)(long)i) != 0) {
            serverLog(LL_WARNING,"Fatal: Can't initialize IO thread.");
            exit(1);
        }
        // I/O 線程數(shù)組
        io_threads[i] = tid;
    }
}
  1. 檢查是否開啟 I/O 多線程模型:默認不激活 I/O 多線程模型,當(dāng) redis.conf 的 io-threads 配置大于 1 并且小于 IO_THREADS_MAX_NUM(128) 則表示開啟 I/O 多線程模式。
  2. 創(chuàng)建 io_threads_list 鏈表,用于保存每個線程需要處理的 I/O 任務(wù)。
  3. 創(chuàng)建子線程,創(chuàng)建的時候先上鎖,掛起子線程不讓其進入工作模式,等初始化工作完成再開啟。
  4. 指定 I/O 線程的入口函數(shù) IOThreadMain,I/O 線程開始工作。

I/O 線程核心函數(shù)

IOThreadMain 函數(shù)主要負責(zé)等待啟動信號、執(zhí)行特定的 I/O 操作,并在完成操作后重置線程狀態(tài),以便再次等待下一次啟動信號。

void *IOThreadMain(void *myid) {
    /* 每個線程創(chuàng)建一個 id */
    long id = (unsigned long)myid;
    char thdname[16];
    .....
    // 進入無限循環(huán),等待主線程發(fā)出干活信號
    while(1) {
        /* 沒有使用 sleep 設(shè)置等待時間實現(xiàn)忙等,而是循環(huán),耗費 CPU*/
        for (int j = 0; j < 1000000; j++) {
            // 等待待處理的 I/O 操作出現(xiàn),也就是讀寫客戶端數(shù)據(jù)
            if (getIOPendingCount(id) != 0) break;
        }

        /*留機會給主線程上鎖,掛起當(dāng)前子線程 */
        if (getIOPendingCount(id) == 0) {
            pthread_mutex_lock(&io_threads_mutex[id]);
            pthread_mutex_unlock(&io_threads_mutex[id]);
            continue;
        }

        serverAssert(getIOPendingCount(id) != 0);

        /* 根據(jù)線程 id 以及待分配列表進行任務(wù)分配 */
        listIter li;
        listNode *ln;
        listRewind(io_threads_list[id],&li);
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            if (io_threads_op == IO_THREADS_OP_WRITE) {
                // 將可寫客戶端任務(wù)分配
                writeToClient(c,0);
            } else if (io_threads_op == IO_THREADS_OP_READ) {
                // 讀取客戶端 socket 數(shù)據(jù)
                readQueryFromClient(c->conn);
            } else {
                serverPanic("io_threads_op value is unknown");
            }
        }

        listEmpty(io_threads_list[id]);
        setIOPendingCount(id, 0);
    }
}

待讀取客戶端任務(wù)分配

Redis 會在主線程 initServer 初始化服務(wù)器的時候會注冊 beforeSleep函數(shù),里面會調(diào)用 handleClientsWithPendingReadsUsingThreads函數(shù)實現(xiàn)待處理任務(wù)分配邏輯。該函數(shù)的主要作用如下。

  • 將所有待讀的客戶端平均分配到不同的 I/O 線程的列表中。
  • 通過設(shè)置 io_threads_op 和調(diào)用 setIOPendingCount 函數(shù),通知各個 I/O 線程開始處理可讀取的客戶端數(shù)據(jù)。
  • 主線程也參與處理客戶端讀取,以確保更好的并發(fā)性能。
  • 主線程等待所有 I/O 線程完成讀取 socket 工作。

這種設(shè)計采用了“扇出 -> 扇入”的范式,通過將工作分發(fā)到多個 I/O 線程,再將結(jié)果合并回主線程,以提高并發(fā)性能。

int handleClientsWithPendingReadsUsingThreads(void) {
    ......

    /* 將所有待處理的客戶端平均分配到不同的 I/O 線程的列表中*/
    listIter li;
    listNode *ln;
    listRewind(server.clients_pending_read,&li);
    int item_id = 0;
    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++;
    }

    /* 通過設(shè)置 `io_threads_op` 和調(diào)用 `setIOPendingCount` 函數(shù),通知各個 I/O 線程開始處理可讀取的客戶端數(shù)據(jù)。 */
    io_threads_op = IO_THREADS_OP_READ;
    for (int j = 1; j < server.io_threads_num; j++) {
        int count = listLength(io_threads_list[j]);
        setIOPendingCount(j, count);
    }

    /* 主線程處理第一個等待隊列任務(wù) */
    listRewind(io_threads_list[0],&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        readQueryFromClient(c->conn);
    }
    listEmpty(io_threads_list[0]);

    /* 主線程處理完任務(wù)后,忙等等待所有 I/O 線程完成讀取 socket 工作 */
    while(1) {
        unsigned long pending = 0;
        for (int j = 1; j < server.io_threads_num; j++)
            pending += getIOPendingCount(j);
        if (pending == 0) break;
    }

    ......

    return processed;
}

待寫回客戶端任務(wù)分配

與上面類似, beforeSleep函數(shù)里面會調(diào)用 handleClientsWithPendingWritesUsingThreads函數(shù)實現(xiàn)可寫客戶端處理任務(wù)分配給 I/O 線程,源代碼跟 handleClientsWithPendingReadsUsingThreads類似,不貼了。差別就是這個函數(shù)處理的事情是把響應(yīng)寫回 socket。

  • 將所有待寫的客戶端平均分配到不同的 I/O 線程的列表中。
  • 設(shè)置 io_threads_op 為 IO_THREADS_OP_READ通知各個 I/O 線程開始處理可寫的客戶端數(shù)據(jù)。
  • 主線程也參與處理客戶端讀取,以確保更好的并發(fā)性能。
  • 主線程等待所有 I/O 線程完成讀取 socket 工作。

模型缺陷

Redis 的多線程網(wǎng)絡(luò)模型實際上并不是一個標準的 Multi-Reactors/Master-Workers 模型,I/O 線程任務(wù)僅僅是通過 socket 讀取客戶端請求命令并解析,以及把指令執(zhí)行結(jié)果回寫給 socket ,沒有真正去執(zhí)行命令。

所有客戶端命令最后還需要回到主線程去執(zhí)行,因此對多核的利用率并不算高,而且每次主線程都必須在分配完任務(wù)之后忙輪詢等待所有 I/O 線程完成任務(wù)之后才能繼續(xù)執(zhí)行其他邏輯。

在我看來,Redis 目前的多線程方案更像是一個折中的選擇,只是黛玉騎鬼火,還未達到必殺技的階段。

責(zé)任編輯:武曉燕 來源: 碼哥跳動
相關(guān)推薦

2024-08-09 12:11:07

2023-04-13 08:00:45

Redis底層性能

2021-02-02 10:55:09

等級保護2.0信息安全網(wǎng)絡(luò)安全

2018-09-21 14:32:00

iPaas云應(yīng)用部署

2011-06-24 17:23:30

網(wǎng)站優(yōu)化

2013-05-10 09:23:14

iPaaS混合云集成云集成

2010-08-24 14:57:33

外企職場

2011-06-27 14:56:49

SEO

2009-10-13 16:38:04

強行關(guān)閉VMware虛

2009-07-22 15:02:18

2010-08-11 16:43:05

職場

2023-04-07 17:44:43

2009-01-03 09:14:00

2017-03-13 15:39:09

Windows 10進程必殺技

2024-02-02 11:24:00

I/O高并發(fā)場景

2009-09-28 11:16:23

UPS電源

2013-12-18 11:34:17

云文件共享服務(wù)云文件同步服務(wù)BYOD

2019-11-12 09:32:35

高并發(fā)流量協(xié)議

2022-08-04 20:41:42

高并發(fā)流量SQL

2011-06-29 17:41:56

SEO
點贊
收藏

51CTO技術(shù)棧公眾號