久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

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

135次閱讀
沒有評論

共計(jì) 9000 個(gè)字符,預(yù)計(jì)需要花費(fèi) 23 分鐘才能閱讀完成。

這期內(nèi)容當(dāng)中丸趣 TV 小編將會(huì)給大家?guī)碛嘘P(guān)如何解碼 Redis 最易被忽視的 CPU 和內(nèi)存占用高問題,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

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

一、短連接導(dǎo)致 CPU 高

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

1、對比實(shí)例

下面使用 redis-benchmark 工具分別使用長連接和短連接做一個(gè)對比實(shí)驗(yàn),redis-server 為社區(qū)版 4.0.10。

1)長連接測試

使用 10000 個(gè)長連接向 redis-server 發(fā)送 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 分析,發(fā)現(xiàn)占用 CPU 最高的是 readQueryFromClient,即主要是在處理來自用戶端的請求。

2)短連接測試

使用 10000 個(gè)短連接向 redis-server 發(fā)送 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 分析,發(fā)現(xiàn)占用 CPU 最高的確實(shí)是 listSearchKey,而 readQueryFromClient 所占 CPU 的比例比 listSearchKey 要低得多,也就是說 CPU 有點(diǎn)“不務(wù)正業(yè)”了,處理用戶請求變成了副業(yè),而搜索 list 卻成為了主業(yè)。所以在同樣的業(yè)務(wù)請求量下,使用短連接會(huì)增加 CPU 的負(fù)擔(dān)。

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

每次重新建連接引入的網(wǎng)絡(luò)開銷。

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

2、Redis 連接釋放

我們從代碼層面來看下 redis-server 在用戶端發(fā)起連接釋放后都會(huì)做哪些事情,redis-server 在收到用戶端的斷連請求時(shí)會(huì)直接進(jìn)入到 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 維護(hù)了一個(gè) server.clients 鏈表,當(dāng)用戶端建立連接后,新建一個(gè) client 對象并追加到 server.clients 上,  當(dāng)連接釋放時(shí),需求從 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 節(jié)點(diǎn)對象,這里復(fù)雜為 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- 
 c- fd = -1;
 }
 /* ......... */

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

3、優(yōu)化

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

這個(gè)問題非常簡單,server.clients 是個(gè)雙向鏈表,只要當(dāng) client 對象在創(chuàng)建時(shí)記住自己的內(nèi)存地址,釋放時(shí)就不需要遍歷 server.clients。接下來嘗試優(yōu)化下:

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) {
 /*  這時(shí)不再需求搜索 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- 
 c- fd = -1;
 }
 /* ......... */

優(yōu)化后短連接測試

使用 10000 個(gè)短連接向 redis-server 發(fā)送 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

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

二、info 命令導(dǎo)致 CPU 高

有用戶通過定期執(zhí)行 info 命令監(jiān)視 redis 的狀態(tài),這會(huì)在一定程度上導(dǎo)致 CPU 占用偏高。頻繁執(zhí)行 info 時(shí)通過 perf 分析發(fā)現(xiàn) getClientsMaxBuffers、getClientOutputBufferMemoryUsage 及 getMemoryOverheadData 這幾個(gè)函數(shù)占用 CPU 比較高。

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

client
connected_clients:1
client_longest_output_list:0 // redis-server 端最長的 outputbuffer 列表長度
client_biggest_input_buf:0. // redis-server 端最長的 inputbuffer 字節(jié)長度
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 為維護(hù)自身結(jié)構(gòu)所額外占用的內(nèi)存量
used_memory_startup:786552
used_memory_dataset:12210
used_memory_dataset_perc:19.74%
為了得到 client_longest_output_list、client_longest_output_list 狀態(tài),需要遍歷 redis-server 端所有的 client,  如 getClientsMaxBuffers 所示,可能看到這里也是存在同樣的 O(N) 運(yù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,  復(fù)雜度 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 狀態(tài),同樣也需要遍歷所有 client 計(jì)算所有 client 的 outputBuffer 所占用的內(nèi)存總量,如 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,  計(jì)算所有 client outputBuffer 占用的內(nèi)存總和,復(fù)雜度為 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;
}

實(shí)驗(yàn)

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

1)建立一個(gè)連接,不斷執(zhí)行 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 
}

實(shí)驗(yàn)結(jié)果表明,CPU 占用僅為 20% 左右。

2)建立 9999 個(gè)空閑連接,及一個(gè)連接不斷執(zhí)行 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 
}

實(shí)驗(yàn)結(jié)果表明 CPU 能夠達(dá)到 80%,所以在連接數(shù)較高時(shí),盡量避免使用 info 命令。

3)pipeline 導(dǎo)致內(nèi)存占用高

有用戶發(fā)現(xiàn)在使用 pipeline 做只讀操作時(shí),redis-server 的內(nèi)存容量偶爾也會(huì)出現(xiàn)明顯的上漲, 這是對 pipeline 的使不當(dāng)造成的。下面先以一個(gè)簡單的例子來說明 Redis 的 pipeline 邏輯是怎樣的。

下面通過 golang 語言實(shí)現(xiàn)以 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 中的內(nèi)容以一特定的協(xié)議格式發(fā)送到 redis-server 端
 fmt.Println(redis.String(c.Receive()))
 fmt.Println(redis.String(c.Receive()))
 fmt.Println(redis.String(c.Receive()))
}

而此時(shí) server 端收到的內(nèi)容為:

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

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

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)
 }
}

考慮這樣一種情況:

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

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

盡量不要使用短連接;

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

使用 pipeline 時(shí),要及時(shí)接收請求處理結(jié)果,且 pipeline 不宜一次打包太多請求。

上述就是丸趣 TV 小編為大家分享的如何解碼 Redis 最易被忽視的 CPU 和內(nèi)存占用高問題了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注丸趣 TV 行業(yè)資訊頻道。

正文完
 
丸趣
版權(quán)聲明:本站原創(chuàng)文章,由 丸趣 2023-07-17發(fā)表,共計(jì)9000字。
轉(zhuǎn)載說明:除特殊說明外本站除技術(shù)相關(guān)以外文章皆由網(wǎng)絡(luò)搜集發(fā)布,轉(zhuǎn)載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 突泉县| 阿拉善左旗| 南康市| 新兴县| 南通市| 电白县| 甘孜县| 田林县| 峨山| 子长县| 屏东县| 永仁县| 外汇| 巩义市| 罗田县| 彰武县| 定南县| 乌鲁木齐县| 云霄县| 射洪县| 临汾市| 册亨县| 苏尼特右旗| 淳安县| 安康市| 乾安县| 万州区| 曲周县| 辽阳县| 望奎县| 杨浦区| 宁安市| 长垣县| 富裕县| 松江区| 西藏| 山阳县| 新建县| 黎城县| 洛扎县| 项城市|