共計 9000 個字符,預計需要花費 23 分鐘才能閱讀完成。
這期內容當中丸趣 TV 小編將會給大家帶來有關如何解碼 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,即主要是在處理來自用戶端的請求。
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-
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-
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 不宜一次打包太多請求。
上述就是丸趣 TV 小編為大家分享的如何解碼 Redis 最易被忽視的 CPU 和內存占用高問題了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注丸趣 TV 行業資訊頻道。