redis6.0多線程源碼剖析

1、概述

redis6.0 新增了 多線程機制。一直存在兩個疑問:

 

1)、 爲什麼要加入多線程

2)、多線程機制,爲什麼不設計成跟memcache 一樣

 

2、解析

 

首先第一個問題,redis 區別於memcache 的一點是,redis是單線程。但是性能也很好,主要得益於他的 IO多路複用,還有單線程省了多線程切換上下文的開銷。線程上下文切換,可能會造成CPU CACHE MISS, CPU的一級緩存和三級緩存的 速度不是一個量級的。這次redis6.0 開啓了IO多線程的機制,主要是考慮,redis的讀寫網絡的read/write系統調用,在redis執行期間佔用了大量的CPU時間。如果把網絡IO做成多線程會提升很大性能。另一方面,如果是比較耗時的操作(比如lrange,del 大key),避免阻塞主線程太多時間,開線線程異步處理 不失爲一個好的選擇。

 

第二個問題,redis選擇多線程模型的時候,爲什麼不選擇跟memcache一樣的?redis的作者Antirez 曾經說過:

 

Redis 支持多線程有 2 種可行的方式:

第一種就是像“Memcached”那樣,一個 Redis 實例開啓多個線程,從而提升GET/SET等簡單命令中每秒可以執行的操作。這涉及到 I/O、命令解析等多線程處理,因此,我們將其稱之爲“I/O threading”。

另一種就是允許在不同的線程中執行較耗時較慢的命令,以確保其它客戶端不被阻塞,我們將這種線程模型稱爲“Slow commands threading”。

經過深思熟慮,Redis 不會採用“I/O threading”,Redis 在運行時主要受制於網絡和內存,所以提升 Redis 性能主要是通過在多個 Redis 實例,特別是 Redis 集羣。

接下來我們主要會考慮改進兩個方面

  • Redis 集羣的多個實例通過編排能夠合理地使用本地實例的磁盤,避免同時重寫 AOF。

  • 提供一個 Redis 集羣代理,便於用戶在沒有較好的集羣協議客戶端時抽象出一個集羣。

補充說明一下,Redis 和 Memcached 一樣是一個內存系統,但不同於 Memcached。

多線程是複雜的,必須考慮使用簡單的數據模型,執行 LPUSH 的線程需要服務其他執行 LPOP 的線程。

我真正期望的實際是“slow operations threading”,在 Redis 6 或 Redis 7 中,將提供“key-level locking”,使得線程可以完全獲得對鍵的控制以處理緩慢的操作

 

簡單的說就是,redis目前性能還是受限於網絡和內存。所以採用多線程來處理網絡IO讀寫和解析,執行命令依然是交給主線程來做。多線程的併發控制又是複雜的(比如事務,LPUSH/LPOP的併發控制),爲了保持redis簡單的設計原則,採用IO多線程是比較合適的。

 

3、 源碼剖析

3.1、關鍵變量

io_threads_active:是否開始 IO 多線程

io_threads_op :操作類型 讀寫IO_THREADS_OP_WRITE or IO_THREADS_OP_READ

io_threads_list[IO_THREADS_MAX_NUM] :每個線程的 等待客戶端列表

io_threads_pending:每個線程的 等待客戶端個數

 

3.1、初始化線程,註冊回調

 

第25行,調用pthread_create 庫函數創建線程,並且註冊線程回調函數IOThreadMain。線程TID 綁定線程ID io_threads[i] = tid

 


void initThreadedIO(void) {
    io_threads_active = 0; /* We start with threads not active. */

    /* Don't spawn any thread if the user selected a single thread:
     * we'll handle I/O directly from the main thread. */
    if (server.io_threads_num == 1) return;

    if (server.io_threads_num > IO_THREADS_MAX_NUM) {
        serverLog(LL_WARNING,"Fatal: too many I/O threads configured. "
                             "The maximum number is %d.", IO_THREADS_MAX_NUM);
        exit(1);
    }

    /* Spawn and initialize the I/O threads. */
    for (int i = 0; i < server.io_threads_num; i++) {
        /* Things we do for all the threads including the main thread. */
        io_threads_list[i] = listCreate();
        if (i == 0) continue; /* Thread 0 is the main thread. */

        /* Things we do only for the additional threads. */
        pthread_t tid;
        pthread_mutex_init(&io_threads_mutex[i],NULL);
        io_threads_pending[i] = 0;
        pthread_mutex_lock(&io_threads_mutex[i]); /* Thread will be stopped. */
        if (pthread_create(&tid,NULL,IOThreadMain,(void*)(long)i) != 0) {
            serverLog(LL_WARNING,"Fatal: Can't initialize IO thread.");
            exit(1);
        }
        io_threads[i] = tid;
    }
}

3.2、回調函數

 

第12 行其實是一個循環等待的實現,這裏不用sleep,是爲了避免設置sleep 的時間不合適造成性能的損失, 但是循環等待 也會 佔用CPU,也是一個開銷。

 

第17 行其實是允許主線程來關閉其他線程的操作,只要將某個線程i的io_threads_pending[id] 設置爲0

 

第32行是將當前線程等待隊列裏所有的請求client,依次取出處理, 讀操作通過 readQueryFromClient 處理, 寫操作通過writeToClient 處理。

 


void *IOThreadMain(void *myid) {
    /* The ID is the thread number (from 0 to server.iothreads_num-1), and is
     * used by the thread to just manipulate a single sub-array of clients. */
    long id = (unsigned long)myid;
    char thdname[16];

    snprintf(thdname, sizeof(thdname), "io_thd_%ld", id);
    redis_set_thread_title(thdname);

    while(1) {
        /* Wait for start */
        for (int j = 0; j < 1000000; j++) {
            if (io_threads_pending[id] != 0) break;
        }

        /* Give the main thread a chance to stop this thread. */
        if (io_threads_pending[id] == 0) {
            pthread_mutex_lock(&io_threads_mutex[id]);
            pthread_mutex_unlock(&io_threads_mutex[id]);
            continue;
        }

        serverAssert(io_threads_pending[id] != 0);

        if (tio_debug) printf("[%ld] %d to handle\n", id, (int)listLength(io_threads_list[id]));

        /* Process: note that the main thread will never touch our list
         * before we drop the pending count to 0. */
        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) {
                writeToClient(c,0);
            } else if (io_threads_op == IO_THREADS_OP_READ) {
                readQueryFromClient(c->conn);
            } else {
                serverPanic("io_threads_op value is unknown");
            }
        }
        listEmpty(io_threads_list[id]);
        io_threads_pending[id] = 0;

        if (tio_debug) printf("[%ld] Done\n", id);
    }
}

3.3、分配待處理任務

 

下面函數的主要功能是將IO請求,分配給不同的IO線程去處理。

 

第24行可以看出,分配策略是RR,int target_id = item_id % server.io_threads_num; 是用id做的hash取模。

 

第25行是選出IO線程後,將請求client,加入到他的隊列尾部。

 

第49行是更新標識,記錄各個線程隊列的長度,來通知回調函數執行。

 

第50行,是等所有線程都執行完成後,再回歸主線程。

 

第56行,是主線程執行接下來的命令操作。

 


int handleClientsWithPendingWritesUsingThreads(void) {
    int processed = listLength(server.clients_pending_write);
    if (processed == 0) return 0; /* Return ASAP if there are no clients. */

    /* If I/O threads are disabled or we have few clients to serve, don't
     * use I/O threads, but thejboring synchronous code. */
    if (server.io_threads_num == 1 || stopThreadedIOIfNeeded()) {
        return handleClientsWithPendingWrites();
    }

    /* Start threads if needed. */
    if (!io_threads_active) startThreadedIO();

    if (tio_debug) printf("%d TOTAL WRITE pending clients\n", processed);

    /* Distribute the clients across N different lists. */
    listIter li;
    listNode *ln;
    listRewind(server.clients_pending_write,&li);
    int item_id = 0;
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        c->flags &= ~CLIENT_PENDING_WRITE;
        int target_id = item_id % server.io_threads_num;
        listAddNodeTail(io_threads_list[target_id],c);
        item_id++;
    }

    /* Give the start condition to the waiting threads, by setting the
     * start condition atomic var. */
    io_threads_op = IO_THREADS_OP_WRITE;
    for (int j = 1; j < server.io_threads_num; j++) {
        int count = listLength(io_threads_list[j]);
        io_threads_pending[j] = count;
    }

    /* Also use the main thread to process a slice of clients. */
    listRewind(io_threads_list[0],&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        writeToClient(c,0);
    }
    listEmpty(io_threads_list[0]);

    /* Wait for all the other threads to end their work. */
    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;
    }
    if (tio_debug) printf("I/O WRITE All threads finshed\n");

    /* Run the list of clients again to install the write handler where
     * needed. */
    listRewind(server.clients_pending_write,&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);

        /* Install the write handler if there are pending writes in some
         * of the clients. */
        if (clientHasPendingReplies(c) &&
                connSetWriteHandler(c->conn, sendReplyToClient) == AE_ERR)
        {
            freeClientAsync(c);
        }
    }
    listEmpty(server.clients_pending_write);
    return processed;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章