成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

 我們在使用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命令:

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

最終QPS:

  1. PING_INLINE: 92902.27 requests per second 
  2. PING_BULK: 93580.38 requests per second 

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

 

2)短連接測試

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

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

最終QPS:

  1. PING_INLINE: 15187.18 requests per second 
  2. PING_BULK: 16471.75 requests per second 

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

 

從QPS上看,短連接與長連接差距比較大,原因來自兩方面:

  • 每次重新建連接引入的網絡開銷。
  • 釋放連接時,redis-server需消耗額外的CPU周期做清理工作。(這一點可以嘗試從redis-server端做優化)

2、Redis連接釋放

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

  1. void freeClient(client *c) { 
  2.     listNode *ln; 
  3.  
  4.     /* .........*/ 
  5.  
  6.     /* Free the query buffer */ 
  7.     sdsfree(c->querybuf); 
  8.     sdsfree(c->pending_querybuf); 
  9.     c->querybuf = NULL
  10.  
  11.     /* Deallocate structures used to block on blocking ops. */ 
  12.     if (c->flags & CLIENT_BLOCKED) unblockClient(c); 
  13.     dictRelease(c->bpop.keys); 
  14.  
  15.     /* UNWATCH all the keys */ 
  16.     unwatchAllKeys(c); 
  17.     listRelease(c->watched_keys); 
  18.  
  19.     /* Unsubscribe from all the pubsub channels */ 
  20.     pubsubUnsubscribeAllChannels(c,0); 
  21.     pubsubUnsubscribeAllPatterns(c,0); 
  22.     dictRelease(c->pubsub_channels); 
  23.     listRelease(c->pubsub_patterns); 
  24.  
  25.     /* Free data structures. */ 
  26.     listRelease(c->reply); 
  27.     freeClientArgv(c); 
  28.  
  29.     /* Unlink the client: this will close the socket, remove the I/O 
  30.      * handlers, and remove references of the client from different 
  31.      * places where active clients may be referenced. */ 
  32.     /*  redis-server維護了一個server.clients鏈表,當用戶端建立連接后,新建一個client對象并追加到server.clients上, 
  33.         當連接釋放時,需求從server.clients上刪除client對象 */ 
  34.     unlinkClient(c); 
  35.  
  36.    /* ...........*/ 
  37. void unlinkClient(client *c) { 
  38.     listNode *ln; 
  39.  
  40.     /* If this is marked as current client unset it. */ 
  41.     if (server.current_client == c) server.current_client = NULL
  42.  
  43.     /* Certain operations must be done only if the client has an active socket. 
  44.      * If the client was already unlinked or if it's a "fake client" the 
  45.      * fd is already set to -1. */ 
  46.     if (c->fd != -1) { 
  47.         /* 搜索server.clients鏈表,然后刪除client節點對象,這里復雜為O(N) */ 
  48.         ln = listSearchKey(server.clients,c); 
  49.         serverAssert(ln != NULL); 
  50.         listDelNode(server.clients,ln); 
  51.  
  52.         /* Unregister async I/O handlers and close the socket. */ 
  53.         aeDeleteFileEvent(server.el,c->fd,AE_READABLE); 
  54.         aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE); 
  55.         close(c->fd); 
  56.         c->fd = -1; 
  57.     } 
  58.  
  59.    /*   ......... */ 

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

3、優化

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

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

  1. client *createClient(int fd) { 
  2.     client *c = zmalloc(sizeof(client)); 
  3.    /*  ........  */ 
  4.     listSetFreeMethod(c->pubsub_patterns,decrRefCountVoid); 
  5.     listSetMatchMethod(c->pubsub_patterns,listMatchObjects); 
  6.     if (fd != -1) { 
  7.         /*  client記錄自身所在list的listNode地址 */ 
  8.         c->client_list_node = listAddNodeTailEx(server.clients,c); 
  9.     }  
  10.     initClientMultiState(c); 
  11.     return c; 
  12. void unlinkClient(client *c) { 
  13.     listNode *ln; 
  14.  
  15.     /* If this is marked as current client unset it. */ 
  16.     if (server.current_client == c) server.current_client = NULL
  17.  
  18.     /* Certain operations must be done only if the client has an active socket. 
  19.      * If the client was already unlinked or if it's a "fake client" the 
  20.      * fd is already set to -1. */ 
  21.     if (c->fd != -1) { 
  22.         /* 這時不再需求搜索server.clients鏈表 */ 
  23.         //ln = listSearchKey(server.clients,c); 
  24.         //serverAssert(ln != NULL); 
  25.         //listDelNode(server.clients,ln); 
  26.         listDelNode(server.clients, c->client_list_node); 
  27.  
  28.         /* Unregister async I/O handlers and close the socket. */ 
  29.         aeDeleteFileEvent(server.el,c->fd,AE_READABLE); 
  30.         aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE); 
  31.         close(c->fd); 
  32.         c->fd = -1; 
  33.     } 
  34.  
  35.    /*   ......... */ 

優化后短連接測試

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

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

最終QPS:

  1. PING_INLINE: 21884.23 requests per second 
  2. PING_BULK: 21454.62 requests per second 

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

二、info命令導致CPU高

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

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

  1. client 
  2. connected_clients:1 
  3. client_longest_output_list:0 // redis-server端最長的outputbuffer列表長度 
  4. client_biggest_input_buf:0. // redis-server端最長的inputbuffer字節長度 
  5. blocked_clients:0 
  6. Memory 
  7. used_memory:848392 
  8. used_memory_human:828.51K 
  9. used_memory_rss:3620864 
  10. used_memory_rss_human:3.45M 
  11. used_memory_peak:619108296 
  12. used_memory_peak_human:590.43M 
  13. used_memory_peak_perc:0.14% 
  14. used_memory_overhead:836182 // 除dataset外,redis-server為維護自身結構所額外占用的內存量 
  15. used_memory_startup:786552 
  16. used_memory_dataset:12210 
  17. used_memory_dataset_perc:19.74% 
  18. 為了得到client_longest_output_list、client_longest_output_list狀態,需要遍歷redis-server端所有的client, 如getClientsMaxBuffers所示,可能看到這里也是存在同樣的O(N)運算。 
  19. void getClientsMaxBuffers(unsigned long *longest_output_list, 
  20.                           unsigned long *biggest_input_buffer) { 
  21.     client *c; 
  22.     listNode *ln; 
  23.     listIter li; 
  24.     unsigned long lol = 0, bib = 0; 
  25.     /* 遍歷所有client, 復雜度O(N) */ 
  26.     listRewind(server.clients,&li); 
  27.     while ((ln = listNext(&li)) != NULL) { 
  28.         c = listNodeValue(ln); 
  29.  
  30.         if (listLength(c->reply) > lol) lol = listLength(c->reply); 
  31.         if (sdslen(c->querybuf) > bib) bib = sdslen(c->querybuf); 
  32.     } 
  33.     *longest_output_list = lol; 
  34.     *biggest_input_buffer = bib; 
  35. 為了得到used_memory_overhead狀態,同樣也需要遍歷所有client計算所有client的outputBuffer所占用的內存總量,如getMemoryOverheadData所示: 
  36. struct redisMemOverhead *getMemoryOverheadData(void) { 
  37.  
  38.     /* ......... */ 
  39.     mem = 0; 
  40.     if (server.repl_backlog) 
  41.         mem += zmalloc_size(server.repl_backlog); 
  42.     mh->repl_backlog = mem; 
  43.     mem_total += mem; 
  44.    /* ...............*/ 
  45.     mem = 0; 
  46.     if (listLength(server.clients)) { 
  47.         listIter li; 
  48.         listNode *ln; 
  49.         /*  遍歷所有的client, 計算所有client outputBuffer占用的內存總和,復雜度為O(N)  */ 
  50.         listRewind(server.clients,&li); 
  51.         while((ln = listNext(&li))) { 
  52.             client *c = listNodeValue(ln); 
  53.             if (c->flags & CLIENT_SLAVE) 
  54.                 continue
  55.             mem += getClientOutputBufferMemoryUsage(c); 
  56.             mem += sdsAllocSize(c->querybuf); 
  57.             mem += sizeof(client); 
  58.         } 
  59.     } 
  60.     mh->clients_normal = mem; 
  61.     mem_total+=mem; 
  62.  
  63.     mem = 0; 
  64.     if (server.aof_state != AOF_OFF) { 
  65.         mem += sdslen(server.aof_buf); 
  66.         mem += aofRewriteBufferSize(); 
  67.     } 
  68.     mh->aof_buffer = mem; 
  69.     mem_total+=mem; 
  70.  
  71.   /* ......... */ 
  72.  
  73.     return mh; 

實驗

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

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

  1. func main() {                                                                                                                                              
  2.      c, err := redis.Dial("tcp", addr)                                                                                                              
  3.      if err != nil {                                                                                                         
  4.         fmt.Println("Connect to redis error:", err)                                                           
  5.         return                                                                                                                
  6.      }                                                                                                                                                                                                                                                    
  7.      for {                                                                                                                      
  8.         c.Do("info")                                                                                                      
  9.      }                                                                                                                                                                                                                                               
  10.      return                                                                                                                   

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

 

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

  1. func main() {                                                                   
  2.      clients := []redis.Conn{}                                      
  3.      for i := 0; i < 9999; i++ {                                     
  4.         c, err := redis.Dial("tcp", addr)                       
  5.         if err != nil {                                                       
  6.            fmt.Println("Connect to redis error:", err)  
  7.            return                                                              
  8.         }                                                                          
  9.         clients = append(clients, c)                            
  10.      }                                                                             
  11.      c, err := redis.Dial("tcp", addr)                          
  12.      if err != nil {                                                          
  13.         fmt.Println("Connect to redis error:", err)     
  14.         return                                                                 
  15.      }                                                                                                                                                           
  16.      for {                                                                         
  17.         _, err = c.Do("info")                                                               
  18.         if err != nil {                                                        
  19.            panic(err)                                                                      
  20.         }                                                                           
  21.      }                                                                                
  22.      return                                                                              

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

 

3)pipeline導致內存占用高

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

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

  1. import ( 
  2.     "fmt" 
  3.     "github.com/garyburd/redigo/redis" 
  4.  
  5. func main(){ 
  6.     c, err := redis.Dial("tcp""127.0.0.1:6379"
  7.     if err != nil { 
  8.         panic(err) 
  9.     } 
  10.     c.Send("get""key1")       //緩存到client端的buffer中 
  11.     c.Send("get""key2")       //緩存到client端的buffer中 
  12.     c.Send("get""key3")       //緩存到client端的buffer中 
  13.     c.Flush()                   //將buffer中的內容以一特定的協議格式發送到redis-server端 
  14.     fmt.Println(redis.String(c.Receive())) 
  15.     fmt.Println(redis.String(c.Receive())) 
  16.     fmt.Println(redis.String(c.Receive())) 

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

  1. *2\r\n$3\r\nget\r\n$4\r\nkey1\r\n*2\r\n$3\r\nget\r\n$4\r\nkey2\r\n*2\r\n$3\r\nget\r\n$4\r\nkey3\r\n 

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

  1. readQueryFromClient(client* c) { 
  2.     read(c->querybuf) // c->query="*2\r\n$3\r\nget\r\n$4\r\nkey1\r\n*2\r\n$3\r\nget\r\n$4\r\nkey2\r\n*2\r\n$3\r\nget\r\n$4\r\nkey3\r\n" 
  3.     cmdsNum = parseCmdNum(c->querybuf)  // cmdNum = 3 
  4.     while(cmsNum--) { 
  5.         cmd = parseCmd(c->querybuf)    // cmd:  get key1、get key2、get key3 
  6.         reply = execCmd(cmd) 
  7.         appendReplyBuffer(reply) 
  8.         markClientPendingWrite(c) 
  9.     } 

考慮這樣一種情況:

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

小結

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

  • 盡量不要使用短連接;
  • 盡量不要在連接數比較高的場景下頻繁使用info;
  • 使用pipeline時,要及時接收請求處理結果,且pipeline不宜一次打包太多請求。

作者介紹

張鵬義,騰訊云數據庫高級工程師,曾參與華為Taurus分布式數據研發及騰訊CynosDB for pg研發工作,現從事騰訊云Redis數據庫研發工作。

 

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

 

 

責任編輯:武曉燕 來源: DBAplus社群
相關推薦

2021-02-26 13:35:46

JavaCPU內存

2013-10-24 15:15:45

Linux配置問題

2013-10-22 10:08:59

linux安全權限配置權限管理

2010-01-28 10:11:18

IT金飯碗

2017-11-27 12:08:10

后端服務spring mvc項目

2013-11-29 09:30:52

2013-08-01 13:55:55

Android 4.3新特性

2010-04-29 16:36:19

Oracle數據庫

2012-11-28 15:53:16

災難恢復

2019-08-30 12:01:48

2023-08-04 14:31:43

Python核心項目

2011-08-17 10:04:13

vSphere 5虛擬化

2017-02-08 09:51:27

JavaScript細節

2024-10-18 14:29:28

2023-11-06 18:02:28

Linux實用命令

2017-08-15 17:09:31

Linux命令

2009-12-03 14:22:57

2009-10-29 16:41:23

2024-02-29 07:48:55

Python編程語言上下文管理器

2017-01-15 15:13:37

Android性能優化優化點
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 成人在线观看欧美 | 99热欧美 | 欧美日韩国产一区二区三区 | 欧美1级 | 国产成人久久精品一区二区三区 | 亚洲精品一区二区在线 | 国产一级片免费视频 | 精品小视频 | 人人鲁人人莫人人爱精品 | 亚洲欧美激情网 | 国产在线播放一区二区三区 | 男人的天堂在线视频 | 波多野结衣av中文字幕 | 久久亚洲国产精品日日av夜夜 | 日韩高清中文字幕 | 成人在线精品视频 | 国产欧美一区二区三区在线看蜜臀 | 久草福利 | jav成人av免费播放 | 国产高清视频一区 | 欧美一级淫片免费视频黄 | 日韩在线欧美 | 国产区在线 | 99精品一区二区三区 | 丁香久久 | 亚洲国产成人精品久久 | 亚洲国产aⅴ精品一区二区 免费观看av | 激情av在线| 日朝毛片 | 欧美精品一区在线 | h视频在线观看免费 | 国产成人精品久久二区二区91 | 色播久久 | 91精品国产91久久综合桃花 | 国产高清免费 | 精品亚洲二区 | 国产精品一区二区视频 | 国产精品成人一区二区三区夜夜夜 | 日韩三级在线 | 亚洲欧美视频一区二区 | 国产丝袜一区二区三区免费视频 |