解碼Redis最易被忽視的CPU和記憶體佔用高問題
作者介紹
張鵬義,騰訊雲資料庫高階工程師,曾參與華為Taurus分散式資料研發及騰訊CynosDB for PG研發工作,現從事騰訊雲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命令:
./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,即主要是在處理來自使用者端的請求。
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的負擔。
從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%左右。
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命令。
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不宜一次打包太多請求。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69940575/viewspace-2675255/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java中的CPU佔用高和記憶體佔用高的問題排查Java記憶體
- Windbg分析高記憶體佔用問題記憶體
- ubuntu解決GPU視訊記憶體佔用問題UbuntuGPU記憶體
- 利用Windbg分析高記憶體佔用問題記憶體
- 電腦記憶體佔用過高怎麼辦 電腦記憶體佔用過高解決方法記憶體
- 被忽視的開發安全問題
- mysql佔用記憶體高的一種解決方法MySql記憶體
- win10記憶體佔用高怎麼解決_win10系統記憶體佔用高解決步驟Win10記憶體
- 使用Process Explorer/Process Hacker和Windbg高效排查軟體高CPU佔用問題
- java專案cpu或記憶體過高,排查問題思路Java記憶體
- mysql中CPU或記憶體利用率過高問題MySql記憶體
- win10開機記憶體佔用高怎麼解決_win10開機後記憶體佔用高的解決措施Win10記憶體
- 如何檢視 Linux 下 CPU、記憶體和交換分割槽的佔用率?Linux記憶體
- Redis的資料被刪除,佔用記憶體咋還那麼大?Redis記憶體
- MacOs中docker.Hyperkit佔用記憶體過高無法停止問題MacDocker記憶體
- 如何在生產環境排查 Rust 記憶體佔用過高問題Rust記憶體
- 【.Net Core】分析.net core在linux下記憶體佔用過高問題Linux記憶體
- 求問,如何讓手機 cpu 或者記憶體佔用調整到 100%?記憶體
- Linux(CentOS) 檢視當前佔用CPU或記憶體最多的K個程式LinuxCentOS記憶體
- Redis 實戰 —— 12. 降低記憶體佔用Redis記憶體
- GaussDB(DWS)效能調優,解決DM區大記憶體佔用問題記憶體
- 雲伺服器解決MSSQL 2005 佔用記憶體過大問題伺服器SQL記憶體
- 桌面視窗管理器佔用記憶體過高怎麼辦 電腦莫名其妙記憶體佔用很高記憶體
- Java 程式佔用 VIRT 虛擬記憶體超高的問題研究Java記憶體
- SHARED POOL中KGH: NOACCESS佔用大量記憶體的問題分析記憶體
- Chrome 再次最佳化記憶體佔用問題,新增記憶體釋放開關Chrome記憶體
- ubuntu下解決埠被佔用的問題Ubuntu
- Redis 檢視所有 key 的 value 值所佔記憶體大小Redis記憶體
- win10 audiodg狂佔記憶體怎麼辦_win10 audiodg佔用記憶體過高的解決方法Win10記憶體
- oracle RDBMS Kernel Executable 佔用記憶體過高Oracle記憶體
- php-fpm 記憶體過高,CPU佔有率過高帶來的最佳化和調整PHP記憶體
- Redis Quicklist 竟讓記憶體佔用狂降50%?RedisUI記憶體
- ubuntu下解決埠被佔用問題Ubuntu
- Docker-Java限制cpu和記憶體及淺析原始碼解決docker磁碟掛載失效問題DockerJava記憶體原始碼
- 禁用software_reporter_tool.exe 解決CPU高佔用率的問題
- 微軟Win10版本2004解決了高CPU佔用和磁碟效能問題微軟Win10
- 被忽視的問題:測試環境穩定性治理
- 如何檢視MySQL資料庫佔多大記憶體,佔用太多記憶體怎麼辦?MySql資料庫記憶體