Redis高級實踐之————Redis短連接性能優化

摘要: 對於Redis服務,通常我們推薦用戶使用長連接來訪問Redis,但是由於某些用戶在連接池失效的時候還是會建立大量的短連接或者用戶由於客戶端限制還是隻能使用短連接來訪問Redis,而原生的Redis在頻繁建立短連接的時候有一定性能損耗,本文從源碼角度對Redis短連接的性能進行了優化。

1. 問題

通過歷史監控我們可以發現用戶在頻繁使用短連接的時候Redis的cpu使用率有顯著的上升

2. 排查

通過扁鵲查看但是Redis的cpu運行情況如下

bianque

從扁鵲我們可以看到Redis在freeClient的時候會頻繁調用listSearchKey,並且該函數佔用了百分30左右的調用量,如果我們可以優化降低該調用,短連接性能將得到具體提升。

3. 優化

通過以上分析我們可以知道Redis在釋放鏈接的時候頻繁調用了listSearchKey,通過查看Redis關閉客戶端源碼如下:

void freeClient(redisClient *c) {
    listNode *ln;

    /* If this is marked as current client unset it */
    if (server.current_client == c) server.current_client = NULL;

    /* If it is our master that's beging disconnected we should make sure
     * to cache the state to try a partial resynchronization later.
     *
     * Note that before doing this we make sure that the client is not in
     * some unexpected state, by checking its flags. */
    if (server.master && c->flags & REDIS_MASTER) {
        redisLog(REDIS_WARNING,"Connection with master lost.");
        if (!(c->flags & (REDIS_CLOSE_AFTER_REPLY|
                          REDIS_CLOSE_ASAP|
                          REDIS_BLOCKED| REDIS_UNBLOCKED)))
        {
            replicationCacheMaster(c);
            return;
        }
    }

    /* Log link disconnection with slave */
    if ((c->flags & REDIS_SLAVE) && !(c->flags & REDIS_MONITOR)) {
        redisLog(REDIS_WARNING,"Connection with slave %s lost.",
            replicationGetSlaveName(c));
    }

    /* Free the query buffer */
    sdsfree(c->querybuf);
    c->querybuf = NULL;

    /* Deallocate structures used to block on blocking ops. */
    if (c->flags & REDIS_BLOCKED)
        unblockClientWaitingData(c);
    dictRelease(c->bpop.keys);

    freeClientArgv(c);

    /* Remove from the list of clients */
    if (c->fd != -1) {
        ln = listSearchKey(server.clients,c);
        redisAssert(ln != NULL);
        listDelNode(server.clients,ln);
    }

    /* When client was just unblocked because of a blocking operation,
     * remove it from the list of unblocked clients. */
    if (c->flags & REDIS_UNBLOCKED) {
        ln = listSearchKey(server.unblocked_clients,c);
        redisAssert(ln != NULL);
        listDelNode(server.unblocked_clients,ln);
    }
    ...
    ...
    ...
    /* Release other dynamically allocated client structure fields,
     * and finally release the client structure itself. */
    if (c->name) decrRefCount(c->name);
    zfree(c->argv);
    freeClientMultiState(c);
    sdsfree(c->peerid);
    if (c->pause_event > 0) aeDeleteTimeEvent(server.el, c->pause_event);
    zfree(c);
}

從源碼我們可以看到Redis在釋放鏈接的時候遍歷server.clients查找到對應的redisClient對象然後調用listDelNode把該redisClient對象從server.clients刪除,代碼如下:

/* Remove from the list of clients */
    if (c->fd != -1) {
        ln = listSearchKey(server.clients,c);
        redisAssert(ln != NULL);
        listDelNode(server.clients,ln);
    }

查看server.clients爲List結構,而redis定義的List爲雙端鏈表,我們可以在createClient的時候將redisClient的指針地址保留再freeClient的時候直接刪除對應的listNode即可,無需再次遍歷server.clients,代碼優化如下:

3.1 createClient修改

redisClient *createClient(int fd) {
    redisClient *c = zmalloc(sizeof(redisClient));

    /* passing -1 as fd it is possible to create a non connected client.
     * This is useful since all the Redis commands needs to be executed
     * in the context of a client. When commands are executed in other
     * contexts (for instance a Lua script) we need a non connected client. */
    if (fd != -1) {
        anetNonBlock(NULL,fd);
        anetEnableTcpNoDelay(NULL,fd);
        if (server.tcpkeepalive)
            anetKeepAlive(NULL,fd,server.tcpkeepalive);
        if (aeCreateFileEvent(server.el,fd,AE_READABLE,
            readQueryFromClient, c) == AE_ERR)
        {
            close(fd);
            zfree(c);
            return NULL;
        }
    }
     ...
    if (fd != -1) {
        c->client_list_node = listAddNodeTailReturnNode(server.clients,c);
    }
    return c;
}

3.2 freeClient修改

freeClient修改如下:

 /* Remove from the list of clients */
    if (c->fd != -1) {
        if (c->client_list_node != NULL) listDelNode(server.clients,c->client_list_node);
    }

3.3 優化結果

在同一臺物理機上啓動優化前後的Redis,分別進行壓測,壓測命令如下:

redis-benchmark -h host -p port -k 0 -t get -n 100000  -c 8000

其中-k 代表使用短連接進行測試

原生Redis-server結果:

99.74% <= 963 milliseconds
99.78% <= 964 milliseconds
99.84% <= 965 milliseconds
99.90% <= 966 milliseconds
99.92% <= 967 milliseconds
99.94% <= 968 milliseconds
99.99% <= 969 milliseconds
100.00% <= 969 milliseconds
10065.42 requests per second

優化後Redis-server結果

99.69% <= 422 milliseconds
99.72% <= 423 milliseconds
99.80% <= 424 milliseconds
99.82% <= 425 milliseconds
99.86% <= 426 milliseconds
99.89% <= 427 milliseconds
99.94% <= 428 milliseconds
99.96% <= 429 milliseconds
99.97% <= 430 milliseconds
100.00% <= 431 milliseconds
13823.61 requests per second

我們可以看到優化之後的Redis-server性能在短連接多的場景下提升了百分30%以上。