亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何解碼Redis最易被忽視的CPU和內存占用高問題

發布時間:2021-11-29 16:34:16 來源:億速云 閱讀:235 作者:柒染 欄目:數據庫

這期內容當中小編將會給大家帶來有關如何解碼Redis最易被忽視的CPU和內存占用高問題,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

我們在使用Redis時,總會碰到一些redis-server端CPU及內存占用比較高的問題。下面以幾個實際案例為例,來討論一下在使用Redis時容易忽視的幾種情形。

一、短連接導致CPU高

某用戶反映QPS不高,從監控看CPU確實偏高。既然QPS不高,那么redis-server自身很可能在做某些清理工作或者用戶在執行復雜度較高的命令,經排查無沒有進行key過期刪除操作,沒有執行復雜度高的命令。
上機器對redis-server進行perf分析,發現函數listSearchKey占用CPU比較高,分析調用棧發現在釋放連接時會頻繁調用listSearchKey,且用戶反饋說是使用的短連接,所以推斷是頻繁釋放連接導致CPU占用有所升高。

1、對比實例

下面使用redis-benchmark工具分別使用長連接和短連接做一個對比實驗,redis-server為社區版4.0.10。

1)長連接測試

使用10000個長連接向redis-server發送50w次ping命令:

./redis-benchmark -h host -p port -t ping -c 10000 -n 500000 -k 1(k=1表示使用長連接,k=0表示使用短連接)

最終QPS:

PING_INLINE: 92902.27 requests per second
PING_BULK: 93580.38 requests per second

對redis-server分析,發現占用CPU最高的是readQueryFromClient,即主要是在處理來自用戶端的請求。

如何解碼Redis最易被忽視的CPU和內存占用高問題

2)短連接測試

使用10000個短連接向redis-server發送50w次ping命令:

./redis-benchmark -h host -p port -t ping -c 10000 -n 500000 -k 0

最終QPS:

PING_INLINE: 15187.18 requests per second
PING_BULK: 16471.75 requests per second

對redis-server分析,發現占用CPU最高的確實是listSearchKey,而readQueryFromClient所占CPU的比例比listSearchKey要低得多,也就是說CPU有點“不務正業”了,處理用戶請求變成了副業,而搜索list卻成為了主業。所以在同樣的業務請求量下,使用短連接會增加CPU的負擔。

如何解碼Redis最易被忽視的CPU和內存占用高問題從QPS上看,短連接與長連接差距比較大,原因來自兩方面:

  • 每次重新建連接引入的網絡開銷。

  • 釋放連接時,redis-server需消耗額外的CPU周期做清理工作。(這一點可以嘗試從redis-server端做優化)

2、Redis連接釋放

我們從代碼層面來看下redis-server在用戶端發起連接釋放后都會做哪些事情,redis-server在收到用戶端的斷連請求時會直接進入到freeClient。

void freeClient(client *c) {
    listNode *ln;
    /* .........*/
    /* Free the query buffer */
    sdsfree(c->querybuf);
    sdsfree(c->pending_querybuf);
    c->querybuf = NULL;
    /* Deallocate structures used to block on blocking ops. */
    if (c->flags & CLIENT_BLOCKED) unblockClient(c);
    dictRelease(c->bpop.keys);
    /* UNWATCH all the keys */
    unwatchAllKeys(c);
    listRelease(c->watched_keys);
    /* Unsubscribe from all the pubsub channels */
    pubsubUnsubscribeAllChannels(c,0);
    pubsubUnsubscribeAllPatterns(c,0);
    dictRelease(c->pubsub_channels);
    listRelease(c->pubsub_patterns);
    /* Free data structures. */
    listRelease(c->reply);
    freeClientArgv(c);
    /* Unlink the client: this will close the socket, remove the I/O
     * handlers, and remove references of the client from different
     * places where active clients may be referenced. */
    /*  redis-server維護了一個server.clients鏈表,當用戶端建立連接后,新建一個client對象并追加到server.clients上,
        當連接釋放時,需求從server.clients上刪除client對象 */
    unlinkClient(c);
   /* ...........*/
}
void unlinkClient(client *c) {
    listNode *ln;
    /* If this is marked as current client unset it. */
    if (server.current_client == c) server.current_client = NULL;
    /* Certain operations must be done only if the client has an active socket.
     * If the client was already unlinked or if it's a "fake client" the
     * fd is already set to -1. */
    if (c->fd != -1) {
        /* 搜索server.clients鏈表,然后刪除client節點對象,這里復雜為O(N) */
        ln = listSearchKey(server.clients,c);
        serverAssert(ln != NULL);
        listDelNode(server.clients,ln);
        /* Unregister async I/O handlers and close the socket. */
        aeDeleteFileEvent(server.el,c->fd,AE_READABLE);
        aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
        close(c->fd);
        c->fd = -1;
    }
   /*   ......... */

所以在每次連接斷開時,都存在一個O(N)的運算。對于redis這樣的內存數據庫,我們應該盡量避開O(N)運算,特別是在連接數比較大的場景下,對性能影響比較明顯。雖然用戶只要不使用短連接就能避免,但在實際的場景中,用戶端連接池被打滿后,用戶也可能會建立一些短連接。

3、優化

從上面的分析看,每次連接釋放時都會進行O(N)的運算,那能不能降復雜度降到O(1)呢?

這個問題非常簡單,server.clients是個雙向鏈表,只要當client對象在創建時記住自己的內存地址,釋放時就不需要遍歷server.clients。接下來嘗試優化下:

client *createClient(int fd) {
    client *c = zmalloc(sizeof(client));
   /*  ........  */
    listSetFreeMethod(c->pubsub_patterns,decrRefCountVoid);
    listSetMatchMethod(c->pubsub_patterns,listMatchObjects);
    if (fd != -1) {
        /*  client記錄自身所在list的listNode地址 */
        c->client_list_node = listAddNodeTailEx(server.clients,c);
    } 
    initClientMultiState(c);
    return c;
}
void unlinkClient(client *c) {
    listNode *ln;
    /* If this is marked as current client unset it. */
    if (server.current_client == c) server.current_client = NULL;
    /* Certain operations must be done only if the client has an active socket.
     * If the client was already unlinked or if it's a "fake client" the
     * fd is already set to -1. */
    if (c->fd != -1) {
        /* 這時不再需求搜索server.clients鏈表 */
        //ln = listSearchKey(server.clients,c);
        //serverAssert(ln != NULL);
        //listDelNode(server.clients,ln);
        listDelNode(server.clients, c->client_list_node);
        /* Unregister async I/O handlers and close the socket. */
        aeDeleteFileEvent(server.el,c->fd,AE_READABLE);
        aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
        close(c->fd);
        c->fd = -1;
    }
   /*   ......... */

優化后短連接測試

使用10000個短連接向redis-server發送50w次ping命令:

./redis-benchmark -h host -p port -t ping -c 10000 -n 500000 -k 0

最終QPS:

PING_INLINE: 21884.23 requests per second
PING_BULK: 21454.62 requests per second

與優化前相比,短連接性能能夠提升30+%,所以能夠保證存在短連接的情況下,性能不至于太差。

二、info命令導致CPU高

有用戶通過定期執行info命令監視redis的狀態,這會在一定程度上導致CPU占用偏高。頻繁執行info時通過perf分析發現getClientsMaxBuffers、getClientOutputBufferMemoryUsage及getMemoryOverheadData這幾個函數占用CPU比較高。

通過Info命令,可以拉取到redis-server端的如下一些狀態信息(未列全):

client
connected_clients:1
client_longest_output_list:0 // redis-server端最長的outputbuffer列表長度
client_biggest_input_buf:0. // redis-server端最長的inputbuffer字節長度
blocked_clients:0
Memory
used_memory:848392
used_memory_human:828.51K
used_memory_rss:3620864
used_memory_rss_human:3.45M
used_memory_peak:619108296
used_memory_peak_human:590.43M
used_memory_peak_perc:0.14%
used_memory_overhead:836182 // 除dataset外,redis-server為維護自身結構所額外占用的內存量
used_memory_startup:786552
used_memory_dataset:12210
used_memory_dataset_perc:19.74%
為了得到client_longest_output_list、client_longest_output_list狀態,需要遍歷redis-server端所有的client, 如getClientsMaxBuffers所示,可能看到這里也是存在同樣的O(N)運算。
void getClientsMaxBuffers(unsigned long *longest_output_list,
                          unsigned long *biggest_input_buffer) {
    client *c;
    listNode *ln;
    listIter li;
    unsigned long lol = 0, bib = 0;
    /* 遍歷所有client, 復雜度O(N) */
    listRewind(server.clients,&li);
    while ((ln = listNext(&li)) != NULL) {
        c = listNodeValue(ln);
        if (listLength(c->reply) > lol) lol = listLength(c->reply);
        if (sdslen(c->querybuf) > bib) bib = sdslen(c->querybuf);
    }
    *longest_output_list = lol;
    *biggest_input_buffer = bib;
}
為了得到used_memory_overhead狀態,同樣也需要遍歷所有client計算所有client的outputBuffer所占用的內存總量,如getMemoryOverheadData所示:
struct redisMemOverhead *getMemoryOverheadData(void) {
    /* ......... */
    mem = 0;
    if (server.repl_backlog)
        mem += zmalloc_size(server.repl_backlog);
    mh->repl_backlog = mem;
    mem_total += mem;
   /* ...............*/
    mem = 0;
    if (listLength(server.clients)) {
        listIter li;
        listNode *ln;
        /*  遍歷所有的client, 計算所有client outputBuffer占用的內存總和,復雜度為O(N)  */
        listRewind(server.clients,&li);
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            if (c->flags & CLIENT_SLAVE)
                continue;
            mem += getClientOutputBufferMemoryUsage(c);
            mem += sdsAllocSize(c->querybuf);
            mem += sizeof(client);
        }
    }
    mh->clients_normal = mem;
    mem_total+=mem;
    mem = 0;
    if (server.aof_state != AOF_OFF) {
        mem += sdslen(server.aof_buf);
        mem += aofRewriteBufferSize();
    }
    mh->aof_buffer = mem;
    mem_total+=mem;
  /* ......... */
    return mh;
}

實驗

從上面的分析知道,當連接數較高時(O(N)的N大),如果頻率執行info命令,會占用較多CPU。

1)建立一個連接,不斷執行info命令

func main() {                                                                                                                                             
     c, err := redis.Dial("tcp", addr)                                                                                                             
     if err != nil {                                                                                                        
        fmt.Println("Connect to redis error:", err)                                                          
        return                                                                                                               
     }                                                                                                                                                                                                                                                   
     for {                                                                                                                     
        c.Do("info")                                                                                                     
     }                                                                                                                                                                                                                                              
     return                                                                                                                  
}

實驗結果表明,CPU占用僅為20%左右。

如何解碼Redis最易被忽視的CPU和內存占用高問題


2)建立9999個空閑連接,及一個連接不斷執行info

func main() {                                                                  
     clients := []redis.Conn{}                                     
     for i := 0; i < 9999; i++ {                                    
        c, err := redis.Dial("tcp", addr)                      
        if err != nil {                                                      
           fmt.Println("Connect to redis error:", err) 
           return                                                             
        }                                                                         
        clients = append(clients, c)                           
     }                                                                            
     c, err := redis.Dial("tcp", addr)                         
     if err != nil {                                                         
        fmt.Println("Connect to redis error:", err)    
        return                                                                
     }                                                                                                                                                          
     for {                                                                        
        _, err = c.Do("info")                                                              
        if err != nil {                                                       
           panic(err)                                                                     
        }                                                                          
     }                                                                               
     return                                                                             
}

實驗結果表明CPU能夠達到80%,所以在連接數較高時,盡量避免使用info命令。

如何解碼Redis最易被忽視的CPU和內存占用高問題

3)pipeline導致內存占用高

有用戶發現在使用pipeline做只讀操作時,redis-server的內存容量偶爾也會出現明顯的上漲, 這是對pipeline的使不當造成的。下面先以一個簡單的例子來說明Redis的pipeline邏輯是怎樣的。

下面通過golang語言實現以pipeline的方式從redis-server端讀取key1、key2、key3。

import (
    "fmt"
    "github.com/garyburd/redigo/redis"
)
func main(){
    c, err := redis.Dial("tcp", "127.0.0.1:6379")
    if err != nil {
        panic(err)
    }
    c.Send("get", "key1")       //緩存到client端的buffer中
    c.Send("get", "key2")       //緩存到client端的buffer中
    c.Send("get", "key3")       //緩存到client端的buffer中
    c.Flush()                   //將buffer中的內容以一特定的協議格式發送到redis-server端
    fmt.Println(redis.String(c.Receive()))
    fmt.Println(redis.String(c.Receive()))
    fmt.Println(redis.String(c.Receive()))
}

而此時server端收到的內容為:

*2 $3 get $4 key1 *2 $3 get $4 key2 *2 $3 get $4 key3

下面是一段redis-server端非正式的代碼處理邏輯,redis-server端從接收到的內容依次解析出命令、執行命令、將執行結果緩存到replyBuffer中,并將用戶端標記為有內容需要寫出。等到下次事件調度時再將replyBuffer中的內容通過socket發送到client,所以并不是處理完一條命令就將結果返回用戶端。

readQueryFromClient(client* c) {
    read(c->querybuf) // c->query="*2 $3 get $4 key1 *2 $3 get $4 key2 *2 $3 get $4 key3 "
    cmdsNum = parseCmdNum(c->querybuf)  // cmdNum = 3
    while(cmsNum--) {
        cmd = parseCmd(c->querybuf)    // cmd:  get key1、get key2、get key3
        reply = execCmd(cmd)
        appendReplyBuffer(reply)
        markClientPendingWrite(c)
    }
}

考慮這樣一種情況:

如果用戶端程序處理比較慢,未能及時通過c.Receive()從TCP的接收buffer中讀取內容或者因為某些BUG導致沒有執行c.Receive(),當接收buffer滿了后,server端的TCP滑動窗口為0,導致server端無法發送replyBuffer中的內容,所以replyBuffer由于遲遲得不到釋放而占用額外的內存。當pipeline一次打包的命令數太多,以及包含如mget、hgetall、lrange等操作多個對象的命令時,問題會更突出。

上面幾種情況,都是非常簡單的問題,沒有復雜的邏輯,在大部分場景下都不算問題,但是在一些極端場景下要把Redis用好,開發者還是需要關注這些細節。建議:

  • 盡量不要使用短連接;

  • 盡量不要在連接數比較高的場景下頻繁使用info;

  • 使用pipeline時,要及時接收請求處理結果,且pipeline不宜一次打包太多請求。

上述就是小編為大家分享的如何解碼Redis最易被忽視的CPU和內存占用高問題了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

庆城县| 故城县| 渝中区| 平邑县| 宽城| 阿勒泰市| 从江县| 四子王旗| 昌吉市| 广安市| 砀山县| 库伦旗| 个旧市| 嫩江县| 澄城县| 鸡西市| 江津市| 宾川县| 普洱| 翼城县| 辽中县| 汕尾市| 纳雍县| 永平县| 张掖市| 宜宾县| 吉隆县| 琼海市| 西峡县| 塔河县| 洛浦县| 民乐县| 通城县| 五大连池市| 临夏市| 湘潭县| 尼玛县| 望奎县| 洱源县| 吴桥县| 石首市|